mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-13 13:30:40 +00:00
543de6e939
# General Updates * Fix some cases where documentation symbols would not be available when mouseovering at certain positions in the code * Scaffolding to help embedders have more control over how `typeof(x)` refines types * Refinements to require-by-string semantics. See https://github.com/luau-lang/rfcs/pull/56 for details. * Fix for https://github.com/luau-lang/luau/issues/1405 # New Solver * Fix many crashes (thanks you for your bug reports!) * Type functions can now call each other * Type functions all evaluate in a single VM. This should improve typechecking performance and reduce memory use. * `export type function` is now forbidden and fails with a clear error message * Type functions that access locals in the surrounding environment are now properly a parse error * You can now use `:setindexer(types.never, types.never)` to delete an indexer from a table type. # Internal Contributors Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
620 lines
17 KiB
C++
620 lines
17 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Luau/TypePath.h"
|
|
|
|
#include "Luau/Type.h"
|
|
#include "Luau/TypeArena.h"
|
|
#include "Luau/TypePack.h"
|
|
|
|
#include "ClassFixture.h"
|
|
#include "doctest.h"
|
|
#include "Fixture.h"
|
|
#include "ScopedFlags.h"
|
|
|
|
#include <optional>
|
|
|
|
using namespace Luau;
|
|
using namespace Luau::TypePath;
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2);
|
|
LUAU_DYNAMIC_FASTINT(LuauTypePathMaximumTraverseSteps);
|
|
|
|
struct TypePathFixture : Fixture
|
|
{
|
|
ScopedFastFlag sff1{FFlag::LuauSolverV2, true};
|
|
};
|
|
|
|
struct TypePathBuiltinsFixture : BuiltinsFixture
|
|
{
|
|
ScopedFastFlag sff1{FFlag::LuauSolverV2, true};
|
|
};
|
|
|
|
TEST_SUITE_BEGIN("TypePathManipulation");
|
|
|
|
TEST_CASE("append")
|
|
{
|
|
SUBCASE("empty_paths")
|
|
{
|
|
Path p;
|
|
CHECK(p.append(Path{}).empty());
|
|
}
|
|
|
|
SUBCASE("empty_path_with_path")
|
|
{
|
|
Path p1;
|
|
Path p2(TypeField::Metatable);
|
|
|
|
Path result = p1.append(p2);
|
|
CHECK(result == Path(TypeField::Metatable));
|
|
}
|
|
|
|
SUBCASE("two_paths")
|
|
{
|
|
Path p1(TypeField::IndexLookup);
|
|
Path p2(TypeField::Metatable);
|
|
|
|
Path result = p1.append(p2);
|
|
CHECK(result == Path({TypeField::IndexLookup, TypeField::Metatable}));
|
|
}
|
|
|
|
SUBCASE("all_components")
|
|
{
|
|
Path p1({TypeField::IndexLookup, TypeField::Metatable});
|
|
Path p2({TypeField::Metatable, PackField::Arguments});
|
|
|
|
Path result = p1.append(p2);
|
|
CHECK(result == Path({TypeField::IndexLookup, TypeField::Metatable, TypeField::Metatable, PackField::Arguments}));
|
|
}
|
|
|
|
SUBCASE("does_not_mutate")
|
|
{
|
|
Path p1(TypeField::IndexLookup);
|
|
Path p2(TypeField::Metatable);
|
|
|
|
p1.append(p2);
|
|
CHECK(p1 == Path(TypeField::IndexLookup));
|
|
CHECK(p2 == Path(TypeField::Metatable));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("push")
|
|
{
|
|
Path p;
|
|
Path result = p.push(TypeField::Metatable);
|
|
|
|
CHECK(p.empty());
|
|
CHECK(result == Path(TypeField::Metatable));
|
|
}
|
|
|
|
TEST_CASE("pop")
|
|
{
|
|
SUBCASE("empty_path")
|
|
{
|
|
Path p;
|
|
CHECK(p.empty());
|
|
CHECK(p.pop().empty());
|
|
}
|
|
}
|
|
|
|
TEST_SUITE_END(); // TypePathManipulation
|
|
|
|
TEST_SUITE_BEGIN("TypePathTraversal");
|
|
|
|
#define TYPESOLVE_CODE(code) \
|
|
do \
|
|
{ \
|
|
CheckResult result = check(code); \
|
|
LUAU_REQUIRE_NO_ERRORS(result); \
|
|
} while (false);
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "empty_traversal")
|
|
{
|
|
CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "table_property")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
local x = { y = 123 }
|
|
)");
|
|
|
|
CHECK(traverseForType(requireType("x"), Path(TypePath::Property{"y", true}), builtinTypes) == builtinTypes->numberType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "class_property")
|
|
{
|
|
CHECK(traverseForType(vector2InstanceType, Path(TypePath::Property{"X", true}), builtinTypes) == builtinTypes->numberType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathBuiltinsFixture, "metatable_property")
|
|
{
|
|
SUBCASE("meta_does_not_contribute")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
local x = setmetatable({ x = 123 }, {})
|
|
)");
|
|
}
|
|
|
|
SUBCASE("meta_and_table_supply_property")
|
|
{
|
|
// since the table takes priority, the __index property won't matter
|
|
TYPESOLVE_CODE(R"(
|
|
local x = setmetatable({ x = 123 }, { __index = { x = 'foo' } })
|
|
)");
|
|
}
|
|
|
|
SUBCASE("only_meta_supplies_property")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
local x = setmetatable({}, { __index = { x = 123 } })
|
|
)");
|
|
}
|
|
|
|
CHECK(traverseForType(requireType("x"), Path(TypePath::Property::read("x")), builtinTypes) == builtinTypes->numberType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "index")
|
|
{
|
|
SUBCASE("unions")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type T = number | string | boolean
|
|
)");
|
|
|
|
SUBCASE("in_bounds")
|
|
{
|
|
CHECK(traverseForType(requireTypeAlias("T"), Path(TypePath::Index{1}), builtinTypes) == builtinTypes->stringType);
|
|
}
|
|
|
|
SUBCASE("out_of_bounds")
|
|
{
|
|
CHECK(traverseForType(requireTypeAlias("T"), Path(TypePath::Index{97}), builtinTypes) == std::nullopt);
|
|
}
|
|
}
|
|
|
|
SUBCASE("intersections")
|
|
{
|
|
// use functions to avoid the intersection being normalized away
|
|
TYPESOLVE_CODE(R"(
|
|
type T = (() -> ()) & ((true) -> false) & ((false) -> true)
|
|
)");
|
|
|
|
SUBCASE("in_bounds")
|
|
{
|
|
auto result = traverseForType(requireTypeAlias("T"), Path(TypePath::Index{1}), builtinTypes);
|
|
CHECK(result);
|
|
|
|
if (result)
|
|
CHECK(toString(*result) == "(true) -> false");
|
|
}
|
|
|
|
SUBCASE("out_of_bounds")
|
|
{
|
|
CHECK(traverseForType(requireTypeAlias("T"), Path(TypePath::Index{97}), builtinTypes) == std::nullopt);
|
|
}
|
|
}
|
|
|
|
SUBCASE("type_packs")
|
|
{
|
|
// use functions to avoid the intersection being normalized away
|
|
TYPESOLVE_CODE(R"(
|
|
type T = (number, string, true, false) -> ()
|
|
)");
|
|
|
|
SUBCASE("in_bounds")
|
|
{
|
|
Path path = Path({TypePath::PackField::Arguments, TypePath::Index{1}});
|
|
auto result = traverseForType(requireTypeAlias("T"), path, builtinTypes);
|
|
CHECK(result == builtinTypes->stringType);
|
|
}
|
|
|
|
SUBCASE("out_of_bounds")
|
|
{
|
|
Path path = Path({TypePath::PackField::Arguments, TypePath::Index{72}});
|
|
auto result = traverseForType(requireTypeAlias("T"), path, builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ClassFixture, "metatables")
|
|
{
|
|
SUBCASE("string")
|
|
{
|
|
auto result = traverseForType(builtinTypes->stringType, Path(TypeField::Metatable), builtinTypes);
|
|
CHECK(result == getMetatable(builtinTypes->stringType, builtinTypes));
|
|
}
|
|
|
|
SUBCASE("string_singleton")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type T = "foo"
|
|
)");
|
|
|
|
auto result = traverseForType(requireTypeAlias("T"), Path(TypeField::Metatable), builtinTypes);
|
|
CHECK(result == getMetatable(builtinTypes->stringType, builtinTypes));
|
|
}
|
|
|
|
SUBCASE("table")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type Table = { foo: number }
|
|
type Metatable = { bar: number }
|
|
local tbl: Table = { foo = 123 }
|
|
local mt: Metatable = { bar = 456 }
|
|
local res = setmetatable(tbl, mt)
|
|
)");
|
|
|
|
// Tricky test setup because 'setmetatable' mutates the argument 'tbl' type
|
|
auto result = traverseForType(requireType("res"), Path(TypeField::Table), builtinTypes);
|
|
auto expected = lookupType("Table");
|
|
REQUIRE(expected);
|
|
CHECK(result == follow(*expected));
|
|
}
|
|
|
|
SUBCASE("metatable")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
local mt = { foo = 123 }
|
|
local tbl = setmetatable({}, mt)
|
|
)");
|
|
|
|
auto result = traverseForType(requireType("tbl"), Path(TypeField::Metatable), builtinTypes);
|
|
CHECK(result == requireType("mt"));
|
|
}
|
|
|
|
SUBCASE("class")
|
|
{
|
|
auto result = traverseForType(vector2InstanceType, Path(TypeField::Metatable), builtinTypes);
|
|
// ClassFixture's Vector2 metatable is just an empty table, but it's there.
|
|
CHECK(result);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "bounds")
|
|
{
|
|
SUBCASE("free_type")
|
|
{
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
unfreeze(arena);
|
|
|
|
TypeId ty = arena.freshType(frontend.globals.globalScope.get());
|
|
FreeType* ft = getMutable<FreeType>(ty);
|
|
|
|
SUBCASE("upper")
|
|
{
|
|
ft->upperBound = builtinTypes->numberType;
|
|
auto result = traverseForType(ty, Path(TypeField::UpperBound), builtinTypes);
|
|
CHECK(result == builtinTypes->numberType);
|
|
}
|
|
|
|
SUBCASE("lower")
|
|
{
|
|
ft->lowerBound = builtinTypes->booleanType;
|
|
auto result = traverseForType(ty, Path(TypeField::LowerBound), builtinTypes);
|
|
CHECK(result == builtinTypes->booleanType);
|
|
}
|
|
}
|
|
|
|
SUBCASE("unbounded_type")
|
|
{
|
|
CHECK(traverseForType(builtinTypes->numberType, Path(TypeField::UpperBound), builtinTypes) == std::nullopt);
|
|
CHECK(traverseForType(builtinTypes->numberType, Path(TypeField::LowerBound), builtinTypes) == std::nullopt);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "indexers")
|
|
{
|
|
SUBCASE("table")
|
|
{
|
|
SUBCASE("lookup_indexer")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type T = { [string]: boolean }
|
|
)");
|
|
|
|
auto lookupResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexLookup), builtinTypes);
|
|
auto resultResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexResult), builtinTypes);
|
|
|
|
CHECK(lookupResult == builtinTypes->stringType);
|
|
CHECK(resultResult == builtinTypes->booleanType);
|
|
}
|
|
|
|
SUBCASE("no_indexer")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type T = { y: number }
|
|
)");
|
|
|
|
auto lookupResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexLookup), builtinTypes);
|
|
auto resultResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexResult), builtinTypes);
|
|
|
|
CHECK(lookupResult == std::nullopt);
|
|
CHECK(resultResult == std::nullopt);
|
|
}
|
|
}
|
|
|
|
// TODO: Class types
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "negated")
|
|
{
|
|
SUBCASE("valid")
|
|
{
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
unfreeze(arena);
|
|
|
|
TypeId ty = arena.addType(NegationType{builtinTypes->numberType});
|
|
auto result = traverseForType(ty, Path(TypeField::Negated), builtinTypes);
|
|
CHECK(result == builtinTypes->numberType);
|
|
}
|
|
|
|
SUBCASE("not_negation")
|
|
{
|
|
auto result = traverseForType(builtinTypes->numberType, Path(TypeField::Negated), builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "variadic")
|
|
{
|
|
SUBCASE("valid")
|
|
{
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
unfreeze(arena);
|
|
|
|
TypePackId tp = arena.addTypePack(VariadicTypePack{builtinTypes->numberType});
|
|
auto result = traverseForType(tp, Path(TypeField::Variadic), builtinTypes);
|
|
CHECK(result == builtinTypes->numberType);
|
|
}
|
|
|
|
SUBCASE("not_variadic")
|
|
{
|
|
auto result = traverseForType(builtinTypes->numberType, Path(TypeField::Variadic), builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "arguments")
|
|
{
|
|
SUBCASE("function")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
function f(x: number, y: string)
|
|
end
|
|
)");
|
|
|
|
auto result = traverseForPack(requireType("f"), Path(PackField::Arguments), builtinTypes);
|
|
CHECK(result);
|
|
if (result)
|
|
CHECK(toString(*result) == "number, string");
|
|
}
|
|
|
|
SUBCASE("not_function")
|
|
{
|
|
auto result = traverseForPack(builtinTypes->booleanType, Path(PackField::Arguments), builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "returns")
|
|
{
|
|
SUBCASE("function")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
function f(): (number, string)
|
|
return 123, "foo"
|
|
end
|
|
)");
|
|
|
|
auto result = traverseForPack(requireType("f"), Path(PackField::Returns), builtinTypes);
|
|
CHECK(result);
|
|
if (result)
|
|
CHECK(toString(*result) == "number, string");
|
|
}
|
|
|
|
SUBCASE("not_function")
|
|
{
|
|
auto result = traverseForPack(builtinTypes->booleanType, Path(PackField::Returns), builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "tail")
|
|
{
|
|
SUBCASE("has_tail")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type T = (number, string, ...boolean) -> ()
|
|
)");
|
|
|
|
auto result = traverseForPack(requireTypeAlias("T"), Path({PackField::Arguments, PackField::Tail}), builtinTypes);
|
|
CHECK(result);
|
|
if (result)
|
|
CHECK(toString(*result) == "...boolean");
|
|
}
|
|
|
|
SUBCASE("finite_pack")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type T = (number, string) -> ()
|
|
)");
|
|
|
|
auto result = traverseForPack(requireTypeAlias("T"), Path({PackField::Arguments, PackField::Tail}), builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
|
|
SUBCASE("type")
|
|
{
|
|
auto result = traverseForPack(builtinTypes->stringType, Path({PackField::Arguments, PackField::Tail}), builtinTypes);
|
|
CHECK(result == std::nullopt);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "cycles" * doctest::timeout(0.5))
|
|
{
|
|
// This will fail an occurs check, but it's a quick example of a cyclic type
|
|
// where there _is_ no traversal.
|
|
SUBCASE("bound_cycle")
|
|
{
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
unfreeze(arena);
|
|
|
|
TypeId a = arena.addType(BlockedType{});
|
|
TypeId b = arena.addType(BoundType{a});
|
|
asMutable(a)->ty.emplace<BoundType>(b);
|
|
|
|
CHECK_THROWS(traverseForType(a, Path(TypeField::IndexResult), builtinTypes));
|
|
}
|
|
|
|
SUBCASE("table_contains_itself")
|
|
{
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
unfreeze(arena);
|
|
|
|
TypeId tbl = arena.addType(TableType{});
|
|
getMutable<TableType>(tbl)->props["a"] = Luau::Property(tbl);
|
|
|
|
auto result = traverseForType(tbl, Path(TypePath::Property{"a", true}), builtinTypes);
|
|
CHECK(result == tbl);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "step_limit")
|
|
{
|
|
ScopedFastInt sfi(DFInt::LuauTypePathMaximumTraverseSteps, 2);
|
|
|
|
TYPESOLVE_CODE(R"(
|
|
type T = {
|
|
x: {
|
|
y: {
|
|
z: number
|
|
}
|
|
}
|
|
}
|
|
)");
|
|
|
|
TypeId root = requireTypeAlias("T");
|
|
Path path = PathBuilder().readProp("x").readProp("y").readProp("z").build();
|
|
auto result = traverseForType(root, path, builtinTypes);
|
|
CHECK(!result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathBuiltinsFixture, "complex_chains")
|
|
{
|
|
SUBCASE("add_metamethod_return_type")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type Meta = {
|
|
__add: (Tab, Tab) -> number,
|
|
}
|
|
|
|
type Tab = typeof(setmetatable({}, {} :: Meta))
|
|
)");
|
|
|
|
TypeId root = requireTypeAlias("Tab");
|
|
Path path = PathBuilder().mt().readProp("__add").rets().index(0).build();
|
|
auto result = traverseForType(root, path, builtinTypes);
|
|
CHECK(result == builtinTypes->numberType);
|
|
}
|
|
|
|
SUBCASE("overloaded_fn_overload_one_argument_two")
|
|
{
|
|
TYPESOLVE_CODE(R"(
|
|
type Obj = {
|
|
method: ((true, false) -> string) & ((string) -> number)
|
|
}
|
|
)");
|
|
|
|
TypeId root = requireTypeAlias("Obj");
|
|
Path path = PathBuilder().readProp("method").index(0).args().index(1).build();
|
|
auto result = traverseForType(root, path, builtinTypes);
|
|
CHECK(*result == builtinTypes->falseType);
|
|
}
|
|
}
|
|
|
|
TEST_SUITE_END(); // TypePathTraversal
|
|
|
|
TEST_SUITE_BEGIN("TypePathToString");
|
|
|
|
TEST_CASE("field")
|
|
{
|
|
ScopedFastFlag sff[] = {
|
|
{FFlag::LuauSolverV2, false},
|
|
};
|
|
|
|
CHECK(toString(PathBuilder().prop("foo").build()) == R"(["foo"])");
|
|
}
|
|
|
|
TEST_CASE("index")
|
|
{
|
|
CHECK(toString(PathBuilder().index(0).build()) == "[0]");
|
|
}
|
|
|
|
TEST_CASE("chain")
|
|
{
|
|
CHECK(toString(PathBuilder().index(0).mt().build()) == "[0].metatable()");
|
|
}
|
|
|
|
TEST_SUITE_END(); // TypePathToString
|
|
|
|
TEST_SUITE_BEGIN("TypePathBuilder");
|
|
|
|
TEST_CASE("empty_path")
|
|
{
|
|
Path p = PathBuilder().build();
|
|
CHECK(p.empty());
|
|
}
|
|
|
|
TEST_CASE("prop")
|
|
{
|
|
ScopedFastFlag sff[] = {
|
|
{FFlag::LuauSolverV2, false},
|
|
};
|
|
|
|
Path p = PathBuilder().prop("foo").build();
|
|
CHECK(p == Path(TypePath::Property{"foo"}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "readProp")
|
|
{
|
|
Path p = PathBuilder().readProp("foo").build();
|
|
CHECK(p == Path(TypePath::Property::read("foo")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypePathFixture, "writeProp")
|
|
{
|
|
Path p = PathBuilder().writeProp("foo").build();
|
|
CHECK(p == Path(TypePath::Property::write("foo")));
|
|
}
|
|
|
|
TEST_CASE("index")
|
|
{
|
|
Path p = PathBuilder().index(0).build();
|
|
CHECK(p == Path(TypePath::Index{0}));
|
|
}
|
|
|
|
TEST_CASE("fields")
|
|
{
|
|
CHECK(PathBuilder().mt().build() == Path(TypeField::Metatable));
|
|
CHECK(PathBuilder().lb().build() == Path(TypeField::LowerBound));
|
|
CHECK(PathBuilder().ub().build() == Path(TypeField::UpperBound));
|
|
CHECK(PathBuilder().indexKey().build() == Path(TypeField::IndexLookup));
|
|
CHECK(PathBuilder().indexValue().build() == Path(TypeField::IndexResult));
|
|
CHECK(PathBuilder().negated().build() == Path(TypeField::Negated));
|
|
CHECK(PathBuilder().variadic().build() == Path(TypeField::Variadic));
|
|
CHECK(PathBuilder().args().build() == Path(PackField::Arguments));
|
|
CHECK(PathBuilder().rets().build() == Path(PackField::Returns));
|
|
CHECK(PathBuilder().tail().build() == Path(PackField::Tail));
|
|
}
|
|
|
|
TEST_CASE("chained")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CHECK(
|
|
PathBuilder().index(0).readProp("foo").mt().readProp("bar").args().index(1).build() ==
|
|
Path({Index{0}, TypePath::Property::read("foo"), TypeField::Metatable, TypePath::Property::read("bar"), PackField::Arguments, Index{1}})
|
|
);
|
|
}
|
|
|
|
TEST_SUITE_END(); // TypePathBuilder
|