mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-12 21:10:37 +00:00
f5dabc2998
In this update we improve overall stability of the new type solver and address some type inference issues with it. If you use the new solver and want to use all new fixes included in this release, you have to reference an additional Luau flag: ```c++ LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) ``` And set its value to `644`: ```c++ DFInt::LuauTypeSolverRelease.value = 644; // Or a higher value for future updates ``` ## New Solver * Fixed a debug assertion failure in autocomplete (Fixes #1391) * Fixed type function distribution issue which transformed `len<>` and `unm<>` into `not<>` (Fixes #1416) * Placed a limit on the possible normalized table intersection size as a temporary measure to avoid hangs and out-of-memory issues for complex type refinements * Internal recursion limits are now respected in the subtyping operations and in autocomplete, to avoid stack overflow crashes * Fixed false positive errors on assignments to tables whose indexers are unions of strings * Fixed memory corruption crashes in subtyping of generic types containing other generic types in their bounds --- Internal Contributors: Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
1264 lines
37 KiB
C++
1264 lines
37 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Luau/TypeFunction.h"
|
|
|
|
#include "Luau/ConstraintSolver.h"
|
|
#include "Luau/NotNull.h"
|
|
#include "Luau/Type.h"
|
|
|
|
#include "ClassFixture.h"
|
|
#include "Fixture.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
using namespace Luau;
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTFLAG(LuauUserDefinedTypeFunctions)
|
|
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
|
|
|
|
struct TypeFunctionFixture : Fixture
|
|
{
|
|
TypeFunction swapFunction;
|
|
|
|
TypeFunctionFixture()
|
|
: Fixture(true, false)
|
|
{
|
|
swapFunction = TypeFunction{
|
|
/* name */ "Swap",
|
|
/* reducer */
|
|
[](TypeId instance, const std::vector<TypeId>& tys, const std::vector<TypePackId>& tps, NotNull<TypeFunctionContext> ctx
|
|
) -> TypeFunctionReductionResult<TypeId>
|
|
{
|
|
LUAU_ASSERT(tys.size() == 1);
|
|
TypeId param = follow(tys.at(0));
|
|
|
|
if (isString(param))
|
|
{
|
|
return TypeFunctionReductionResult<TypeId>{ctx->builtins->numberType, false, {}, {}};
|
|
}
|
|
else if (isNumber(param))
|
|
{
|
|
return TypeFunctionReductionResult<TypeId>{ctx->builtins->stringType, false, {}, {}};
|
|
}
|
|
else if (is<BlockedType>(param) || is<PendingExpansionType>(param) || is<TypeFunctionInstanceType>(param) ||
|
|
(ctx->solver && ctx->solver->hasUnresolvedConstraints(param)))
|
|
{
|
|
return TypeFunctionReductionResult<TypeId>{std::nullopt, false, {param}, {}};
|
|
}
|
|
else
|
|
{
|
|
return TypeFunctionReductionResult<TypeId>{std::nullopt, true, {}, {}};
|
|
}
|
|
}
|
|
};
|
|
|
|
unfreeze(frontend.globals.globalTypes);
|
|
TypeId t = frontend.globals.globalTypes.addType(GenericType{"T"});
|
|
GenericTypeDefinition genericT{t};
|
|
|
|
ScopePtr globalScope = frontend.globals.globalScope;
|
|
globalScope->exportedTypeBindings["Swap"] =
|
|
TypeFun{{genericT}, frontend.globals.globalTypes.addType(TypeFunctionInstanceType{NotNull{&swapFunction}, {t}, {}})};
|
|
freeze(frontend.globals.globalTypes);
|
|
}
|
|
};
|
|
|
|
TEST_SUITE_BEGIN("TypeFunctionTests");
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "basic_type_function")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type A = Swap<number>
|
|
type B = Swap<string>
|
|
type C = Swap<boolean>
|
|
|
|
local x = 123
|
|
local y: Swap<typeof(x)> = "foo"
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK("string" == toString(requireTypeAlias("A")));
|
|
CHECK("number" == toString(requireTypeAlias("B")));
|
|
CHECK("Swap<boolean>" == toString(requireTypeAlias("C")));
|
|
CHECK("string" == toString(requireType("y")));
|
|
CHECK("Type function instance Swap<boolean> is uninhabited" == toString(result.errors[0]));
|
|
};
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "function_as_fn_ret")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local swapper: <T>(T) -> Swap<T>
|
|
local a = swapper(123)
|
|
local b = swapper("foo")
|
|
local c = swapper(false)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK("string" == toString(requireType("a")));
|
|
CHECK("number" == toString(requireType("b")));
|
|
CHECK("Swap<boolean>" == toString(requireType("c")));
|
|
CHECK("Type function instance Swap<boolean> is uninhabited" == toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "function_as_fn_arg")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local swapper: <T>(Swap<T>) -> T
|
|
local a = swapper(123)
|
|
local b = swapper(false)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK("unknown" == toString(requireType("a")));
|
|
CHECK("unknown" == toString(requireType("b")));
|
|
CHECK("Type 'number' could not be converted into 'never'" == toString(result.errors[0]));
|
|
CHECK("Type 'boolean' could not be converted into 'never'" == toString(result.errors[1]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "resolve_deep_functions")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local x: Swap<Swap<Swap<string>>>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("number" == toString(requireType("x")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "unsolvable_function")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local impossible: <T>(Swap<T>) -> Swap<Swap<T>>
|
|
local a = impossible(123)
|
|
local b = impossible(true)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(6, result);
|
|
CHECK(toString(result.errors[0]) == "Type function instance Swap<Swap<T>> is uninhabited");
|
|
CHECK(toString(result.errors[1]) == "Type function instance Swap<T> is uninhabited");
|
|
CHECK(toString(result.errors[2]) == "Type function instance Swap<Swap<T>> is uninhabited");
|
|
CHECK(toString(result.errors[3]) == "Type function instance Swap<T> is uninhabited");
|
|
CHECK(toString(result.errors[4]) == "Type function instance Swap<Swap<T>> is uninhabited");
|
|
CHECK(toString(result.errors[5]) == "Type function instance Swap<T> is uninhabited");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "table_internal_functions")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local t: <T>({T}) -> {Swap<T>}
|
|
local a = t({1, 2, 3})
|
|
local b = t({"a", "b", "c"})
|
|
local c = t({true, false, true})
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(requireType("a")) == "{string}");
|
|
CHECK(toString(requireType("b")) == "{number}");
|
|
// FIXME: table types are constructing a trivial union here.
|
|
CHECK(toString(requireType("c")) == "{Swap<boolean | boolean | boolean>}");
|
|
CHECK(toString(result.errors[0]) == "Type function instance Swap<boolean | boolean | boolean> is uninhabited");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "function_internal_functions")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local f0: <T>(T) -> (() -> T)
|
|
local f: <T>(T) -> (() -> Swap<T>)
|
|
local a = f(1)
|
|
local b = f("a")
|
|
local c = f(true)
|
|
local d = f0(1)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(requireType("a")) == "() -> string");
|
|
CHECK(toString(requireType("b")) == "() -> number");
|
|
CHECK(toString(requireType("c")) == "() -> Swap<boolean>");
|
|
CHECK(toString(result.errors[0]) == "Type function instance Swap<boolean> is uninhabited");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "add_function_at_work")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local function add(a, b)
|
|
return a + b
|
|
end
|
|
|
|
local a = add(1, 2)
|
|
local b = add(1, "foo")
|
|
local c = add("foo", 1)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(requireType("a")) == "number");
|
|
CHECK(toString(requireType("b")) == "add<number, string>");
|
|
CHECK(toString(requireType("c")) == "add<string, number>");
|
|
CHECK(
|
|
toString(result.errors[0]) ==
|
|
"Operator '+' could not be applied to operands of types number and string; there is no corresponding overload for __add"
|
|
);
|
|
CHECK(
|
|
toString(result.errors[1]) ==
|
|
"Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add"
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_add_function_at_work")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type T = add<number | T, number>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireTypeAlias("T")) == "number");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "mul_function_with_union_of_multiplicatives")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
loadDefinition(R"(
|
|
declare class Vec2
|
|
function __mul(self, rhs: number): Vec2
|
|
end
|
|
|
|
declare class Vec3
|
|
function __mul(self, rhs: number): Vec3
|
|
end
|
|
)");
|
|
|
|
CheckResult result = check(R"(
|
|
type T = mul<Vec2 | Vec3, number>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireTypeAlias("T")) == "Vec2 | Vec3");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "mul_function_with_union_of_multiplicatives_2")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
loadDefinition(R"(
|
|
declare class Vec3
|
|
function __mul(self, rhs: number): Vec3
|
|
function __mul(self, rhs: Vec3): Vec3
|
|
end
|
|
)");
|
|
|
|
CheckResult result = check(R"(
|
|
type T = mul<number | Vec3, Vec3>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireTypeAlias("T")) == "Vec3");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "internal_functions_raise_errors")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local function innerSum(a, b)
|
|
local _ = a + b
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(
|
|
toString(result.errors[0]) ==
|
|
"Operator '+' could not be applied to operands of types unknown and unknown; there is no corresponding overload for __add"
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "type_functions_can_be_shadowed")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type add<T> = string -- shadow add
|
|
|
|
-- this should be ok
|
|
function hi(f: add<unknown>)
|
|
return string.format("hi %s", f)
|
|
end
|
|
|
|
-- this should still work totally fine (and use the real type function)
|
|
function plus(a, b)
|
|
return a + b
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK(toString(requireType("hi")) == "(string) -> string");
|
|
CHECK(toString(requireType("plus")) == "<a, b>(a, b) -> add<a, b>");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "type_functions_inhabited_with_normalization")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local useGridConfig : any
|
|
local columns = useGridConfig("columns", {}) or 1
|
|
local gutter = useGridConfig('gutter', {}) or 0
|
|
local margin = useGridConfig('margin', {}) or 0
|
|
return function(frameAbsoluteWidth: number)
|
|
local cellAbsoluteWidth = (frameAbsoluteWidth - 2 * margin + gutter) / columns - gutter
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_works")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type KeysOfMyObject = keyof<MyObject>
|
|
|
|
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
|
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_works_with_metatables")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local metatable = { __index = {w = 1} }
|
|
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
|
|
type MyObject = typeof(obj)
|
|
type KeysOfMyObject = keyof<MyObject>
|
|
|
|
local function ok(idx: KeysOfMyObject): "w" | "x" | "y" | "z" return idx end
|
|
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"w\" | \"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_single_entry_no_uniontype")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local tbl_A = { abc = "value" }
|
|
local tbl_B = { a1 = nil, ["a2"] = nil }
|
|
|
|
type keyof_A = keyof<typeof(tbl_A)>
|
|
type keyof_B = keyof<typeof(tbl_B)>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK(toString(requireTypeAlias("keyof_A")) == "\"abc\"");
|
|
CHECK(toString(requireTypeAlias("keyof_B")) == "\"a1\" | \"a2\"");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_errors_if_it_has_nontable_part")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type KeysOfMyObject = keyof<MyObject | boolean>
|
|
|
|
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
|
)");
|
|
|
|
// FIXME(CLI-95289): we should actually only report the type function being uninhabited error at its first use, I think?
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Type 'MyObject | boolean' does not have keys, so 'keyof<MyObject | boolean>' is invalid");
|
|
CHECK(toString(result.errors[1]) == "Type 'MyObject | boolean' does not have keys, so 'keyof<MyObject | boolean>' is invalid");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_string_indexer")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type MyOtherObject = { [string]: number }
|
|
type KeysOfMyOtherObject = keyof<MyOtherObject>
|
|
type KeysOfMyObjects = keyof<MyObject | MyOtherObject>
|
|
|
|
local function ok(idx: KeysOfMyOtherObject): "z" return idx end
|
|
local function err(idx: KeysOfMyObjects): "z" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("string", toString(tpm->givenTp));
|
|
|
|
tpm = get<TypePackMismatch>(result.errors[1]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_common_subset_if_union_of_differing_tables")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type MyOtherObject = { w: number, y: number, z: number }
|
|
type KeysOfMyObject = keyof<MyObject | MyOtherObject>
|
|
|
|
local function err(idx: KeysOfMyObject): "z" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_never_for_empty_table")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeyofEmpty = keyof<{}>
|
|
|
|
local foo = ((nil :: any) :: KeyofEmpty)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireType("foo")) == "never");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_works")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type KeysOfMyObject = rawkeyof<MyObject>
|
|
|
|
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
|
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_ignores_metatables")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local metatable = { __index = {w = 1} }
|
|
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
|
|
type MyObject = typeof(obj)
|
|
type KeysOfMyObject = rawkeyof<MyObject>
|
|
|
|
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
|
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_errors_if_it_has_nontable_part")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type KeysOfMyObject = rawkeyof<MyObject | boolean>
|
|
|
|
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
|
)");
|
|
|
|
// FIXME(CLI-95289): we should actually only report the type function being uninhabited error at its first use, I think?
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Type 'MyObject | boolean' does not have keys, so 'rawkeyof<MyObject | boolean>' is invalid");
|
|
CHECK(toString(result.errors[1]) == "Type 'MyObject | boolean' does not have keys, so 'rawkeyof<MyObject | boolean>' is invalid");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_common_subset_if_union_of_differing_tables")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = { x: number, y: number, z: number }
|
|
type MyOtherObject = { w: number, y: number, z: number }
|
|
type KeysOfMyObject = rawkeyof<MyObject | MyOtherObject>
|
|
|
|
local function err(idx: KeysOfMyObject): "z" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"y\" | \"z\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_never_for_empty_table")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type RawkeyofEmpty = rawkeyof<{}>
|
|
|
|
local foo = ((nil :: any) :: RawkeyofEmpty)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireType("foo")) == "never");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_works_on_classes")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeysOfMyObject = keyof<BaseClass>
|
|
|
|
local function ok(idx: KeysOfMyObject): "BaseMethod" | "BaseField" | "Touched" return idx end
|
|
local function err(idx: KeysOfMyObject): "BaseMethod" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("\"BaseMethod\"", toString(tpm->wantedTp));
|
|
CHECK_EQ("\"BaseField\" | \"BaseMethod\" | \"Touched\"", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_errors_if_it_has_nonclass_part")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeysOfMyObject = keyof<BaseClass | boolean>
|
|
|
|
local function err(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end
|
|
)");
|
|
|
|
// FIXME(CLI-95289): we should actually only report the type function being uninhabited error at its first use, I think?
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Type 'BaseClass | boolean' does not have keys, so 'keyof<BaseClass | boolean>' is invalid");
|
|
CHECK(toString(result.errors[1]) == "Type 'BaseClass | boolean' does not have keys, so 'keyof<BaseClass | boolean>' is invalid");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_common_subset_if_union_of_differing_classes")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeysOfMyObject = keyof<BaseClass | Vector2>
|
|
|
|
local function ok(idx: KeysOfMyObject): never return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_works_with_parent_classes_too")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeysOfMyObject = keyof<ChildClass>
|
|
|
|
local function ok(idx: KeysOfMyObject): "BaseField" | "BaseMethod" | "Method" | "Touched" return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "binary_type_function_works_with_default_argument")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type result = mul<number>
|
|
|
|
local function thunk(): result return 5 * 4 end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("() -> number" == toString(requireType("thunk")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "vector2_multiply_is_overloaded")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local v = Vector2.New(1, 2)
|
|
|
|
local v2 = v * 1.5
|
|
local v3 = v * v
|
|
local v4 = v * "Hello" -- line 5
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK(5 == result.errors[0].location.begin.line);
|
|
CHECK(5 == result.errors[0].location.end.line);
|
|
|
|
CHECK("Vector2" == toString(requireType("v2")));
|
|
CHECK("Vector2" == toString(requireType("v3")));
|
|
CHECK("mul<Vector2, string>" == toString(requireType("v4")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_rfc_example")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local animals = {
|
|
cat = { speak = function() print "meow" end },
|
|
dog = { speak = function() print "woof woof" end },
|
|
monkey = { speak = function() print "oo oo" end },
|
|
fox = { speak = function() print "gekk gekk" end }
|
|
}
|
|
|
|
type AnimalType = keyof<typeof(animals)>
|
|
|
|
function speakByType(animal: AnimalType)
|
|
animals[animal].speak()
|
|
end
|
|
|
|
speakByType("dog") -- ok
|
|
speakByType("cactus") -- errors
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
|
REQUIRE(tm);
|
|
CHECK_EQ("\"cat\" | \"dog\" | \"fox\" | \"monkey\"", toString(tm->wantedType));
|
|
CHECK_EQ("\"cactus\"", toString(tm->givenType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_oss_crash_gh1161")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local EnumVariants = {
|
|
["a"] = 1, ["b"] = 2, ["c"] = 3
|
|
}
|
|
|
|
type EnumKey = keyof<typeof(EnumVariants)>
|
|
|
|
function fnA<T>(i: T): keyof<T> end
|
|
|
|
function fnB(i: EnumKey) end
|
|
|
|
local result = fnA(EnumVariants)
|
|
fnB(result)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(get<ConstraintSolvingIncompleteError>(result.errors[0]));
|
|
CHECK(get<FunctionExitsWithoutReturning>(result.errors[1]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeFunctionFixture, "fuzzer_numeric_binop_doesnt_assert_on_generalizeFreeType")
|
|
{
|
|
CheckResult result = check(R"(
|
|
Module 'l0':
|
|
local _ = (67108864)(_ >= _).insert
|
|
do end
|
|
do end
|
|
_(...,_(_,_(_()),_()))
|
|
(67108864)()()
|
|
_(_ ~= _ // _,l0)(_(_({n0,})),_(_),_)
|
|
_(setmetatable(_,{[...]=_,}))
|
|
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_concat_function_at_work")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type T = concat<string | T, string>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireTypeAlias("T")) == "string");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "exceeded_distributivity_limits")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 10};
|
|
|
|
loadDefinition(R"(
|
|
declare class A
|
|
function __mul(self, rhs: unknown): A
|
|
end
|
|
|
|
declare class B
|
|
function __mul(self, rhs: unknown): B
|
|
end
|
|
|
|
declare class C
|
|
function __mul(self, rhs: unknown): C
|
|
end
|
|
|
|
declare class D
|
|
function __mul(self, rhs: unknown): D
|
|
end
|
|
)");
|
|
|
|
CheckResult result = check(R"(
|
|
type T = mul<A | B | C | D, A | B | C | D>
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(get<UninhabitedTypeFunction>(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "didnt_quite_exceed_distributivity_limits")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
// We duplicate the test here because we want to make sure the test failed
|
|
// due to exceeding the limits specifically, rather than any possible reasons.
|
|
ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 20};
|
|
|
|
loadDefinition(R"(
|
|
declare class A
|
|
function __mul(self, rhs: unknown): A
|
|
end
|
|
|
|
declare class B
|
|
function __mul(self, rhs: unknown): B
|
|
end
|
|
|
|
declare class C
|
|
function __mul(self, rhs: unknown): C
|
|
end
|
|
|
|
declare class D
|
|
function __mul(self, rhs: unknown): D
|
|
end
|
|
)");
|
|
|
|
CheckResult result = check(R"(
|
|
type T = mul<A | B | C | D, A | B | C | D>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_equivalence_with_distributivity")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
loadDefinition(R"(
|
|
declare class A
|
|
function __mul(self, rhs: unknown): A
|
|
end
|
|
|
|
declare class B
|
|
function __mul(self, rhs: unknown): B
|
|
end
|
|
|
|
declare class C
|
|
function __mul(self, rhs: unknown): C
|
|
end
|
|
|
|
declare class D
|
|
function __mul(self, rhs: unknown): D
|
|
end
|
|
)");
|
|
|
|
CheckResult result = check(R"(
|
|
type T = mul<A | B, C | D>
|
|
type U = mul<A, C> | mul<A, D> | mul<B, C> | mul<B, D>
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK(toString(requireTypeAlias("T")) == "A | B");
|
|
CHECK(toString(requireTypeAlias("U")) == "A | A | B | B");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "we_shouldnt_warn_that_a_reducible_type_function_is_uninhabited")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local Debounce = false
|
|
local Active = false
|
|
|
|
local function Use(Mode)
|
|
|
|
if Mode ~= nil then
|
|
|
|
if Mode == false and Active == false then
|
|
return
|
|
else
|
|
Active = not Mode
|
|
end
|
|
|
|
Debounce = false
|
|
end
|
|
Active = not Active
|
|
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
type IdxAType = index<MyObject, "a">
|
|
type IdxBType = index<MyObject, keyof<MyObject>>
|
|
|
|
local function ok(idx: IdxAType): string return idx end
|
|
local function ok2(idx: IdxBType): string | number | boolean return idx end
|
|
local function err(idx: IdxAType): boolean return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("boolean", toString(tpm->wantedTp));
|
|
CHECK_EQ("string", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_wait_for_pending_no_crash")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local PlayerData = {
|
|
Coins = 0,
|
|
Level = 1,
|
|
Exp = 0,
|
|
MaxExp = 100
|
|
}
|
|
type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>>
|
|
-- This function makes it think that there's going to be a pending expansion
|
|
local function UpdateData(key: Keys, value)
|
|
PlayerData[key] = value
|
|
end
|
|
UpdateData("Coins", 2)
|
|
)");
|
|
|
|
// Should not crash!
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_array")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local MyObject = {"hello", 1, true}
|
|
type IdxAType = index<typeof(MyObject), number>
|
|
|
|
local function ok(idx: IdxAType): string | number | boolean return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_generic_types")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local function access<T, K>(tbl: T & {}, key: K): index<T, K>
|
|
return tbl[key]
|
|
end
|
|
|
|
local subjects = {
|
|
english = "boring",
|
|
math = "fun"
|
|
}
|
|
|
|
local key: "english" = "english"
|
|
local a: string = access(subjects, key)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_bad_indexer")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
type errType1 = index<MyObject, "d">
|
|
type errType2 = index<MyObject, boolean>
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"d\"' does not exist on type 'MyObject'");
|
|
CHECK(toString(result.errors[1]) == "Property 'boolean' does not exist on type 'MyObject'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_var_indexer")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
local key = "a"
|
|
|
|
type errType1 = index<MyObject, key>
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Second argument to index<MyObject, _> is not a valid index type");
|
|
CHECK(toString(result.errors[1]) == "Unknown type 'key'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_union_type_indexer")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
|
|
type idxType = index<MyObject, "a" | "b">
|
|
local function ok(idx: idxType): string | number return idx end
|
|
|
|
type errType = index<MyObject, "a" | "d">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"a\" | \"d\"' does not exist on type 'MyObject'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_union_type_indexee")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
type MyObject2 = {a: number}
|
|
|
|
type idxTypeA = index<MyObject | MyObject2, "a">
|
|
local function ok(idx: idxTypeA): string | number return idx end
|
|
|
|
type errType = index<MyObject | MyObject2, "b">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"b\"' does not exist on type 'MyObject | MyObject2'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_rfc_alternative_section")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string}
|
|
type MyObject2 = {a: string, b: number}
|
|
|
|
local function edgeCase(param: MyObject)
|
|
type unknownType = index<typeof(param), "b">
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"b\"' does not exist on type 'MyObject'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "index_type_function_works_on_classes")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeysOfMyObject = index<BaseClass, "BaseField">
|
|
|
|
local function ok(idx: KeysOfMyObject): number return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "index_type_function_works_on_classes_with_parents")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type KeysOfMyObject = index<ChildClass, "BaseField">
|
|
|
|
local function ok(idx: KeysOfMyObject): number return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_index_metatables")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local exampleClass = { Foo = "text", Bar = true }
|
|
|
|
local exampleClass2 = setmetatable({ Foo = 8 }, { __index = exampleClass })
|
|
type exampleTy2 = index<typeof(exampleClass2), "Foo">
|
|
local function ok(idx: exampleTy2): number return idx end
|
|
|
|
local exampleClass3 = setmetatable({ Bar = 5 }, { __index = exampleClass })
|
|
type exampleTy3 = index<typeof(exampleClass3), "Foo">
|
|
local function ok2(idx: exampleTy3): string return idx end
|
|
|
|
type exampleTy4 = index<typeof(exampleClass3), "Foo" | "Bar">
|
|
local function ok3(idx: exampleTy4): string | number return idx end
|
|
|
|
type errTy = index<typeof(exampleClass2), "Car">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"Car\"' does not exist on type 'exampleClass2'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
type RawAType = rawget<MyObject, "a">
|
|
type RawBType = rawget<MyObject, keyof<MyObject>>
|
|
local function ok(idx: RawAType): string return idx end
|
|
local function ok2(idx: RawBType): string | number | boolean return idx end
|
|
local function err(idx: RawAType): boolean return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tpm);
|
|
CHECK_EQ("boolean", toString(tpm->wantedTp));
|
|
CHECK_EQ("string", toString(tpm->givenTp));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_array")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local MyObject = {"hello", 1, true}
|
|
type RawAType = rawget<typeof(MyObject), number>
|
|
local function ok(idx: RawAType): string | number | boolean return idx end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_errors_w_var_indexer")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
local key = "a"
|
|
type errType1 = rawget<MyObject, key>
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Second argument to rawget<MyObject, _> is not a valid index type");
|
|
CHECK(toString(result.errors[1]) == "Unknown type 'key'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_union_type_indexer")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
type rawType = rawget<MyObject, "a" | "b">
|
|
local function ok(idx: rawType): string | number return idx end
|
|
type errType = rawget<MyObject, "a" | "d">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"a\" | \"d\"' does not exist on type 'MyObject'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_union_type_indexee")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyObject = {a: string, b: number, c: boolean}
|
|
type MyObject2 = {a: number}
|
|
type rawTypeA = rawget<MyObject | MyObject2, "a">
|
|
local function ok(idx: rawTypeA): string | number return idx end
|
|
type errType = rawget<MyObject | MyObject2, "b">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"b\"' does not exist on type 'MyObject | MyObject2'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_index_metatables")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local exampleClass = { Foo = "text", Bar = true }
|
|
local exampleClass2 = setmetatable({ Foo = 8 }, { __index = exampleClass })
|
|
type exampleTy2 = rawget<typeof(exampleClass2), "Foo">
|
|
local function ok(idx: exampleTy2): number return idx end
|
|
local exampleClass3 = setmetatable({ Bar = 5 }, { __index = exampleClass })
|
|
type errType = rawget<typeof(exampleClass3), "Foo">
|
|
type errType2 = rawget<typeof(exampleClass3), "Bar" | "Foo">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"Foo\"' does not exist on type 'exampleClass3'");
|
|
CHECK(toString(result.errors[1]) == "Property '\"Bar\" | \"Foo\"' does not exist on type 'exampleClass3'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "rawget_type_function_errors_w_classes")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type PropsOfMyObject = rawget<BaseClass, "BaseField">
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "Property '\"BaseField\"' does not exist on type 'BaseClass'");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "user_defined_type_function_errors")
|
|
{
|
|
if (!FFlag::LuauUserDefinedTypeFunctions)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type function foo()
|
|
return nil
|
|
end
|
|
)");
|
|
LUAU_CHECK_ERROR_COUNT(1, result);
|
|
CHECK(toString(result.errors[0]) == "This syntax is not supported");
|
|
}
|
|
|
|
TEST_SUITE_END();
|