luau/tests/TypeInfer.test.cpp
2022-03-11 08:55:02 -08:00

5394 lines
132 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
#include <algorithm>
LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr)
LUAU_FASTFLAG(LuauEqConstraint)
using namespace Luau;
TEST_SUITE_BEGIN("TypeInfer");
TEST_CASE_FIXTURE(Fixture, "tc_hello_world")
{
CheckResult result = check("local a = 7");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId aType = requireType("a");
CHECK_EQ(getPrimitiveType(aType), PrimitiveTypeVar::Number);
}
TEST_CASE_FIXTURE(Fixture, "tc_propagation")
{
CheckResult result = check("local a = 7 local b = a");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId bType = requireType("b");
CHECK_EQ(getPrimitiveType(bType), PrimitiveTypeVar::Number);
}
TEST_CASE_FIXTURE(Fixture, "tc_error")
{
CheckResult result = check("local a = 7 local b = 'hi' a = b");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{
requireType("a"),
requireType("b"),
}}));
}
TEST_CASE_FIXTURE(Fixture, "tc_error_2")
{
CheckResult result = check("local a = 7 a = 'hi'");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{
requireType("a"),
typeChecker.stringType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "tc_function")
{
CheckResult result = check("function five() return 5 end");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* fiveType = get<FunctionTypeVar>(requireType("five"));
REQUIRE(fiveType != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value")
{
CheckResult result = check("local f = nil; f = 'hello world'");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId ty = requireType("f");
CHECK_EQ(getPrimitiveType(ty), PrimitiveTypeVar::String);
}
TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site")
{
CheckResult result = check(R"(
local a
function f(x) a = x end
f(1)
f("foo")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("number", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode")
{
CheckResult result = check(R"(
--!nocheck
function f(x)
return x
end
-- we get type information even if there's type errors
f(1, 2)
)");
CHECK_EQ("(any) -> (...any)", toString(requireType("f")));
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "check_function_bodies")
{
CheckResult result = check("function myFunction() local a = 0 a = true end");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 44}, Position{0, 48}}, TypeMismatch{
typeChecker.numberType,
typeChecker.booleanType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "infer_return_type")
{
CheckResult result = check("function take_five() return 5 end");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* takeFiveType = get<FunctionTypeVar>(requireType("take_five"));
REQUIRE(takeFiveType != nullptr);
std::vector<TypeId> retVec = flatten(takeFiveType->retType).first;
REQUIRE(!retVec.empty());
REQUIRE_EQ(*follow(retVec[0]), *typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "infer_from_function_return_type")
{
CheckResult result = check("function take_five() return 5 end local five = take_five()");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *follow(requireType("five")));
}
TEST_CASE_FIXTURE(Fixture, "cannot_call_primitives")
{
CheckResult result = check("local foo = 5 foo()");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CannotCallNonFunction>(result.errors[0]) != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "cannot_call_tables")
{
CheckResult result = check("local foo = {} foo()");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<CannotCallNonFunction>(result.errors[0]) != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "infer_that_function_does_not_return_a_table")
{
CheckResult result = check(R"(
function take_five()
return 5
end
take_five().prop = 888
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{5, 8}, Position{5, 24}}, NotATable{typeChecker.numberType}}));
}
TEST_CASE_FIXTURE(Fixture, "expr_statement")
{
CheckResult result = check("local foo = 5 foo()");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "generic_function")
{
CheckResult result = check(R"(
function id(x) return x end
local a = id(55)
local b = id(nil)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("a"));
CHECK_EQ(*typeChecker.nilType, *requireType("b"));
}
TEST_CASE_FIXTURE(Fixture, "vararg_functions_should_allow_calls_of_any_types_and_size")
{
CheckResult result = check(R"(
function f(...) end
f(1)
f("foo", 2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "vararg_function_is_quantified")
{
CheckResult result = check(R"(
local T = {}
function T.f(...)
local result = {}
for i = 1, select("#", ...) do
local dictionary = select(i, ...)
for key, value in pairs(dictionary) do
result[key] = value
end
end
return result
end
return T
)");
auto r = first(getMainModule()->getModuleScope()->returnType);
REQUIRE(r);
TableTypeVar* ttv = getMutable<TableTypeVar>(*r);
REQUIRE(ttv);
TypeId k = ttv->props["f"].type;
REQUIRE(k);
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "for_loop")
{
CheckResult result = check(R"(
local q
for i=0, 50, 2 do
q = i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("q"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop")
{
CheckResult result = check(R"(
local n
local s
for i, v in pairs({ "foo" }) do
n = i
s = v
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("n"));
CHECK_EQ(*typeChecker.stringType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_next")
{
CheckResult result = check(R"(
local n
local s
for i, v in next, { "foo", "bar" } do
n = i
s = v
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("n"));
CHECK_EQ(*typeChecker.stringType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_with_an_iterator_of_type_any")
{
CheckResult result = check(R"(
local it: any
local a, b
for i, v in it do
a, b = i, v
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_should_fail_with_non_function_iterator")
{
CheckResult result = check(R"(
local foo = "bar"
for i, v in foo do
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_with_just_one_iterator_is_ok")
{
CheckResult result = check(R"(
local function keys(dictionary)
local new = {}
local index = 1
for key in pairs(dictionary) do
new[index] = key
index = index + 1
end
return new
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_with_a_custom_iterator_should_type_check")
{
CheckResult result = check(R"(
local function range(l, h): () -> number
return function()
return l
end
end
for n: string in range(1, 10) do
print(n)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error")
{
CheckResult result = check(R"(
function f(x)
gobble.prop = x.otherprop
end
local p
for _, part in i_am_not_defined do
p = part
f(part)
part.thirdprop = false
end
)");
CHECK_EQ(2, result.errors.size());
TypeId p = requireType("p");
CHECK_EQ("*unknown*", toString(p));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_non_function")
{
CheckResult result = check(R"(
local bad_iter = 5
for a in bad_iter() do
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CannotCallNonFunction>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_factory_not_returning_the_right_amount_of_values")
{
CheckResult result = check(R"(
local function hasDivisors(value: number, table)
return false
end
function prime_iter(state, index)
while hasDivisors(index, state) do
index += 1
end
state[index] = true
return index
end
function primes1()
return prime_iter, {}
end
function primes2()
return prime_iter, {}, ""
end
function primes3()
return prime_iter, {}, 2
end
for p in primes1() do print(p) end -- mismatch in argument count
for p in primes2() do print(p) end -- mismatch in argument types, prime_iter takes {}, number, we are given {}, string
for p in primes3() do print(p) end -- no error
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Arg);
CHECK_EQ(2, acm->expected);
CHECK_EQ(1, acm->actual);
TypeMismatch* tm = get<TypeMismatch>(result.errors[1]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ(typeChecker.stringType, tm->givenType);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_iterator_requiring_args_but_none_given")
{
CheckResult result = check(R"(
function prime_iter(state, index)
return 1
end
for p in prime_iter do print(p) end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Arg);
CHECK_EQ(2, acm->expected);
CHECK_EQ(0, acm->actual);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any")
{
CheckResult result = check(R"(
function bar(): any
return true
end
local a
for b in bar do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(typeChecker.anyType, requireType("a"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2")
{
CheckResult result = check(R"(
function bar(): any
return true
end
local a
for b in bar() do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any")
{
CheckResult result = check(R"(
local bar: any
local a
for b in bar do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2")
{
CheckResult result = check(R"(
local bar: any
local a
for b in bar() do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error")
{
CheckResult result = check(R"(
local a
for b in bar do
a = b
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error2")
{
CheckResult result = check(R"(
function bar(c) return c end
local a
for b in bar() do
a = b
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_custom_iterator")
{
CheckResult result = check(R"(
function primes()
return function (state: number) end, 2
end
for p, q in primes do
q = ""
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ(typeChecker.stringType, tm->givenType);
}
TEST_CASE_FIXTURE(Fixture, "if_statement")
{
CheckResult result = check(R"(
local a
local b
if true then
a = 'hello'
else
b = 999
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.stringType, *requireType("a"));
CHECK_EQ(*typeChecker.numberType, *requireType("b"));
}
TEST_CASE_FIXTURE(Fixture, "while_loop")
{
CheckResult result = check(R"(
local i
while true do
i = 8
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("i"));
}
TEST_CASE_FIXTURE(Fixture, "repeat_loop")
{
CheckResult result = check(R"(
local i
repeat
i = 'hi'
until true
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.stringType, *requireType("i"));
}
TEST_CASE_FIXTURE(Fixture, "repeat_loop_condition_binds_to_its_block")
{
CheckResult result = check(R"(
repeat
local x = true
until x
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "symbols_in_repeat_block_should_not_be_visible_beyond_until_condition")
{
CheckResult result = check(R"(
repeat
local x = true
until x
print(x)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "table_length")
{
CheckResult result = check(R"(
local t = {}
local s = #t
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(nullptr != get<TableTypeVar>(requireType("t")));
CHECK_EQ(*typeChecker.numberType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "string_length")
{
CheckResult result = check(R"(
local s = "Hello, World!"
local t = #s
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "string_index")
{
CheckResult result = check(R"(
local s = "Hello, World!"
local t = s[4]
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NotATable* nat = get<NotATable>(result.errors[0]);
REQUIRE(nat);
CHECK_EQ("string", toString(nat->ty));
CHECK_EQ("*unknown*", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "length_of_error_type_does_not_produce_an_error")
{
CheckResult result = check(R"(
local l = #this_is_not_defined
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "indexing_error_type_does_not_produce_an_error")
{
CheckResult result = check(R"(
local originalReward = unknown.Parent.Reward:GetChildren()[1]
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_indexer")
{
CheckResult result = check("local a = {} a[0] = 7 a[0] = nil");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "wrong_assign_does_hit_indexer")
{
CheckResult result = check("local a = {} a[0] = 7 a[0] = 't'");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{
typeChecker.numberType,
typeChecker.stringType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_no_indexer")
{
CheckResult result = check("local a = {a=1, b=2} a['a'] = nil");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{
typeChecker.numberType,
typeChecker.nilType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "dot_on_error_type_does_not_produce_an_error")
{
CheckResult result = check(R"(
local foo = (true).x
foo.x = foo.y
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon")
{
CheckResult result = check(R"(
local someTable = {}
someTable.Function1 = function(Arg1)
end
someTable.Function1() -- Argument count mismatch
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CountMismatch>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2")
{
CheckResult result = check(R"(
local someTable = {}
someTable.Function2 = function(Arg1, Arg2)
end
someTable.Function2() -- Argument count mismatch
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CountMismatch>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_another_overload_works")
{
CheckResult result = check(R"(
type T = {method: ((T, number) -> number) & ((number) -> number)}
local T: T
T.method(4)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "list_only_alternative_overloads_that_match_argument_count")
{
CheckResult result = check(R"(
local multiply: ((number)->number) & ((number)->string) & ((number, number)->number)
multiply("")
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ(typeChecker.stringType, tm->givenType);
ExtraInformation* ei = get<ExtraInformation>(result.errors[1]);
REQUIRE(ei);
CHECK_EQ("Other overloads are also not viable: (number) -> string", ei->message);
}
TEST_CASE_FIXTURE(Fixture, "list_all_overloads_if_no_overload_takes_given_argument_count")
{
CheckResult result = check(R"(
local multiply: ((number)->number) & ((number)->string) & ((number, number)->number)
multiply()
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("No overload for function accepts 0 arguments.", ge->message);
ExtraInformation* ei = get<ExtraInformation>(result.errors[1]);
REQUIRE(ei);
CHECK_EQ("Available overloads: (number) -> number; (number) -> string; and (number, number) -> number", ei->message);
}
TEST_CASE_FIXTURE(Fixture, "dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists")
{
CheckResult result = check(R"(
local multiply: ((number)->number) & ((number)->string) & ((number, number)->number)
multiply(1, "")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ(typeChecker.stringType, tm->givenType);
}
TEST_CASE_FIXTURE(Fixture, "infer_return_type_from_selected_overload")
{
CheckResult result = check(R"(
type T = {method: ((T, number) -> number) & ((number) -> string)}
local T: T
local a = T.method(T, 4)
local b = T.method(5)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("string", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "too_many_arguments")
{
CheckResult result = check(R"(
--!nonstrict
function g(a: number) end
g()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = result.errors[0];
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(1, acm->expected);
CHECK_EQ(0, acm->actual);
}
TEST_CASE_FIXTURE(Fixture, "any_type_propagates")
{
CheckResult result = check(R"(
local foo: any
local bar = foo:method("argument")
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("bar")));
}
TEST_CASE_FIXTURE(Fixture, "can_subscript_any")
{
CheckResult result = check(R"(
local foo: any
local bar = foo[5]
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("bar")));
}
// Not strictly correct: metatables permit overriding this
TEST_CASE_FIXTURE(Fixture, "can_get_length_of_any")
{
CheckResult result = check(R"(
local foo: any = {}
local bar = #foo
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("bar")));
}
TEST_CASE_FIXTURE(Fixture, "recursive_function")
{
CheckResult result = check(R"(
function count(n: number)
if n == 0 then
return 0
else
return count(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "lambda_form_of_local_function_cannot_be_recursive")
{
CheckResult result = check(R"(
local f = function() return f() end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "recursive_local_function")
{
CheckResult result = check(R"(
local function count(n: number)
if n == 0 then
return 0
else
return count(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
// FIXME: This and the above case get handled very differently. It's pretty dumb.
// We really should unify the two code paths, probably by deleting AstStatFunction.
TEST_CASE_FIXTURE(Fixture, "another_recursive_local_function")
{
CheckResult result = check(R"(
local count
function count(n: number)
if n == 0 then
return 0
else
return count(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_rets")
{
CheckResult result = check(R"(
function f()
return f
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("t1 where t1 = () -> t1", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_args")
{
CheckResult result = check(R"(
function f(g)
return f(f)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("t1 where t1 = (t1) -> ()", toString(requireType("f")));
}
// TODO: File a Jira about this
/*
TEST_CASE_FIXTURE(Fixture, "unifying_vararg_pack_with_fixed_length_pack_produces_fixed_length_pack")
{
CheckResult result = check(R"(
function a(x) return 1 end
a(...)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE(bool(getMainModule()->getModuleScope()->varargPack));
TypePackId varargPack = *getMainModule()->getModuleScope()->varargPack;
auto iter = begin(varargPack);
auto endIter = end(varargPack);
CHECK(iter != endIter);
++iter;
CHECK(iter == endIter);
CHECK(!iter.tail());
}
*/
TEST_CASE_FIXTURE(Fixture, "method_depends_on_table")
{
CheckResult result = check(R"(
-- This catches a bug where x:m didn't count as a use of x
-- so toposort would happily reorder a definition of
-- function x:m before the definition of x.
function g() f() end
local x = {}
function x:m() end
function f() x:m() end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "another_higher_order_function")
{
CheckResult result = check(R"(
local Get_des
function Get_des(func)
Get_des(func)
end
local function f(d)
d:IsA("BasePart")
d.Parent:FindFirstChild("Humanoid")
d:IsA("Decal")
end
Get_des(f)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "another_other_higher_order_function")
{
CheckResult result = check(R"(
local d
d:foo()
d:foo()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted")
{
CheckResult result = check(R"(
function foo()
return bar(999), bar("hi")
end
function bar(i)
return i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "generic_table_method")
{
CheckResult result = check(R"(
local T = {}
function T:bar(i)
return i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId tType = requireType("T");
TableTypeVar* tTable = getMutable<TableTypeVar>(tType);
REQUIRE(tTable != nullptr);
TypeId barType = tTable->props["bar"].type;
REQUIRE(barType != nullptr);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(follow(barType));
REQUIRE_MESSAGE(ftv != nullptr, "Should be a function: " << *barType);
std::vector<TypeId> args = flatten(ftv->argTypes).first;
TypeId argType = args.at(1);
CHECK_MESSAGE(get<Unifiable::Generic>(argType), "Should be generic: " << *barType);
}
TEST_CASE_FIXTURE(Fixture, "correctly_instantiate_polymorphic_member_functions")
{
CheckResult result = check(R"(
local T = {}
function T:foo()
return T:bar(5)
end
function T:bar(i)
return i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
const TableTypeVar* t = get<TableTypeVar>(requireType("T"));
REQUIRE(t != nullptr);
std::optional<Property> fooProp = get(t->props, "foo");
REQUIRE(bool(fooProp));
const FunctionTypeVar* foo = get<FunctionTypeVar>(follow(fooProp->type));
REQUIRE(bool(foo));
std::optional<TypeId> ret_ = first(foo->retType);
REQUIRE(bool(ret_));
TypeId ret = follow(*ret_);
REQUIRE_EQ(getPrimitiveType(ret), PrimitiveTypeVar::Number);
}
TEST_CASE_FIXTURE(Fixture, "methods_are_topologically_sorted")
{
CheckResult result = check(R"(
local T = {}
function T:foo()
return T:bar(999), T:bar("hi")
end
function T:bar(i)
return i
end
local a, b = T:foo()
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("a")));
CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "local_function")
{
CheckResult result = check(R"(
function f()
return 8
end
function g()
local function f()
return 'hello'
end
return f
end
local h = g()
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId h = follow(requireType("h"));
const FunctionTypeVar* ftv = get<FunctionTypeVar>(h);
REQUIRE(ftv != nullptr);
std::optional<TypeId> rt = first(ftv->retType);
REQUIRE(bool(rt));
TypeId retType = follow(*rt);
CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(retType));
}
TEST_CASE_FIXTURE(Fixture, "unify_nearly_identical_recursive_types")
{
CheckResult result = check(R"(
local o
o:method()
local p
p:method()
o = p
)");
}
/*
* We had a bug in instantiation where the argument types of 'f' and 'g' would be inferred as
* f {+ method: function(<CYCLE>): (t2, T3...) +}
* g {+ method: function({+ method: function(<CYCLE>): (t2, T3...) +}): (t5, T6...) +}
*
* The type of 'g' is totally wrong as t2 and t5 should be unified, as should T3 with T6.
*
* The correct unification of the argument to 'g' is
*
* {+ method: function(<CYCLE>): (t5, T6...) +}
*/
TEST_CASE_FIXTURE(Fixture, "instantiate_cyclic_generic_function")
{
auto result = check(R"(
function f(o)
o:method()
end
function g(o)
f(o)
end
)");
TypeId g = requireType("g");
const FunctionTypeVar* gFun = get<FunctionTypeVar>(g);
REQUIRE(gFun != nullptr);
auto optionArg = first(gFun->argTypes);
REQUIRE(bool(optionArg));
TypeId arg = follow(*optionArg);
const TableTypeVar* argTable = get<TableTypeVar>(arg);
REQUIRE(argTable != nullptr);
std::optional<Property> methodProp = get(argTable->props, "method");
REQUIRE(bool(methodProp));
const FunctionTypeVar* methodFunction = get<FunctionTypeVar>(methodProp->type);
REQUIRE(methodFunction != nullptr);
std::optional<TypeId> methodArg = first(methodFunction->argTypes);
REQUIRE(bool(methodArg));
REQUIRE_EQ(follow(*methodArg), follow(arg));
}
TEST_CASE_FIXTURE(Fixture, "varlist_declared_by_for_in_loop_should_be_free")
{
CheckResult result = check(R"(
local T = {}
function T.f(p)
for i, v in pairs(p) do
T.f(v)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "properly_infer_iteratee_is_a_free_table")
{
// In this case, we cannot know the element type of the table {}. It could be anything.
// We therefore must initially ascribe a free typevar to iter.
CheckResult result = check(R"(
for iter in pairs({}) do
iter:g().p = true
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "quantify_methods_defined_using_dot_syntax_and_explicit_self_parameter")
{
check(R"(
local T = {}
function T.method(self)
self:method()
end
function T.method2(self)
self:method()
end
T:method2()
)");
}
TEST_CASE_FIXTURE(Fixture, "free_rhs_table_can_also_be_bound")
{
check(R"(
local o
local v = o:i()
function g(u)
v = u
end
o:f(g)
o:h()
o:h()
)");
}
TEST_CASE_FIXTURE(Fixture, "require")
{
fileResolver.source["game/A"] = R"(
local function hooty(x: number): string
return "Hi there!"
end
return {hooty=hooty}
)";
fileResolver.source["game/B"] = R"(
local Hooty = require(game.A)
local h -- free!
local i = Hooty.hooty(h)
)";
CheckResult aResult = frontend.check("game/A");
dumpErrors(aResult);
LUAU_REQUIRE_NO_ERRORS(aResult);
CheckResult bResult = frontend.check("game/B");
dumpErrors(bResult);
LUAU_REQUIRE_NO_ERRORS(bResult);
ModulePtr b = frontend.moduleResolver.modules["game/B"];
REQUIRE(b != nullptr);
dumpErrors(bResult);
std::optional<TypeId> iType = requireType(b, "i");
REQUIRE_EQ("string", toString(*iType));
std::optional<TypeId> hType = requireType(b, "h");
REQUIRE_EQ("number", toString(*hType));
}
TEST_CASE_FIXTURE(Fixture, "require_types")
{
fileResolver.source["workspace/A"] = R"(
export type Point = {x: number, y: number}
return {}
)";
fileResolver.source["workspace/B"] = R"(
local Hooty = require(workspace.A)
local h: Hooty.Point
)";
CheckResult bResult = frontend.check("workspace/B");
dumpErrors(bResult);
ModulePtr b = frontend.moduleResolver.modules["workspace/B"];
REQUIRE(b != nullptr);
TypeId hType = requireType(b, "h");
REQUIRE_MESSAGE(bool(get<TableTypeVar>(hType)), "Expected table but got " << toString(hType));
}
TEST_CASE_FIXTURE(Fixture, "require_a_variadic_function")
{
fileResolver.source["game/A"] = R"(
local T = {}
function T.f(...) end
return T
)";
fileResolver.source["game/B"] = R"(
local A = require(game.A)
local f = A.f
)";
CheckResult result = frontend.check("game/B");
ModulePtr bModule = frontend.moduleResolver.getModule("game/B");
REQUIRE(bModule != nullptr);
TypeId f = follow(requireType(bModule, "f"));
const FunctionTypeVar* ftv = get<FunctionTypeVar>(f);
REQUIRE(ftv);
auto iter = begin(ftv->argTypes);
auto endIter = end(ftv->argTypes);
REQUIRE(iter == endIter);
REQUIRE(iter.tail());
CHECK(get<VariadicTypePack>(*iter.tail()));
}
TEST_CASE_FIXTURE(Fixture, "assign_prop_to_table_by_calling_any_yields_any")
{
CheckResult result = check(R"(
local f: any
local T = {}
T.prop = f()
return T
)");
LUAU_REQUIRE_NO_ERRORS(result);
TableTypeVar* ttv = getMutable<TableTypeVar>(requireType("T"));
REQUIRE(ttv);
REQUIRE(ttv->props.count("prop"));
REQUIRE_EQ("any", toString(ttv->props["prop"].type));
}
TEST_CASE_FIXTURE(Fixture, "type_error_of_unknown_qualified_type")
{
CheckResult result = check(R"(
local p: SomeModule.DoesNotExist
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE_EQ(result.errors[0], (TypeError{Location{{1, 17}, {1, 40}}, UnknownSymbol{"SomeModule.DoesNotExist"}}));
}
TEST_CASE_FIXTURE(Fixture, "require_module_that_does_not_export")
{
const std::string sourceA = R"(
)";
const std::string sourceB = R"(
local Hooty = require(script.Parent.A)
)";
fileResolver.source["game/Workspace/A"] = sourceA;
fileResolver.source["game/Workspace/B"] = sourceB;
frontend.check("game/Workspace/A");
frontend.check("game/Workspace/B");
ModulePtr aModule = frontend.moduleResolver.modules["game/Workspace/A"];
ModulePtr bModule = frontend.moduleResolver.modules["game/Workspace/B"];
CHECK(aModule->errors.empty());
REQUIRE_EQ(1, bModule->errors.size());
CHECK_MESSAGE(get<IllegalRequire>(bModule->errors[0]), "Should be IllegalRequire: " << toString(bModule->errors[0]));
auto hootyType = requireType(bModule, "Hooty");
CHECK_EQ("*unknown*", toString(hootyType));
}
TEST_CASE_FIXTURE(Fixture, "warn_on_lowercase_parent_property")
{
CheckResult result = check(R"(
local M = require(script.parent.DoesNotMatter)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto ed = get<DeprecatedApiUsed>(result.errors[0]);
REQUIRE(ed);
REQUIRE_EQ("parent", ed->symbol);
}
TEST_CASE_FIXTURE(Fixture, "quantify_any_does_not_bind_to_itself")
{
CheckResult result = check(R"(
local A : any
function A.B() end
A:C()
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId aType = requireType("A");
CHECK_EQ(aType, typeChecker.anyType);
}
TEST_CASE_FIXTURE(Fixture, "table_unifies_into_map")
{
CheckResult result = check(R"(
local Instance: any
local UDim2: any
function Create(instanceType)
return function(data)
local obj = Instance.new(instanceType)
for k, v in pairs(data) do
if type(k) == 'number' then
--v.Parent = obj
else
obj[k] = v
end
end
return obj
end
end
local topbarShadow = Create'ImageLabel'{
Name = "TopBarShadow";
Size = UDim2.new(1, 0, 0, 3);
Position = UDim2.new(0, 0, 1, 0);
Image = "rbxasset://textures/ui/TopBar/dropshadow.png";
BackgroundTransparency = 1;
Active = false;
Visible = false;
};
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "func_expr_doesnt_leak_free")
{
CheckResult result = check(R"(
local p = function(x) return x end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const Luau::FunctionTypeVar* fn = get<FunctionTypeVar>(requireType("p"));
REQUIRE(fn);
auto ret = first(fn->retType);
REQUIRE(ret);
REQUIRE(get<GenericTypeVar>(follow(*ret)));
}
TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
{
CheckResult result = check(R"(
function foo(a, b)
return a(b)
end
function bar()
local c: ((number)->number, number)->number = foo -- no error
c = foo -- no error
local d: ((number)->number, string)->number = foo -- error from arg 2 (string) not being convertable to number from the call a(b)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType));
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2")
{
CheckResult result = check(R"(
function foo(a, b)
return a(b)
end
function bar()
local _: (string, string)->number = foo -- string cannot be converted to (string)->number
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("(string, string) -> number", toString(tm->wantedType));
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "string_method")
{
CheckResult result = check(R"(
local p = ("tacos"):len()
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("p"), *typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "string_function_indirect")
{
CheckResult result = check(R"(
local s:string
local l = s.lower
local p = l(s)
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("p"), *typeChecker.stringType);
}
TEST_CASE_FIXTURE(Fixture, "string_function_other")
{
CheckResult result = check(R"(
local s:string
local p = s:match("foo")
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(requireType("p")), "string?");
}
TEST_CASE_FIXTURE(Fixture, "weird_case")
{
CheckResult result = check(R"(
local function f() return 4 end
local d = math.deg(f())
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "or_joins_types")
{
CheckResult result = check(R"(
local s = "a" or 10
local x:string|number = s
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(*requireType("s")), "number | string");
CHECK_EQ(toString(*requireType("x")), "number | string");
}
TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras")
{
CheckResult result = check(R"(
local s = "a" or 10
local x:number|string = s
local y = x or "s"
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "number | string");
CHECK_EQ(toString(*requireType("y")), "number | string");
}
TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union")
{
CheckResult result = check(R"(
local s = "a" or "b"
local x:string = s
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("s"), *typeChecker.stringType);
}
TEST_CASE_FIXTURE(Fixture, "and_adds_boolean")
{
CheckResult result = check(R"(
local s = "a" and 10
local x:boolean|number = s
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "boolean | number");
}
TEST_CASE_FIXTURE(Fixture, "and_adds_boolean_no_superfluous_union")
{
CheckResult result = check(R"(
local s = "a" and true
local x:boolean = s
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("x"), *typeChecker.booleanType);
}
TEST_CASE_FIXTURE(Fixture, "and_or_ternary")
{
CheckResult result = check(R"(
local s = (1/2) > 0.5 and "a" or 10
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "number | string");
}
TEST_CASE_FIXTURE(Fixture, "first_argument_can_be_optional")
{
CheckResult result = check(R"(
local T = {}
function T.new(a: number?, b: number?, c: number?) return 5 end
local m = T.new()
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_ice_when_failing_the_occurs_check")
{
CheckResult result = check(R"(
--!strict
local s
s(s, 'a')
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "occurs_check_does_not_recurse_forever_if_asked_to_traverse_a_cyclic_type")
{
CheckResult result = check(R"(
--!strict
function u(t, w)
u(u, t)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
#if 0
// CLI-29798
TEST_CASE_FIXTURE(Fixture, "crazy_complexity")
{
CheckResult result = check(R"(
--!nonstrict
A:A():A():A():A():A():A():A():A():A():A():A()
)");
std::cout << "OK! Allocated " << typeChecker.typeVars.size() << " typevars" << std::endl;
}
#endif
// We had a bug where a cyclic union caused a stack overflow.
// ex type U = number | U
TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred")
{
CheckResult result = check(R"(
--!strict
function f(a, b)
a:g(b or {})
a:g(b)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "it_is_ok_not_to_supply_enough_retvals")
{
CheckResult result = check(R"(
function get_two() return 5, 6 end
local a = get_two()
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "duplicate_functions2")
{
CheckResult result = check(R"(
function foo() end
function bar()
local function foo() end
end
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "duplicate_functions_allowed_in_nonstrict")
{
CheckResult result = check(R"(
--!nonstrict
function foo() end
function foo() end
function bar()
local function foo() end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "duplicate_functions_with_different_signatures_not_allowed_in_nonstrict")
{
CheckResult result = check(R"(
--!nonstrict
function foo(): number
return 1
end
foo()
function foo(n: number): number
return 2
end
foo()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("() -> number", toString(tm->wantedType));
CHECK_EQ("(number) -> number", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "tables_get_names_from_their_locals")
{
CheckResult result = check(R"(
local T = {}
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("T", toString(requireType("T")));
}
TEST_CASE_FIXTURE(Fixture, "generalize_table_argument")
{
CheckResult result = check(R"(
function foo(arr)
local work = {}
for i = 1, #arr do
work[i] = arr[i]
end
return arr
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
const FunctionTypeVar* fooType = get<FunctionTypeVar>(requireType("foo"));
REQUIRE(fooType);
std::optional<TypeId> fooArg1 = first(fooType->argTypes);
REQUIRE(fooArg1);
const TableTypeVar* fooArg1Table = get<TableTypeVar>(*fooArg1);
REQUIRE(fooArg1Table);
CHECK_EQ(fooArg1Table->state, TableState::Generic);
}
TEST_CASE_FIXTURE(Fixture, "complicated_return_types_require_an_explicit_annotation")
{
CheckResult result = check(R"(
local i = 0
function most_of_the_natural_numbers(): number?
if i < 10 then
i = i + 1
return i
else
return nil
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* functionType = get<FunctionTypeVar>(requireType("most_of_the_natural_numbers"));
std::optional<TypeId> retType = first(functionType->retType);
REQUIRE(retType);
CHECK(get<UnionTypeVar>(*retType));
}
TEST_CASE_FIXTURE(Fixture, "infer_higher_order_function")
{
CheckResult result = check(R"(
function apply(f, x)
return f(x)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(requireType("apply"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(2, argVec.size());
const FunctionTypeVar* fType = get<FunctionTypeVar>(follow(argVec[0]));
REQUIRE(fType != nullptr);
std::vector<TypeId> fArgs = flatten(fType->argTypes).first;
TypeId xType = argVec[1];
CHECK_EQ(1, fArgs.size());
CHECK_EQ(xType, fArgs[0]);
}
TEST_CASE_FIXTURE(Fixture, "higher_order_function_2")
{
CheckResult result = check(R"(
function bottomupmerge(comp, a, b, left, mid, right)
local i, j = left, mid
for k = left, right do
if i < mid and (j > right or not comp(a[j], a[i])) then
b[k] = a[i]
i = i + 1
else
b[k] = a[j]
j = j + 1
end
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(requireType("bottomupmerge"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(6, argVec.size());
const FunctionTypeVar* fType = get<FunctionTypeVar>(follow(argVec[0]));
REQUIRE(fType != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "higher_order_function_3")
{
CheckResult result = check(R"(
function swap(p)
local t = p[0]
p[0] = p[1]
p[1] = t
return nil
end
function swapTwice(p)
swap(p)
swap(p)
return p
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(requireType("swapTwice"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(1, argVec.size());
const TableTypeVar* argType = get<TableTypeVar>(follow(argVec[0]));
REQUIRE(argType != nullptr);
CHECK(bool(argType->indexer));
}
TEST_CASE_FIXTURE(Fixture, "higher_order_function_4")
{
CheckResult result = check(R"(
function bottomupmerge(comp, a, b, left, mid, right)
local i, j = left, mid
for k = left, right do
if i < mid and (j > right or not comp(a[j], a[i])) then
b[k] = a[i]
i = i + 1
else
b[k] = a[j]
j = j + 1
end
end
end
function mergesort(arr, comp)
local work = {}
for i = 1, #arr do
work[i] = arr[i]
end
local width = 1
while width < #arr do
for i = 1, #arr, 2*width do
bottomupmerge(comp, arr, work, i, math.min(i+width, #arr), math.min(i+2*width-1, #arr))
end
local temp = work
work = arr
arr = temp
width = width * 2
end
return arr
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
/*
* mergesort takes two arguments: an array of some type T and a function that takes two Ts.
* We must assert that these two types are in fact the same type.
* In other words, comp(arr[x], arr[y]) is well-typed.
*/
const FunctionTypeVar* ftv = get<FunctionTypeVar>(requireType("mergesort"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(2, argVec.size());
const TableTypeVar* arg0 = get<TableTypeVar>(follow(argVec[0]));
REQUIRE(arg0 != nullptr);
REQUIRE(bool(arg0->indexer));
const FunctionTypeVar* arg1 = get<FunctionTypeVar>(follow(argVec[1]));
REQUIRE(arg1 != nullptr);
REQUIRE_EQ(2, size(arg1->argTypes));
std::vector<TypeId> arg1Args = flatten(arg1->argTypes).first;
CHECK_EQ(*arg0->indexer->indexResultType, *arg1Args[0]);
CHECK_EQ(*arg0->indexer->indexResultType, *arg1Args[1]);
}
TEST_CASE_FIXTURE(Fixture, "type_errors_infer_types")
{
CheckResult result = check(R"(
local err = (true).x
local c = err.Parent.Reward.GetChildren
local d = err.Parent.Reward
local e = err.Parent
local f = err
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownProperty* err = get<UnknownProperty>(result.errors[0]);
REQUIRE(err != nullptr);
CHECK_EQ("boolean", toString(err->table));
CHECK_EQ("x", err->key);
CHECK_EQ("*unknown*", toString(requireType("c")));
CHECK_EQ("*unknown*", toString(requireType("d")));
CHECK_EQ("*unknown*", toString(requireType("e")));
CHECK_EQ("*unknown*", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "calling_error_type_yields_error")
{
CheckResult result = check(R"(
local a = unknown.Parent.Reward.GetChildren()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownSymbol* err = get<UnknownSymbol>(result.errors[0]);
REQUIRE(err != nullptr);
CHECK_EQ("unknown", err->name);
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "chain_calling_error_type_yields_error")
{
CheckResult result = check(R"(
local a = Utility.Create "Foo" {}
)");
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable")
{
CheckResult result = check(R"(
function add(a: number, b: string)
return a + (tonumber(b) :: number), a .. b
end
local n, s = add(2,"3")
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* functionType = get<FunctionTypeVar>(requireType("add"));
std::optional<TypeId> retType = first(functionType->retType);
CHECK_EQ(std::optional<TypeId>(typeChecker.numberType), retType);
CHECK_EQ(requireType("n"), typeChecker.numberType);
CHECK_EQ(requireType("s"), typeChecker.stringType);
}
TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable_with_follows")
{
CheckResult result = check(R"(
local PI=3.1415926535897931
local SOLAR_MASS=4*PI * PI
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(requireType("SOLAR_MASS"), typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "primitive_arith_possible_metatable")
{
CheckResult result = check(R"(
function add(a: number, b: any)
return a + b
end
local t = add(1,2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "some_primitive_binary_ops")
{
CheckResult result = check(R"(
local a = 4 + 8
local b = a + 9
local s = 'hotdogs'
local t = s .. s
local c = b - a
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("b")));
CHECK_EQ("string", toString(requireType("s")));
CHECK_EQ("string", toString(requireType("t")));
CHECK_EQ("number", toString(requireType("c")));
}
TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
--!strict
local Vec3 = {}
Vec3.__index = Vec3
function Vec3.new()
return setmetatable({x=0, y=0, z=0}, Vec3)
end
export type Vec3 = typeof(Vec3.new())
local thefun: any = function(self, o) return self end
local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun
Vec3.__mul = multiply
local a = Vec3.new()
local b = Vec3.new()
local c = a * b
local d = a * 2
local e = a * 'cabbage'
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Vec3", toString(requireType("a")));
CHECK_EQ("Vec3", toString(requireType("b")));
CHECK_EQ("Vec3", toString(requireType("c")));
CHECK_EQ("Vec3", toString(requireType("d")));
CHECK_EQ("Vec3", toString(requireType("e")));
}
TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection_on_rhs")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
--!strict
local Vec3 = {}
Vec3.__index = Vec3
function Vec3.new()
return setmetatable({x=0, y=0, z=0}, Vec3)
end
export type Vec3 = typeof(Vec3.new())
local thefun: any = function(self, o) return self end
local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun
Vec3.__mul = multiply
local a = Vec3.new()
local b = Vec3.new()
local c = b * a
local d = 2 * a
local e = 'cabbage' * a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Vec3", toString(requireType("a")));
CHECK_EQ("Vec3", toString(requireType("b")));
CHECK_EQ("Vec3", toString(requireType("c")));
CHECK_EQ("Vec3", toString(requireType("d")));
CHECK_EQ("Vec3", toString(requireType("e")));
}
TEST_CASE_FIXTURE(Fixture, "compare_numbers")
{
CheckResult result = check(R"(
local a = 441
local b = 0
local c = a < b
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "compare_strings")
{
CheckResult result = check(R"(
local a = '441'
local b = '0'
local c = a < b
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_metatable")
{
CheckResult result = check(R"(
local a = {}
local b = {}
local c = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable");
}
TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators")
{
CheckResult result = check(R"(
local M = {}
function M.new()
return setmetatable({}, M)
end
type M = typeof(M.new())
local a = M.new()
local b = M.new()
local c = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt");
}
TEST_CASE_FIXTURE(Fixture, "cannot_compare_tables_that_do_not_have_the_same_metatable")
{
CheckResult result = check(R"(
--!strict
local M = {}
function M.new()
return setmetatable({}, M)
end
function M.__lt(left, right) return true end
local a = M.new()
local b = {}
local c = a < b -- line 10
local d = b < a -- line 11
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
REQUIRE_EQ((Location{{10, 18}, {10, 23}}), result.errors[0].location);
REQUIRE_EQ((Location{{11, 18}, {11, 23}}), result.errors[1].location);
}
TEST_CASE_FIXTURE(Fixture, "produce_the_correct_error_message_when_comparing_a_table_with_a_metatable_with_one_that_does_not")
{
CheckResult result = check(R"(
--!strict
local M = {}
function M.new()
return setmetatable({}, M)
end
function M.__lt(left, right) return true end
type M = typeof(M.new())
local a = M.new()
local b = {}
local c = a < b -- line 10
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<GenericError>(result.errors[0]);
REQUIRE(err != nullptr);
// Frail. :|
REQUIRE_EQ("Types M and b cannot be compared with < because they do not have the same metatable", err->message);
}
TEST_CASE_FIXTURE(Fixture, "in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators")
{
CheckResult result = check(R"(
--!nonstrict
function maybe_a_number(): number?
return 50
end
local a = maybe_a_number() < maybe_a_number()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
/*
* This test case exposed an oversight in the treatment of free tables.
* Free tables, like free TypeVars, need to record the scope depth where they were created so that
* we do not erroneously let-generalize them when they are used in a nested lambda.
*
* For more information about let-generalization, see <http://okmij.org/ftp/ML/generalization.html>
*
* The important idea here is that the return type of Counter.new is a table with some metatable.
* That metatable *must* be the same TypeVar as the type of Counter. If it is a copy (produced by
* the generalization process), then it loses the knowledge that its metatable will have an :incr()
* method.
*/
TEST_CASE_FIXTURE(Fixture, "dont_quantify_table_that_belongs_to_outer_scope")
{
CheckResult result = check(R"(
local Counter = {}
Counter.__index = Counter
function Counter.new()
local self = setmetatable({count=0}, Counter)
return self
end
function Counter:incr()
self.count = 1
return self.count
end
local self = Counter.new()
print(self:incr())
)");
LUAU_REQUIRE_NO_ERRORS(result);
TableTypeVar* counterType = getMutable<TableTypeVar>(requireType("Counter"));
REQUIRE(counterType);
const FunctionTypeVar* newType = get<FunctionTypeVar>(follow(counterType->props["new"].type));
REQUIRE(newType);
std::optional<TypeId> newRetType = *first(newType->retType);
REQUIRE(newRetType);
const MetatableTypeVar* newRet = get<MetatableTypeVar>(follow(*newRetType));
REQUIRE(newRet);
const TableTypeVar* newRetMeta = get<TableTypeVar>(newRet->metatable);
REQUIRE(newRetMeta);
CHECK(newRetMeta->props.count("incr"));
CHECK_EQ(follow(newRet->metatable), follow(requireType("Counter")));
}
// TODO: CLI-39624
TEST_CASE_FIXTURE(Fixture, "instantiate_tables_at_scope_level")
{
CheckResult result = check(R"(
--!strict
local Option = {}
Option.__index = Option
function Option.Is(obj)
return (type(obj) == "table" and getmetatable(obj) == Option)
end
return Option
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "typeguard_doesnt_leak_to_elseif")
{
const std::string code = R"(
function f(a)
if type(a) == "boolean" then
local a1 = a
elseif a.fn() then
local a2 = a
else
local a3 = a
end
end
)";
CheckResult result = check(code);
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "should_be_able_to_infer_this_without_stack_overflowing")
{
CheckResult result = check(R"(
local function f(x, y)
return x or y
end
local function dont_crash(x, y)
local z: typeof(f(x, y)) = f(x, y)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "inferring_hundreds_of_self_calls_should_not_suffocate_memory")
{
CheckResult result = check(R"(
("foo")
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
)");
ModulePtr module = getMainModule();
CHECK_GE(50, module->internalTypes.typeVars.size());
}
TEST_CASE_FIXTURE(Fixture, "inferring_crazy_table_should_also_be_quick")
{
CheckResult result = check(R"(
--!strict
function f(U)
U(w:s(an):c()():c():U(s):c():c():U(s):c():U(s):cU()):c():U(s):c():U(s):c():c():U(s):c():U(s):cU()
end
)");
ModulePtr module = getMainModule();
CHECK_GE(100, module->internalTypes.typeVars.size());
}
TEST_CASE_FIXTURE(Fixture, "exponential_blowup_from_copying_types")
{
CheckResult result = check(R"(
--!strict
-- An example of exponential blowup in number of types
-- The problem is that if we define function f(a) return x end
-- then this has type <t>(t)->T where x:T
-- *but* it copies T each time f is applied
-- so { left = f("hi"), right = f(5) }
-- has type { left : T_L, right : T_R }
-- where T_L and T_R are copies of T.
-- x0 : T0 where T0 = {}
local x0 = {}
-- f0 : <t>(t)->T0
local function f0(a) return x0 end
-- x1 : T1 where T1 = { left : T0_L, right : T0_R }
local x1 = { left = f0("hi"), right = f0(5) }
-- f1 : <t>(t)->T1
local function f1(a) return x1 end
-- x2 : T2 where T2 = { left : T1_L, right : T1_R }
local x2 = { left = f1("hi"), right = f1(5) }
-- f2 : <t>(t)->T2
local function f2(a) return x2 end
-- etc etc
local x3 = { left = f2("hi"), right = f2(5) }
local function f3(a) return x3 end
local x4 = { left = f3("hi"), right = f3(5) }
return x4
)");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr module = getMainModule();
// If we're not careful about copying, this ends up with O(2^N) types rather than O(N)
// (in this case 5 vs 31).
CHECK_GE(5, module->interfaceTypes.typeVars.size());
}
TEST_CASE_FIXTURE(Fixture, "mutual_recursion")
{
CheckResult result = check(R"(
--!strict
function newPlayerCharacter()
startGui() -- Unknown symbol 'startGui'
end
local characterAddedConnection: any
function startGui()
characterAddedConnection = game:GetService("Players").LocalPlayer.CharacterAdded:connect(newPlayerCharacter)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "toposort_doesnt_break_mutual_recursion")
{
CheckResult result = check(R"(
--!strict
local x = nil
function f() g() end
-- make sure print(x) doesn't get toposorted here, breaking the mutual block
function g() x = f end
print(x)
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "object_constructor_can_refer_to_method_of_self")
{
// CLI-30902
CheckResult result = check(R"(
--!strict
type Foo = {
fooConn: () -> () | nil
}
local Foo = {}
Foo.__index = Foo
function Foo.new()
local self: Foo = {
fooConn = nil,
}
setmetatable(self, Foo)
self.fooConn = function()
self:method() -- Key 'method' not found in table self
end
return self
end
function Foo:method()
print("foo")
end
local foo = Foo.new()
-- TODO This is the best our current refinement support can offer :(
local bar = foo.fooConn
if bar then bar() end
-- foo.fooConn()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "replace_every_free_type_when_unifying_a_complex_function_with_any")
{
CheckResult result = check(R"(
local a: any
local b
for _, i in pairs(a) do
b = i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("b")));
}
// In these tests, a successful parse is required, so we need the parser to return the AST and then we can test the recursion depth limit in type
// checker. We also want it to somewhat match up with production values, so we push up the parser recursion limit a little bit instead.
TEST_CASE_FIXTURE(Fixture, "check_type_infer_recursion_limit")
{
#if defined(LUAU_ENABLE_ASAN)
int limit = 250;
#elif defined(_DEBUG) || defined(_NOOPT)
int limit = 350;
#else
int limit = 600;
#endif
ScopedFastInt luauRecursionLimit{"LuauRecursionLimit", limit + 100};
ScopedFastInt luauTypeInferRecursionLimit{"LuauTypeInferRecursionLimit", limit - 100};
ScopedFastInt luauCheckRecursionLimit{"LuauCheckRecursionLimit", 0};
CHECK_NOTHROW(check("print('Hello!')"));
CHECK_THROWS_AS(check("function f() return " + rep("{a=", limit) + "'a'" + rep("}", limit) + " end"), std::runtime_error);
}
TEST_CASE_FIXTURE(Fixture, "check_block_recursion_limit")
{
#if defined(LUAU_ENABLE_ASAN)
int limit = 250;
#elif defined(_DEBUG) || defined(_NOOPT)
int limit = 350;
#else
int limit = 600;
#endif
ScopedFastInt luauRecursionLimit{"LuauRecursionLimit", limit + 100};
ScopedFastInt luauCheckRecursionLimit{"LuauCheckRecursionLimit", limit - 100};
CheckResult result = check(rep("do ", limit) + "local a = 1" + rep(" end", limit));
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(nullptr != get<CodeTooComplex>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "check_expr_recursion_limit")
{
#if defined(LUAU_ENABLE_ASAN)
int limit = 250;
#elif defined(_DEBUG) || defined(_NOOPT)
int limit = 350;
#else
int limit = 600;
#endif
ScopedFastInt luauRecursionLimit{"LuauRecursionLimit", limit + 100};
ScopedFastInt luauCheckRecursionLimit{"LuauCheckRecursionLimit", limit - 100};
CheckResult result = check(R"(("foo"))" + rep(":lower()", limit));
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(nullptr != get<CodeTooComplex>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_basic")
{
CheckResult result = check(R"(
local s = 10
s += 20
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "number");
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_op")
{
CheckResult result = check(R"(
local s = 10
s += true
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{{2, 13}, {2, 17}}, TypeMismatch{typeChecker.numberType, typeChecker.booleanType}}));
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_result")
{
CheckResult result = check(R"(
local s = 'hello'
s += 10
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(result.errors[0], (TypeError{Location{{2, 8}, {2, 9}}, TypeMismatch{typeChecker.numberType, typeChecker.stringType}}));
CHECK_EQ(result.errors[1], (TypeError{Location{{2, 8}, {2, 15}}, TypeMismatch{typeChecker.stringType, typeChecker.numberType}}));
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_metatable")
{
CheckResult result = check(R"(
--!strict
type V2B = { x: number, y: number }
local v2b: V2B = { x = 0, y = 0 }
local VMT = {}
type V2 = typeof(setmetatable(v2b, VMT))
function VMT.__add(a: V2, b: V2): V2
return setmetatable({ x = a.x + b.x, y = a.y + b.y }, VMT)
end
local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT)
local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT)
v1 += v2
)");
CHECK_EQ(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_metatable")
{
CheckResult result = check(R"(
--!strict
type V2B = { x: number, y: number }
local v2b: V2B = { x = 0, y = 0 }
local VMT = {}
type V2 = typeof(setmetatable(v2b, VMT))
function VMT.__mod(a: V2, b: V2): number
return a.x * b.x + a.y * b.y
end
local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT)
local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT)
v1 %= v2
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
CHECK_EQ(*tm->wantedType, *requireType("v2"));
CHECK_EQ(*tm->givenType, *typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "dont_ice_if_a_TypePack_is_an_error")
{
CheckResult result = check(R"(
--!strict
function f(s)
print(s)
return f
end
f("foo")("bar")
)");
}
TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it")
{
CheckResult result = check(R"(
--!nonstrict
function f()
return 114
end
return function()
return f():andThen()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "it_is_ok_to_oversaturate_a_higher_order_function_argument")
{
CheckResult result = check(R"(
function onerror() end
function foo() end
xpcall(foo, onerror)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "another_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments")
{
CheckResult result = check(R"(
local mycb: (number, number) -> ()
function f() end
mycb = f
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "call_to_any_yields_any")
{
CheckResult result = check(R"(
local a: any
local b = a()
)");
REQUIRE_EQ("any", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "globals")
{
CheckResult result = check(R"(
--!nonstrict
foo = true
foo = "now i'm a string!"
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "globals2")
{
CheckResult result = check(R"(
--!nonstrict
foo = function() return 1 end
foo = "now i'm a string!"
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("() -> (...any)", toString(tm->wantedType));
CHECK_EQ("string", toString(tm->givenType));
CHECK_EQ("() -> (...any)", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "globals_are_banned_in_strict_mode")
{
CheckResult result = check(R"(
--!strict
foo = true
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownSymbol* us = get<UnknownSymbol>(result.errors[0]);
REQUIRE(us);
CHECK_EQ("foo", us->name);
}
TEST_CASE_FIXTURE(Fixture, "globals_everywhere")
{
CheckResult result = check(R"(
--!nonstrict
foo = 1
if true then
bar = 2
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("foo")));
CHECK_EQ("any", toString(requireType("bar")));
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfAny")
{
CheckResult result = check(R"(
local x: any = {}
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfSealed")
{
CheckResult result = check(R"(
local x: {prop: number} = {prop=9999}
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfNumber")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
local x: number = 9999
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfError")
{
CheckResult result = check(R"(
local x = (true).foo
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "CallOrOfFunctions")
{
CheckResult result = check(R"(
function f() return 1; end
function g() return 2; end
(f or g)()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "CallAndOrOfFunctions")
{
CheckResult result = check(R"(
function f() return 1; end
function g() return 2; end
local x = false
(x and f or g)()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "MixedPropertiesAndIndexers")
{
CheckResult result = check(R"(
local x = {}
x.a = "a"
x[0] = true
x.b = 37
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_do")
{
CheckResult result = check(R"(
do
local a = 1
end
print(a) -- oops!
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownSymbol* us = get<UnknownSymbol>(result.errors[0]);
REQUIRE(us);
CHECK_EQ(us->name, "a");
}
TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_while")
{
CheckResult result = check(R"(
while true do
local a = 1
end
print(a) -- oops!
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownSymbol* us = get<UnknownSymbol>(result.errors[0]);
REQUIRE(us);
CHECK_EQ(us->name, "a");
}
TEST_CASE_FIXTURE(Fixture, "ipairs_produces_integral_indices")
{
CheckResult result = check(R"(
local key
for i, e in ipairs({}) do key = i end
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("number", toString(requireType("key")));
}
TEST_CASE_FIXTURE(Fixture, "checking_should_not_ice")
{
CHECK_NOTHROW(check(R"(
--!nonstrict
f,g = ...
f(g(...))[...] = nil
f,xpcall = ...
local value = g(...)(g(...))
)"));
CHECK_EQ("any", toString(requireType("value")));
}
TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_nonstrict")
{
CheckResult result = check(R"(
--!nonstrict
local function f1(v): number?
if v then
return 1
end
end
local function f2(v)
if v then
return 1
end
end
local function f3(v): ()
if v then
return
end
end
local function f4(v)
if v then
return
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
FunctionExitsWithoutReturning* err = get<FunctionExitsWithoutReturning>(result.errors[0]);
CHECK(err);
}
TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_strict")
{
CheckResult result = check(R"(
--!strict
local function f1(v): number?
if v then
return 1
end
end
local function f2(v)
if v then
return 1
end
end
local function f3(v): ()
if v then
return
end
end
local function f4(v)
if v then
return
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
FunctionExitsWithoutReturning* annotatedErr = get<FunctionExitsWithoutReturning>(result.errors[0]);
CHECK(annotatedErr);
FunctionExitsWithoutReturning* inferredErr = get<FunctionExitsWithoutReturning>(result.errors[1]);
CHECK(inferredErr);
}
// TEST_CASE_FIXTURE(Fixture, "infer_method_signature_of_argument")
// {
// CheckResult result = check(R"(
// function f(a)
// if a.cond then
// return a.method()
// end
// end
// )");
// LUAU_REQUIRE_NO_ERRORS(result);
// CHECK_EQ("A", toString(requireType("f")));
// }
TEST_CASE_FIXTURE(Fixture, "warn_if_you_try_to_require_a_non_modulescript")
{
fileResolver.source["Modules/A"] = "";
fileResolver.sourceTypes["Modules/A"] = SourceCode::Local;
fileResolver.source["Modules/B"] = R"(
local M = require(script.Parent.A)
)";
CheckResult result = frontend.check("Modules/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<IllegalRequire>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "calling_function_with_incorrect_argument_type_yields_errors_spanning_argument")
{
CheckResult result = check(R"(
function foo(a: number, b: string) end
foo("Test", 123)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{3, 12}, Position{3, 18}}, TypeMismatch{
typeChecker.numberType,
typeChecker.stringType,
}}));
CHECK_EQ(result.errors[1], (TypeError{Location{Position{3, 20}, Position{3, 23}}, TypeMismatch{
typeChecker.stringType,
typeChecker.numberType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "calling_function_with_anytypepack_doesnt_leak_free_types")
{
CheckResult result = check(R"(
--!nonstrict
function Test(a)
return 1, ""
end
local tab = {}
table.insert(tab, Test(1));
)");
LUAU_REQUIRE_NO_ERRORS(result);
ToStringOptions opts;
opts.exhaustive = true;
opts.maxTableLength = 0;
CHECK_EQ("{any}", toString(requireType("tab"), opts));
}
TEST_CASE_FIXTURE(Fixture, "too_many_return_values")
{
CheckResult result = check(R"(
--!strict
function f()
return 55
end
local a, b = f()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Result);
CHECK_EQ(acm->expected, 1);
CHECK_EQ(acm->actual, 2);
}
TEST_CASE_FIXTURE(Fixture, "ignored_return_values")
{
CheckResult result = check(R"(
--!strict
function f()
return 55, ""
end
local a = f()
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "function_does_not_return_enough_values")
{
CheckResult result = check(R"(
--!strict
function f(): (number, string)
return 55
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Return);
CHECK_EQ(acm->expected, 2);
CHECK_EQ(acm->actual, 1);
}
TEST_CASE_FIXTURE(Fixture, "typecheck_unary_minus")
{
CheckResult result = check(R"(
--!strict
local foo = {
value = 10
}
local mt = {}
setmetatable(foo, mt)
mt.__unm = function(val: typeof(foo)): string
return val.value .. "test"
end
local a = -foo
local b = 1+-1
local bar = {
value = 10
}
local c = -bar -- disallowed
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("string", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("b")));
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE_EQ(gen->message, "Unary operator '-' not supported by type 'bar'");
}
TEST_CASE_FIXTURE(Fixture, "unary_not_is_boolean")
{
CheckResult result = check(R"(
local b = not "string"
local c = not (math.random() > 0.5 and "string" or 7)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("boolean", toString(requireType("b")));
REQUIRE_EQ("boolean", toString(requireType("c")));
}
TEST_CASE_FIXTURE(Fixture, "disallow_string_and_types_without_metatables_from_arithmetic_binary_ops")
{
CheckResult result = check(R"(
--!strict
local a = "1.24" + 123 -- not allowed
local foo = {
value = 10
}
local b = foo + 1 -- not allowed
local bar = {
value = 1
}
local mt = {}
setmetatable(bar, mt)
mt.__add = function(a: typeof(bar), b: number): number
return a.value + b
end
local c = bar + 1 -- allowed
local d = bar + foo -- not allowed
)");
LUAU_REQUIRE_ERROR_COUNT(3, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE_EQ(*tm->wantedType, *typeChecker.numberType);
REQUIRE_EQ(*tm->givenType, *typeChecker.stringType);
TypeMismatch* tm2 = get<TypeMismatch>(result.errors[2]);
CHECK_EQ(*tm2->wantedType, *typeChecker.numberType);
CHECK_EQ(*tm2->givenType, *requireType("foo"));
GenericError* gen2 = get<GenericError>(result.errors[1]);
REQUIRE_EQ(gen2->message, "Binary operator '+' not supported by types 'foo' and 'number'");
}
// CLI-29033
TEST_CASE_FIXTURE(Fixture, "unknown_type_in_comparison")
{
CheckResult result = check(R"(
function merge(lower, greater)
if lower.y == greater.y then
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "concat_op_on_free_lhs_and_string_rhs")
{
CheckResult result = check(R"(
local function f(x)
return x .. "y"
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CannotInferBinaryOperation>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "concat_op_on_string_lhs_and_free_rhs")
{
CheckResult result = check(R"(
local function f(x)
return "foo" .. x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(string) -> string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown")
{
std::vector<std::string> ops = {"+", "-", "*", "/", "%", "^", ".."};
std::string src = R"(
function foo(a, b)
)";
for (const auto& op : ops)
src += "local _ = a " + op + "b\n";
src += "end";
CheckResult result = check(src);
LUAU_REQUIRE_ERROR_COUNT(ops.size(), result);
CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'a'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "function_cast_error_uses_correct_language")
{
CheckResult result = check(R"(
function foo(a, b): number
return 0
end
local a: (string)->number = foo
local b: (number, number)->(number, number) = foo
local c: (string, number)->number = foo -- no error
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
auto tm1 = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm1);
CHECK_EQ("(string) -> number", toString(tm1->wantedType));
CHECK_EQ("(string, *unknown*) -> number", toString(tm1->givenType));
auto tm2 = get<TypeMismatch>(result.errors[1]);
REQUIRE(tm2);
CHECK_EQ("(number, number) -> (number, number)", toString(tm2->wantedType));
CHECK_EQ("(string, *unknown*) -> number", toString(tm2->givenType));
}
TEST_CASE_FIXTURE(Fixture, "setmetatable_cant_be_used_to_mutate_global_types")
{
{
Fixture fix;
// inherit env from parent fixture checker
fix.typeChecker.globalScope = typeChecker.globalScope;
fix.check(R"(
--!nonstrict
type MT = typeof(setmetatable)
function wtf(arg: {MT}): typeof(table)
arg = wtf(arg)
end
)");
}
// validate sharedEnv post-typecheck; valuable for debugging some typeck crashes but slows fuzzing down
// note: it's important for typeck to be destroyed at this point!
{
for (auto& p : typeChecker.globalScope->bindings)
{
toString(p.second.typeId); // toString walks the entire type, making sure ASAN catches access to destroyed type arenas
}
}
}
TEST_CASE_FIXTURE(Fixture, "evil_table_unification")
{
// this code re-infers the type of _ while processing fields of _, which can cause use-after-free
check(R"(
--!nonstrict
_ = ...
_:table(_,string)[_:gsub(_,...,n0)],_,_:gsub(_,string)[""],_:split(_,...,table)._,n0 = nil
do end
)");
}
TEST_CASE_FIXTURE(Fixture, "overload_is_not_a_function")
{
check(R"(
--!nonstrict
function _(...):((typeof(not _))&(typeof(not _)))&((typeof(not _))&(typeof(not _)))
_(...)(setfenv,_,not _,"")[_] = nil
end
do end
_(...)(...,setfenv,_):_G()
)");
}
TEST_CASE_FIXTURE(Fixture, "cyclic_type_packs")
{
// this has a risk of creating cyclic type packs, causing infinite loops / OOMs
check(R"(
--!nonstrict
_ += _(_,...)
repeat
_ += _(...)
until ... + _
)");
check(R"(
--!nonstrict
_ += _(_(...,...),_(...))
repeat
until _
)");
}
TEST_CASE_FIXTURE(Fixture, "cyclic_follow")
{
check(R"(
--!nonstrict
l0,table,_,_,_ = ...
_,_,_,_.time(...)._.n0,l0,_ = function(l0)
end,_.__index,(_),_.time(_.n0 or _,...)
for l0=...,_,"" do
end
_ += not _
do end
)");
check(R"(
--!nonstrict
n13,_,table,_,l0,_,_ = ...
_,n0[(_)],_,_._(...)._.n39,l0,_._ = function(l84,...)
end,_.__index,"",_,l0._(nil)
for l0=...,table.n5,_ do
end
_:_(...).n1 /= _
do
_(_ + _)
do end
end
)");
}
TEST_CASE_FIXTURE(Fixture, "and_binexps_dont_unify")
{
CheckResult result = check(R"(
--!strict
local t = {}
while true and t[1] do
print(t[1].test)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
struct FindFreeTypeVars
{
bool foundOne = false;
template<typename ID>
void cycle(ID)
{
}
template<typename ID, typename T>
bool operator()(ID, T)
{
return !foundOne;
}
template<typename ID>
bool operator()(ID, Unifiable::Free)
{
foundOne = true;
return false;
}
};
TEST_CASE_FIXTURE(Fixture, "dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar")
{
CheckResult result = check("local x = setmetatable({})");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_where_iteratee_is_free")
{
// This code doesn't pass typechecking. We just care that it doesn't crash.
(void)check(R"(
--!nonstrict
function _:_(...)
end
repeat
if _ then
else
_ = ...
end
until _
for _ in _() do
end
)");
}
TEST_CASE_FIXTURE(Fixture, "tc_after_error_recovery")
{
CheckResult result = check(R"(
local x =
local a = 7
)");
LUAU_REQUIRE_ERRORS(result);
TypeId aType = requireType("a");
CHECK_EQ(getPrimitiveType(aType), PrimitiveTypeVar::Number);
}
// Check that type checker knows about error expressions
TEST_CASE_FIXTURE(Fixture, "tc_after_error_recovery_no_assert")
{
CheckResult result = check("function +() local _ = true end");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "tc_after_error_recovery_no_replacement_name_in_error")
{
{
CheckResult result = check(R"(
--!strict
local t = { x = 10, y = 20 }
return t.
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
{
CheckResult result = check(R"(
--!strict
export type = number
export type = string
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
{
CheckResult result = check(R"(
--!strict
function string.() end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
{
CheckResult result = check(R"(
--!strict
local function () end
local function () end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
{
CheckResult result = check(R"(
--!strict
local dm = {}
function dm.() end
function dm.() end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators")
{
CheckResult result = check(R"(
local a: boolean = true
local b: boolean = false
local foo = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message);
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2")
{
CheckResult result = check(R"(
local a: number | string = ""
local b: number | string = 1
local foo = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message);
}
TEST_CASE_FIXTURE(Fixture, "index_expr_should_be_checked")
{
CheckResult result = check(R"(
local foo: any
print(foo[(true).x])
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownProperty* up = get<UnknownProperty>(result.errors[0]); // Should probably be NotATable
REQUIRE(up);
CHECK_EQ("boolean", toString(up->table));
CHECK_EQ("x", up->key);
}
TEST_CASE_FIXTURE(Fixture, "unreachable_code_after_infinite_loop")
{
{
CheckResult result = check(R"(
function unreachablecodepath(a): number
while true do
if a then return 10 end
end
-- unreachable
end
unreachablecodepath(4)
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
{
CheckResult result = check(R"(
function reachablecodepath(a): number
while true do
if a then break end
return 10
end
print("x") -- correct error
end
reachablecodepath(4)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK(get<FunctionExitsWithoutReturning>(result.errors[0]));
}
{
CheckResult result = check(R"(
function unreachablecodepath(a): number
repeat
if a then return 10 end
until false
-- unreachable
end
unreachablecodepath(4)
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
{
CheckResult result = check(R"(
function reachablecodepath(a, b): number
repeat
if a then break end
if b then return 10 end
until false
print("x") -- correct error
end
reachablecodepath(4)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK(get<FunctionExitsWithoutReturning>(result.errors[0]));
}
{
CheckResult result = check(R"(
function unreachablecodepath(a: number?): number
repeat
return 10
until a ~= nil
-- unreachable
end
unreachablecodepath(4)
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
}
TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union")
{
CheckResult result = check(R"(
--!strict
local _
_ += _ and _ or _ and _ or _ and _
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type contains a self-recursive construct that cannot be resolved", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "stringify_nested_unions_with_optionals")
{
CheckResult result = check(R"(
--!strict
local a: number | (string | boolean) | nil
local b: number = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ("(boolean | number | string)?", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "UnknownGlobalCompoundAssign")
{
// In non-strict mode, global definition is still allowed
{
CheckResult result = check(R"(
--!nonstrict
a = a + 1
print(a)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'");
}
// In strict mode we no longer generate two errors from lhs
{
CheckResult result = check(R"(
--!strict
a += 1
print(a)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'");
}
// In non-strict mode, compound assignment is not a definition, it's a modification
{
CheckResult result = check(R"(
--!nonstrict
a += 1
print(a)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'");
}
}
TEST_CASE_FIXTURE(Fixture, "loop_typecheck_crash_on_empty_optional")
{
CheckResult result = check(R"(
local t = {}
for _ in t do
for _ in assert(missing()) do
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(Fixture, "cli_39932_use_unifier_in_ensure_methods")
{
CheckResult result = check(R"(
local x: {number|number} = {1, 2, 3}
local y = x[1] - x[2]
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "metatable_of_any_can_be_a_table")
{
CheckResult result = check(R"(
--!strict
local T: any
T = {}
T.__index = T
function T.new(...)
local self = {}
setmetatable(self, T)
self:construct(...)
return self
end
function T:construct(index)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_report_type_errors_within_an_AstStatError")
{
CheckResult result = check(R"(
foo
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "dont_report_type_errors_within_an_AstExprError")
{
CheckResult result = check(R"(
local a = foo:
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(Fixture, "dont_ice_on_astexprerror")
{
CheckResult result = check(R"(
local foo = -;
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator")
{
CheckResult result = check(R"(
--!strict
local a: number? = nil
local b: number = a or 1
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator2")
{
CheckResult result = check(R"(
--!nonstrict
local a: number? = nil
local b: number = a or 1
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_strip_nil_from_rhs_or_operator")
{
CheckResult result = check(R"(
--!strict
local a: number? = nil
local b: number = 1 or a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ("number?", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "no_lossy_function_type")
{
CheckResult result = check(R"(
--!strict
local tbl = {}
function tbl:abc(a: number, b: number)
return a
end
tbl:abc(1, 2) -- Line 6
-- | Column 14
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId type = requireTypeAtPosition(Position(6, 14));
CHECK_EQ("(tbl, number, number) -> number", toString(type));
auto ftv = get<FunctionTypeVar>(type);
REQUIRE(ftv);
CHECK(ftv->hasSelf);
}
TEST_CASE_FIXTURE(Fixture, "luau_resolves_symbols_the_same_way_lua_does")
{
CheckResult result = check(R"(
--!strict
function Funky()
local a: number = foo
end
local foo: string = 'hello'
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto e = result.errors.front();
REQUIRE_MESSAGE(get<UnknownSymbol>(e) != nullptr, "Expected UnknownSymbol, but got " << e);
}
TEST_CASE_FIXTURE(Fixture, "operator_eq_verifies_types_do_intersect")
{
CheckResult result = check(R"(
type Array<T> = { [number]: T }
type Fiber = { id: number }
type null = {}
local fiberStack: Array<Fiber | null> = {}
local index = 0
local function f(fiber: Fiber)
local a = fiber ~= fiberStack[index]
local b = fiberStack[index] ~= fiber
end
return f
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "general_require_call_expression")
{
fileResolver.source["game/A"] = R"(
--!strict
return { def = 4 }
)";
fileResolver.source["game/B"] = R"(
--!strict
local tbl = { abc = require(game.A) }
local a : string = ""
a = tbl.abc.def
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "general_require_type_mismatch")
{
fileResolver.source["game/A"] = R"(
return { def = 4 }
)";
fileResolver.source["game/B"] = R"(
local tbl: string = require(game.A)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "nonstrict_self_mismatch_tail")
{
CheckResult result = check(R"(
--!nonstrict
local f = {}
function f:foo(a: number, b: number) end
function bar(...)
f.foo(f, 1, ...)
end
bar(2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "typeof_unresolved_function")
{
CheckResult result = check(R"(
local function f(a: typeof(f)) end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Unknown global 'f'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning")
{
CheckResult result = check(R"(
--!nonstrict
local l0:any,l61:t0<t32> = _,math
while _ do
_()
end
function _():t0<t0>
end
type t0<t32> = any
)");
std::optional<TypeId> ty = requireType("math");
REQUIRE(ty);
const TableTypeVar* ttv = get<TableTypeVar>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
}
TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_2")
{
ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true};
CheckResult result = check(R"(
type X<T> = T
type K = X<typeof(math)>
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> ty = requireType("math");
REQUIRE(ty);
const TableTypeVar* ttv = get<TableTypeVar>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
}
TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_3")
{
ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true};
CheckResult result = check(R"(
type X<T> = T
local a = {}
a.x = 4
local b: X<typeof(a)>
a.y = 5
local c: X<typeof(a)>
c = b
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> ty = requireType("a");
REQUIRE(ty);
const TableTypeVar* ttv = get<TableTypeVar>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
}
TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok")
{
CheckResult result = check(R"(
local n = {}
function n:Clone() end
local m = {}
function m.a(x)
x:Clone()
end
function m.b()
m.a(n)
end
return m
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "self_recursive_instantiated_param")
{
ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true};
// Mutability in type function application right now can create strange recursive types
CheckResult result = check(R"(
type Table = { a: number }
type Self<T> = T
local a: Self<Table>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a")), "Table");
}
TEST_CASE_FIXTURE(Fixture, "no_persistent_typelevel_change")
{
TypeId mathTy = requireType(typeChecker.globalScope, "math");
REQUIRE(mathTy);
TableTypeVar* ttv = getMutable<TableTypeVar>(mathTy);
REQUIRE(ttv);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(ttv->props["frexp"].type);
REQUIRE(ftv);
auto original = ftv->level;
CheckResult result = check("local a = math.frexp");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(ftv->level.level == original.level);
CHECK(ftv->level.subLevel == original.subLevel);
}
TEST_CASE_FIXTURE(Fixture, "table_indexing_error_location")
{
CheckResult result = check(R"(
local foo = {42}
local bar: number?
local baz = foo[bar]
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0].location, Location{Position{3, 16}, Position{3, 19}});
}
TEST_CASE_FIXTURE(Fixture, "table_simple_call")
{
CheckResult result = check(R"(
local a = setmetatable({ x = 2 }, {
__call = function(self)
return (self.x :: number) * 2 -- should work without annotation in the future
end
})
local b = a()
local c = a(2) -- too many arguments
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Argument count mismatch. Function expects 1 argument, but 2 are specified", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "custom_require_global")
{
CheckResult result = check(R"(
--!nonstrict
require = function(a) end
local crash = require(game.A)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "operator_eq_operands_are_not_subtypes_of_each_other_but_has_overlap")
{
ScopedFastFlag sff1{"LuauEqConstraint", true};
CheckResult result = check(R"(
local function f(a: string | number, b: boolean | number)
return a == b
end
)");
// This doesn't produce any errors but for the wrong reasons.
// This unit test serves as a reminder to not try and unify the operands on `==`/`~=`.
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "access_index_metamethod_that_returns_variadic")
{
CheckResult result = check(R"(
type Foo = {x: string}
local t = {}
setmetatable(t, {
__index = function(x: string): ...Foo
return {x = x}
end
})
local foo = t.bar
)");
LUAU_REQUIRE_NO_ERRORS(result);
ToStringOptions o;
o.exhaustive = true;
CHECK_EQ("{| x: string |}", toString(requireType("foo"), o));
}
TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks")
{
CheckResult result = check(R"(
type ( ... ) ( ) ;
( ... ) ( - - ... ) ( - ... )
type = ( ... ) ;
( ... ) ( ) ( ... ) ;
( ... ) ""
)");
CHECK_LE(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks2")
{
CheckResult result = check(R"(
function _(l0:((typeof((pcall)))|((((t0)->())|(typeof(-67108864)))|(any)))|(any),...):(((typeof(0))|(any))|(any),typeof(-67108864),any)
xpcall(_,_,_)
_(_,_,_)
end
)");
CHECK_LE(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying")
{
CheckResult result = check(R"(
function _(l0:t0): (any, ()->())
end
type t0 = t0 | {}
)");
CHECK_LE(0, result.errors.size());
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*unknown*", toString(t0->type));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);
});
CHECK(it != result.errors.end());
}
TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional")
{
CheckResult result = check(R"(
function _(l0:t0): (any, ()->())
return 0,_
end
type t0 = t0 | {}
_(nil)
)");
CHECK_LE(0, result.errors.size());
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*unknown*", toString(t0->type));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);
});
CHECK(it != result.errors.end());
}
TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional2")
{
CheckResult result = check(R"(
function _(l0:({})|(t0)):((((typeof((xpcall)))|(t96<t0>))|(t13))&(t96<t0>),()->typeof(...))
return 0,_
end
type t0<t107> = ((typeof((_G)))|(({})|(t0)))|(t0)
_(nil)
local t: ({})|(t0)
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "no_infinite_loop_when_trying_to_unify_uh_this")
{
CheckResult result = check(R"(
function _(l22,l0):((((boolean)|(t0))|(t0))&(()->(()->(()->()->{},(t0<t22>)|(t0)),any)))
return function():t0<t0>
end
end
type t0<t0> = ((typeof(_))|(any))|(typeof(_))
_()
)");
CHECK_LE(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection")
{
CheckResult result = check(R"(
local l0,l0
repeat
type t0 = ((any)|((any)&((any)|((any)&((any)|(any))))))&(t0)
function _(l0):(t0)&(t0)
while nil do
end
end
until _(_)(_)._
)");
CHECK_LE(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "no_heap_use_after_free_error")
{
CheckResult result = check(R"(
--!nonstrict
_ += _:n0(xpcall,_)
local l0
do end
while _ do
function _:_()
_ += _(_._(_:n0(xpcall,_)))
end
end
)");
CHECK_LE(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back")
{
fileResolver.source["Module/Backend/Types"] = R"(
export type Fiber = {
return_: Fiber?
}
return {}
)";
fileResolver.source["Module/Backend"] = R"(
local Types = require(script.Types)
type Fiber = Types.Fiber
type ReactRenderer = { findFiberByHostInstance: () -> Fiber? }
local function attach(renderer): ()
local function getPrimaryFiber(fiber)
local alternate = fiber.alternate
return fiber
end
local function getFiberIDForNative()
local fiber = renderer.findFiberByHostInstance()
fiber = fiber.return_
return getPrimaryFiber(fiber)
end
end
function culprit(renderer: ReactRenderer): ()
attach(renderer)
end
return culprit
)";
CheckResult result = frontend.check("Module/Backend");
}
TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_ok")
{
CheckResult result = check(R"(
type Tree<T> = { data: T, children: {Tree<T>} }
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_not_ok")
{
ScopedFastFlag sff{"LuauRecursiveTypeParameterRestriction", true};
CheckResult result = check(R"(
-- this would be an infinite type if we allowed it
type Tree<T> = { data: T, children: {Tree<{T}>} }
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "record_matching_overload")
{
CheckResult result = check(R"(
type Overload = ((string) -> string) & ((number) -> number)
local abc: Overload
abc(1)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// AstExprCall is the node that has the overload stored on it.
// findTypeAtPosition will look at the AstExprLocal, but this is not what
// we want to look at.
std::vector<AstNode*> ancestry = findAstAncestryOfPosition(*getMainSourceModule(), Position(3, 10));
REQUIRE_GE(ancestry.size(), 2);
AstExpr* parentExpr = ancestry[ancestry.size() - 2]->asExpr();
REQUIRE(bool(parentExpr));
REQUIRE(parentExpr->is<AstExprCall>());
ModulePtr module = getMainModule();
auto it = module->astOverloadResolvedTypes.find(parentExpr);
REQUIRE(it);
CHECK_EQ(toString(*it), "(number) -> number");
}
TEST_CASE_FIXTURE(Fixture, "return_type_by_overload")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
type Overload = ((string) -> string) & ((number, number) -> number)
local abc: Overload
local x = abc(true)
local y = abc(true,true)
local z = abc(true,true,true)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("string", toString(requireType("x")));
CHECK_EQ("number", toString(requireType("y")));
// Should this be string|number?
CHECK_EQ("string", toString(requireType("z")));
}
TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments")
{
// Simple direct arg to arg propagation
CheckResult result = check(R"(
type Table = { x: number, y: number }
local function f(a: (Table) -> number) return a({x = 1, y = 2}) end
f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// An optional function is accepted, but since we already provide a function, nil can be ignored
result = check(R"(
type Table = { x: number, y: number }
local function f(a: ((Table) -> number)?) if a then return a({x = 1, y = 2}) else return 0 end end
f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Make sure self calls match correct index
result = check(R"(
type Table = { x: number, y: number }
local x = {}
x.b = {x = 1, y = 2}
function x:f(a: (Table) -> number) return a(self.b) end
x:f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Mix inferred and explicit argument types
result = check(R"(
function f(a: (a: number, b: number, c: boolean) -> number) return a(1, 2, true) end
f(function(a: number, b, c) return c and a + b or b - a end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Anonymous function has a variadic pack
result = check(R"(
type Table = { x: number, y: number }
local function f(a: (Table) -> number) return a({x = 1, y = 2}) end
f(function(...) return select(1, ...).z end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0]));
// Can't accept more arguments than provided
result = check(R"(
function f(a: (a: number, b: number) -> number) return a(1, 2) end
f(function(a, b, c, ...) return a + b end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
// Infer from variadic packs into elements
result = check(R"(
function f(a: (...number) -> number) return a(1, 2) end
f(function(a, b) return a + b end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Infer from variadic packs into variadic packs
result = check(R"(
type Table = { x: number, y: number }
function f(a: (...Table) -> number) return a({x = 1, y = 2}, {x = 3, y = 4}) end
f(function(a, ...) local b = ... return b.z end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0]));
// Return type inference
result = check(R"(
type Table = { x: number, y: number }
function f(a: (number) -> Table) return a(4) end
f(function(x) return x * 2 end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0]));
// Return type doesn't inference 'nil'
result = check(R"(
function f(a: (number) -> nil) return a(4) end
f(function(x) print(x) end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument")
{
ScopedFastFlag sff{"LuauUnsealedTableLiteral", true};
CheckResult result = check(R"(
local function sum<a>(x: a, y: a, f: (a, a) -> a) return f(x, y) end
return sum(2, 3, function(a, b) return a + b end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function map<a, b>(arr: {a}, f: (a) -> b) local r = {} for i,v in ipairs(arr) do table.insert(r, f(v)) end return r end
local a = {1, 2, 3}
local r = map(a, function(a) return a + a > 100 end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{boolean}", toString(requireType("r")));
check(R"(
local function foldl<a, b>(arr: {a}, init: b, f: (b, a) -> b) local r = init for i,v in ipairs(arr) do r = f(r, v) end return r end
local a = {1, 2, 3}
local r = foldl(a, {s=0,c=0}, function(a, b) return {s = a.s + b, c = a.c + 1} end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{ c: number, s: number }", toString(requireType("r")));
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded")
{
CheckResult result = check(R"(
local function g1<T>(a: T, f: (T) -> T) return f(a) end
local function g2<T>(a: T, b: T, f: (T, T) -> T) return f(a, b) end
local g12: typeof(g1) & typeof(g2)
g12(1, function(x) return x + x end)
g12(1, 2, function(x, y) return x + y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function g1<T>(a: T, f: (T) -> T) return f(a) end
local function g2<T>(a: T, b: T, f: (T, T) -> T) return f(a, b) end
local g12: typeof(g1) & typeof(g2)
g12({x=1}, function(x) return {x=-x.x} end)
g12({x=1}, {x=2}, function(x, y) return {x=x.x + y.x} end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_lib_function_function_argument")
{
CheckResult result = check(R"(
local a = {{x=4}, {x=7}, {x=1}}
table.sort(a, function(x, y) return x.x < y.x end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments_outside_call")
{
CheckResult result = check(R"(
type Table = { x: number, y: number }
local f: (Table) -> number = function(t) return t.x + t.y end
type TableWithFunc = { x: number, y: number, f: (number, number) -> number }
local a: TableWithFunc = { x = 3, y = 4, f = function(a, b) return a + b end }
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_infer_generic_functions")
{
CheckResult result = check(R"(
local function sum<a>(x: a, y: a, f: (a, a) -> a) return f(x, y) end
local function sumrec(f: typeof(sum))
return sum(2, 3, function(a, b) return a + b end)
end
local b = sumrec(sum) -- ok
local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'; different number of generic type "
"parameters",
toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "infer_return_value_type")
{
CheckResult result = check(R"(
local function f(): {string|number}
return {1, "b", 3}
end
local function g(): (number, {string|number})
return 4, {1, "b", 3}
end
local function h(): ...{string|number}
return {4}, {1, "b", 3}, {"s"}
end
local function i(): ...{string|number}
return {1, "b", 3}, h()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_type_assertion_value_type")
{
CheckResult result = check(R"(
local function f()
return {4, "b", 3} :: {string|number}
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_assignment_value_types")
{
CheckResult result = check(R"(
local a: (number, number) -> number = function(a, b) return a - b end
a = function(a, b) return a + b end
local b: {number|string}
local c: {number|string}
b, c = {2, "s"}, {"b", 4}
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_assignment_value_types_mutable_lval")
{
CheckResult result = check(R"(
local a = {}
a.x = 2
a = setmetatable(a, { __call = function(x) end })
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_through_group_expr")
{
CheckResult result = check(R"(
local function f(a: (number, number) -> number) return a(1, 3) end
f(((function(a, b) return a + b end)))
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "refine_and_or")
{
CheckResult result = check(R"(
local t: {x: number?}? = {x = nil}
local u = t and t.x or 5
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("u")));
}
TEST_CASE_FIXTURE(Fixture, "checked_prop_too_early")
{
CheckResult result = check(R"(
local t: {x: number?}? = {x = nil}
local u = t.x and t or 5
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0]));
CHECK_EQ("number | {| x: number? |}", toString(requireType("u")));
}
TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch")
{
CheckResult result = check(R"(
local t: {x: number?}? = {x = nil}
local u = t and t.x == 5 or t.x == 31337
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0]));
CHECK_EQ("boolean", toString(requireType("u")));
}
TEST_CASE_FIXTURE(Fixture, "substitution_with_bound_table")
{
CheckResult result = check(R"(
type A = { x: number }
local a: A = { x = 1 }
local b = a
type B = typeof(b)
type X<T> = T
local c: X<B>
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions1")
{
CheckResult result = check(R"(local a = if true then "true" else "false")");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId aType = requireType("a");
CHECK_EQ(getPrimitiveType(aType), PrimitiveTypeVar::String);
}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions2")
{
// Test expression containing elseif
CheckResult result = check(R"(
local a = if false then "a" elseif false then "b" else "c"
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId aType = requireType("a");
CHECK_EQ(getPrimitiveType(aType), PrimitiveTypeVar::String);
}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_type_union")
{
CheckResult result = check(R"(local a: number? = if true then 42 else nil)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a"), {true}), "number?");
}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_1")
{
CheckResult result = check(R"(
type X = {number | string}
local a: X = if true then {"1", 2, 3} else {4, 5, 6}
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a"), {true}), "{number | string}");
}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_2")
{
CheckResult result = check(R"(
local a: number? = if true then 1 else nil
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_3")
{
CheckResult result = check(R"(
local function times<T>(n: any, f: () -> T)
local result: {T} = {}
local res = f()
table.insert(result, if true then res else n)
return result
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "type_error_addition")
{
CheckResult result = check(R"(
--!strict
local foo = makesandwich()
local bar = foo.nutrition + 100
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// We should definitely get this error
CHECK_EQ("Unknown global 'makesandwich'", toString(result.errors[0]));
// We get this error if makesandwich() returns a free type
// CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'foo'", toString(result.errors[1]));
}
TEST_CASE_FIXTURE(Fixture, "require_failed_module")
{
fileResolver.source["game/A"] = R"(
return unfortunately()
)";
CheckResult aResult = frontend.check("game/A");
LUAU_REQUIRE_ERRORS(aResult);
CheckResult result = check(R"(
local ModuleA = require(game.A)
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> oty = requireType("ModuleA");
CHECK_EQ("*unknown*", toString(*oty));
}
/*
* If it wasn't instantly obvious, we have the fuzzer to thank for this gem of a test.
*
* We had an issue here where the scope for the `if` block here would
* have an elevated TypeLevel even though there is no function nesting going on.
* This would result in a free typevar for the type of _ that was much higher than
* it should be. This type would be erroneously quantified in the definition of `aaa`.
* This in turn caused an ice when evaluating `_()` in the while loop.
*/
TEST_CASE_FIXTURE(Fixture, "free_typevars_introduced_within_control_flow_constructs_do_not_get_an_elevated_TypeLevel")
{
check(R"(
--!strict
if _ then
_[_], _ = nil
_()
end
local aaa = function():typeof(_) return 1 end
if aaa then
while _() do
end
end
)");
// No ice()? No problem.
}
/*
* This is a bit elaborate. Bear with me.
*
* The type of _ becomes free with the first statement. With the second, we unify it with a function.
*
* At this point, it is important that the newly created fresh types of this new function type are promoted
* to the same level as the original free type. If we do not, they are incorrectly ascribed the level of the
* containing function.
*
* If this is allowed to happen, the final lambda erroneously quantifies the type of _ to something ridiculous
* just before we typecheck the invocation to _.
*/
TEST_CASE_FIXTURE(Fixture, "fuzzer_found_this")
{
check(R"(
l0, _ = nil
local function p()
_()
end
a = _(
function():(typeof(p),typeof(_))
end
)[nil]
)");
}
/*
* We had an issue where part of the type of pairs() was an unsealed table.
* This test depends on FFlagDebugLuauFreezeArena to trigger it.
*/
TEST_CASE_FIXTURE(Fixture, "pairs_parameters_are_not_unsealed_tables")
{
check(R"(
function _(l0:{n0:any})
_ = pairs
end
)");
}
TEST_CASE_FIXTURE(Fixture, "inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table")
{
check(R"(
function Base64FileReader(data)
local reader = {}
local index: number
function reader:PeekByte()
return data:byte(index)
end
function reader:Byte()
return data:byte(index - 1)
end
return reader
end
Base64FileReader()
function ReadMidiEvents(data)
local reader = Base64FileReader(data)
while reader:HasMore() do
(reader:Byte() % 128)
end
end
)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count")
{
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number) -> string
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number) -> string'
caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg")
{
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number, string) -> string
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, string) -> string'
caused by:
Argument #2 type is not compatible. Type 'string' could not be converted into 'number')");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count")
{
CheckResult result = check(R"(
type A = (number, number) -> (number)
type B = (number, number) -> (number, boolean)
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> number' could not be converted into '(number, number) -> (number, boolean)'
caused by:
Function only returns 1 value. 2 are required here)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret")
{
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number, number) -> number
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, number) -> number'
caused by:
Return type is not compatible. Type 'string' could not be converted into 'number')");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult")
{
CheckResult result = check(R"(
type A = (number, number) -> (number, string)
type B = (number, number) -> (number, boolean)
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]),
R"(Type '(number, number) -> (number, string)' could not be converted into '(number, number) -> (number, boolean)'
caused by:
Return #2 type is not compatible. Type 'string' could not be converted into 'boolean')");
}
TEST_CASE_FIXTURE(Fixture, "prop_access_on_any_with_other_options")
{
CheckResult result = check(R"(
local function f(thing: any | string)
local foo = thing.SomeRandomKey
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "table_function_check_use_after_free")
{
CheckResult result = check(R"(
local t = {}
function t.x(value)
for k,v in pairs(t) do end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "table_oop")
{
CheckResult result = check(R"(
--!strict
local Class = {}
Class.__index = Class
type Class = typeof(setmetatable({} :: { x: number }, Class))
function Class.new(x: number): Class
return setmetatable({x = x}, Class)
end
function Class.getx(self: Class)
return self.x
end
function test()
local c = Class.new(42)
local n = c:getx()
local nn = c.x
print(string.format("%d %d", n, nn))
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "recursive_metatable_crash")
{
CheckResult result = check(R"(
local function getIt()
local y
y = setmetatable({}, y)
return y
end
local a = getIt()
local b = getIt()
local c = a or b
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "bound_typepack_promote")
{
// No assertions should trigger
check(R"(
local function p()
local this = {}
this.pf = foo()
function this:IsActive() end
function this:Start(o) end
return this
end
local function h(tp, o)
ep = tp
tp:Start(o)
tp.pf.Connect(function()
ep:IsActive()
end)
end
function on()
local t = p()
h(t)
end
)");
}
TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
{
ScopedFastFlag subtypingVariance{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
--!strict
--!nolint
type FieldSpecifier = {
fieldName: string,
}
type ReadFieldOptions = FieldSpecifier & { from: number? }
type Policies = {
getStoreFieldName: (self: Policies, fieldSpec: FieldSpecifier) -> string,
}
local Policies = {}
local function foo(p: Policies)
end
function Policies:getStoreFieldName(specifier: FieldSpecifier): string
return ""
end
function Policies:readField(options: ReadFieldOptions)
local _ = self:getStoreFieldName(options)
--[[
Type error:
TypeError { "MainModule", Location { { line = 25, col = 16 }, { line = 25, col = 20 } }, TypeMismatch { Policies, {- getStoreFieldName: (tp1) -> (a, b...) -} } }
]]
foo(self)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types")
{
fileResolver.source["game/A"] = R"(
export type Type = { unrelated: boolean }
return {}
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = {}
function x:Destroy(): () end
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_2")
{
ScopedFastFlag immutableTypes{"LuauImmutableTypes", true};
fileResolver.source["game/A"] = R"(
export type Type = { x: { a: number } }
return {}
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = { x = { a = 2 } }
type Rename = typeof(x.x)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_3")
{
ScopedFastFlag immutableTypes{"LuauImmutableTypes", true};
fileResolver.source["game/A"] = R"(
local y = setmetatable({}, {})
export type Type = { x: typeof(y) }
return { x = y }
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = types
type Rename = typeof(x.x)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "indexing_on_string_singletons")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" then
local x = a:byte()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 22})));
}
TEST_CASE_FIXTURE(Fixture, "indexing_on_union_of_string_singletons")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" or a == "bye" then
local x = a:byte()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 22})));
}
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" then
local x = #a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
}
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" or a == "bye" then
local x = #a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 23})));
}
/*
* When we add new properties to an unsealed table, we should do a level check and promote the property type to be at
* the level of the table.
*/
TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the_same_TypeLevel_of_that_table")
{
CheckResult result = check(R"(
--!strict
local T = {}
local function f(prop)
T[1] = {
prop = prop,
}
end
local function g()
local l = T[1].prop
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "global_singleton_types_are_sealed")
{
CheckResult result = check(R"(
local function f(x: string)
local p = x:split('a')
p = table.pack(table.unpack(p, 1, #p - 1))
return p
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "function_decl_quantify_right_type")
{
ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify", true};
fileResolver.source["game/isAMagicMock"] = R"(
--!nonstrict
return function(value)
return false
end
)";
CheckResult result = check(R"(
--!nonstrict
local MagicMock = {}
MagicMock.is = require(game.isAMagicMock)
function MagicMock.is(value)
return false
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "function_decl_non_self_sealed_overwrite")
{
ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify", true};
CheckResult result = check(R"(
function string.len(): number
return 1
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();