mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
02241b6d24
Some checks failed
benchmark / callgrind (map[branch:main name:luau-lang/benchmark-data], ubuntu-22.04) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:macos version:macos-latest]) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:macos-arm version:macos-14]) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:ubuntu version:ubuntu-latest]) (push) Has been cancelled
build / windows (Win32) (push) Has been cancelled
build / windows (x64) (push) Has been cancelled
build / coverage (push) Has been cancelled
build / web (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:macos version:macos-latest]) (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:ubuntu version:ubuntu-20.04]) (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:windows version:windows-latest]) (push) Has been cancelled
release / web (push) Has been cancelled
In this update, we continue to improve the overall stability of the new type solver. We're also shipping some early bits of two new features, one of the language and one of the analysis API: user-defined type functions and an incremental typechecking API. 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 `645`: ```c++ DFInt::LuauTypeSolverRelease.value = 645; // Or a higher value for future updates ``` ## New Solver * Fix a crash where scopes are incorrectly accessed cross-module after they've been deallocated by appropriately zeroing out associated scope pointers for free types, generic types, table types, etc. * Fix a crash where we were incorrectly caching results for bound types in generalization. * Eliminated some unnecessary intermediate allocations in the constraint solver and type function infrastructure. * Built some initial groundwork for an incremental typecheck API for use by language servers. * Built an initial technical preview for [user-defined type functions](https://rfcs.luau-lang.org/user-defined-type-functions.html), more work still to come (including calling type functions from other type functions), but adventurous folks wanting to experiment with it can try it out by enabling `FFlag::LuauUserDefinedTypeFunctionsSyntax` and `FFlag::LuauUserDefinedTypeFunction` in their local environment. Special thanks to @joonyoo181 who built up all the initial infrastructure for this during his internship! ## Miscellaneous changes * Fix a compilation error on Ubuntu (fixes #1437) --- Internal Contributors: Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Jeremy Yoo <jyoo@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vighnesh <vvijay@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> Co-authored-by: Junseo Yoo <jyoo@roblox.com>
1251 lines
37 KiB
C++
1251 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_SUITE_END();
|