luau/tests/TypeInfer.definitions.test.cpp
checkraisefold f1d4621d59
Pre-populate/duplicate check class definitions (new solver) (#1493)
Closes #1492
Tested and working with the test case in the aforementioned issue, along
with the full defs of luau-lsp with no issues or type errors

In normal Luau files, you can use type aliases and type functions before
they are declared. The same extends to declaration files, **except** in
the new solver. The old solver perfectly allows this, and in fact
intentionally adds it:
db809395bf/Analysis/src/TypeInfer.cpp (L1711-L1717)

This causes *much* headache and pain for external projects that make use
of declaration files; namely, luau-lsp generates them from MaximumADHD's
API dump, which is not ordered by dependency. This means silent
error-types popping up everywhere because types are used before they are
declared. The workaround would be to make code to manually reorder class
definitions based on their dependencies with a bunch of code, but this
is clearly not ideal, and won't work for classes dependent on each
other/recursive.

The solution used here is the same as is used for type aliases - the
name binding for the class is given a blocked type before running the
rest of constraint generation on the block. Questions remain:
- Should the logic be split off of `checkAliases`?
- Should a bound type be used, or should the (blocked) binding type be
directly emplaced with the class type? What are the ramifications of
emplacing with the bound versus the raw type? One ramification was
initially ran into through an assertion because the class
`superTy`/`parent` was bound, and several pieces of code assume it is
not, so it had to be made followed.
- Is folllowing `superTy` to set `parent` the correct workaround for the
assertions thrown, or should the code expecting `parent` to be a
ClassType without following it be modified instead to follow `parent`?
- Should `scope->privateTypeBindings` also be checked for the duplicate
error? I would presume so, since having a class with the same name as a
private alias or type function should error as well?

The extraneous whitespace changes are clang-format ones done
automatically that should've been done in the last release - I can
remove them if necessary and let another sync or OSS cleanup commit fix
it.
2024-11-05 15:21:18 -08:00

544 lines
15 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h"
#include "Luau/TypeInfer.h"
#include "Luau/Type.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(LuauNewSolverPrePopulateClasses)
TEST_SUITE_BEGIN("DefinitionTests");
TEST_CASE_FIXTURE(Fixture, "definition_file_simple")
{
loadDefinition(R"(
declare foo: number
declare function bar(x: number): string
declare foo2: typeof(foo)
)");
TypeId globalFooTy = getGlobalBinding(frontend.globals, "foo");
CHECK_EQ(toString(globalFooTy), "number");
TypeId globalBarTy = getGlobalBinding(frontend.globals, "bar");
CHECK_EQ(toString(globalBarTy), "(number) -> string");
TypeId globalFoo2Ty = getGlobalBinding(frontend.globals, "foo2");
CHECK_EQ(toString(globalFoo2Ty), "number");
CheckResult result = check(R"(
local x: number = foo - 1
local y: string = bar(x)
local z: number | string = x
z = y
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "definition_file_loading")
{
loadDefinition(R"(
declare foo: number
export type Asdf = number | string
declare function bar(x: number): string
declare foo2: typeof(foo)
declare function var(...: any): string
)");
TypeId globalFooTy = getGlobalBinding(frontend.globals, "foo");
CHECK_EQ(toString(globalFooTy), "number");
std::optional<TypeFun> globalAsdfTy = frontend.globals.globalScope->lookupType("Asdf");
REQUIRE(bool(globalAsdfTy));
CHECK_EQ(toString(globalAsdfTy->type), "number | string");
TypeId globalBarTy = getGlobalBinding(frontend.globals, "bar");
CHECK_EQ(toString(globalBarTy), "(number) -> string");
TypeId globalFoo2Ty = getGlobalBinding(frontend.globals, "foo2");
CHECK_EQ(toString(globalFoo2Ty), "number");
TypeId globalVarTy = getGlobalBinding(frontend.globals, "var");
CHECK_EQ(toString(globalVarTy), "(...any) -> string");
CheckResult result = check(R"(
local x: number = foo + 1
local y: string = bar(x)
local z: Asdf = x
z = y
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "load_definition_file_errors_do_not_pollute_global_scope")
{
unfreeze(frontend.globals.globalTypes);
LoadDefinitionFileResult parseFailResult = frontend.loadDefinitionFile(
frontend.globals,
frontend.globals.globalScope,
R"(
declare foo
)",
"@test",
/* captureComments */ false
);
freeze(frontend.globals.globalTypes);
REQUIRE(!parseFailResult.success);
std::optional<Binding> fooTy = tryGetGlobalBinding(frontend.globals, "foo");
CHECK(!fooTy.has_value());
LoadDefinitionFileResult checkFailResult = frontend.loadDefinitionFile(
frontend.globals,
frontend.globals.globalScope,
R"(
local foo: string = 123
declare bar: typeof(foo)
)",
"@test",
/* captureComments */ false
);
REQUIRE(!checkFailResult.success);
std::optional<Binding> barTy = tryGetGlobalBinding(frontend.globals, "bar");
CHECK(!barTy.has_value());
}
TEST_CASE_FIXTURE(Fixture, "definition_file_classes")
{
loadDefinition(R"(
declare class Foo
X: number
function inheritance(self): number
end
declare class Bar extends Foo
Y: number
function foo(self, x: number): number
function foo(self, x: string): string
function __add(self, other: Bar): Bar
end
)");
CheckResult result = check(R"(
local x: Bar
local prop: number = x.Y
local inheritedProp: number = x.X
local method: number = x:foo(1)
local method2: string = x:foo("string")
local metamethod: Bar = x + x
local inheritedMethod: number = x:inheritance()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("prop")), "number");
CHECK_EQ(toString(requireType("inheritedProp")), "number");
CHECK_EQ(toString(requireType("method")), "number");
CHECK_EQ(toString(requireType("method2")), "string");
CHECK_EQ(toString(requireType("metamethod")), "Bar");
CHECK_EQ(toString(requireType("inheritedMethod")), "number");
}
TEST_CASE_FIXTURE(Fixture, "class_definitions_cannot_overload_non_function")
{
unfreeze(frontend.globals.globalTypes);
LoadDefinitionFileResult result = frontend.loadDefinitionFile(
frontend.globals,
frontend.globals.globalScope,
R"(
declare class A
X: number
X: string
end
)",
"@test",
/* captureComments */ false
);
freeze(frontend.globals.globalTypes);
REQUIRE(!result.success);
CHECK_EQ(result.parseResult.errors.size(), 0);
REQUIRE(bool(result.module));
REQUIRE_EQ(result.module->errors.size(), 1);
GenericError* ge = get<GenericError>(result.module->errors[0]);
REQUIRE(ge);
CHECK_EQ("Cannot overload non-function class member 'X'", ge->message);
}
TEST_CASE_FIXTURE(Fixture, "class_definitions_cannot_extend_non_class")
{
unfreeze(frontend.globals.globalTypes);
LoadDefinitionFileResult result = frontend.loadDefinitionFile(
frontend.globals,
frontend.globals.globalScope,
R"(
type NotAClass = {}
declare class Foo extends NotAClass
end
)",
"@test",
/* captureComments */ false
);
freeze(frontend.globals.globalTypes);
REQUIRE(!result.success);
CHECK_EQ(result.parseResult.errors.size(), 0);
REQUIRE(bool(result.module));
REQUIRE_EQ(result.module->errors.size(), 1);
GenericError* ge = get<GenericError>(result.module->errors[0]);
REQUIRE(ge);
CHECK_EQ("Cannot use non-class type 'NotAClass' as a superclass of class 'Foo'", ge->message);
}
TEST_CASE_FIXTURE(Fixture, "no_cyclic_defined_classes")
{
unfreeze(frontend.globals.globalTypes);
LoadDefinitionFileResult result = frontend.loadDefinitionFile(
frontend.globals,
frontend.globals.globalScope,
R"(
declare class Foo extends Bar
end
declare class Bar extends Foo
end
)",
"@test",
/* captureComments */ false
);
freeze(frontend.globals.globalTypes);
REQUIRE(!result.success);
}
TEST_CASE_FIXTURE(Fixture, "declaring_generic_functions")
{
loadDefinition(R"(
declare function f<a, b>(a: a, b: b): string
declare function g<a..., b...>(...: a...): b...
declare function h<a, b>(a: a, b: b): (b, a)
)");
CheckResult result = check(R"(
local x = f(1, true)
local y: number, z: string = g("foo", 123)
local w, u = h(1, true)
local f = f
local g = g
local h = h
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("x")), "string");
CHECK_EQ(toString(requireType("w")), "boolean");
CHECK_EQ(toString(requireType("u")), "number");
CHECK_EQ(toString(requireType("f")), "<a, b>(a, b) -> string");
CHECK_EQ(toString(requireType("g")), "<a..., b...>(a...) -> (b...)");
CHECK_EQ(toString(requireType("h")), "<a, b>(a, b) -> (b, a)");
}
TEST_CASE_FIXTURE(Fixture, "class_definition_function_prop")
{
loadDefinition(R"(
declare class Foo
X: (number) -> string
end
declare Foo: {
new: () -> Foo
}
)");
CheckResult result = check(R"(
local x: Foo = Foo.new()
local prop = x.X
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("prop")), "(number) -> string");
}
TEST_CASE_FIXTURE(Fixture, "definition_file_class_function_args")
{
loadDefinition(R"(
declare class Foo
function foo1(self, x: number): number
function foo2(self, x: number, y: string): number
y: (a: number, b: string) -> string
end
declare Foo: {
new: () -> Foo
}
)");
CheckResult result = check(R"(
local x: Foo = Foo.new()
local methodRef1 = x.foo1
local methodRef2 = x.foo2
local prop = x.y
)");
LUAU_REQUIRE_NO_ERRORS(result);
ToStringOptions opts;
opts.functionTypeArguments = true;
CHECK_EQ(toString(requireType("methodRef1"), opts), "(self: Foo, x: number) -> number");
CHECK_EQ(toString(requireType("methodRef2"), opts), "(self: Foo, x: number, y: string) -> number");
CHECK_EQ(toString(requireType("prop"), opts), "(a: number, b: string) -> string");
}
TEST_CASE_FIXTURE(Fixture, "definitions_documentation_symbols")
{
loadDefinition(R"(
declare x: string
export type Foo = string | number
declare class Bar
prop: string
end
declare y: {
x: number,
}
)");
std::optional<Binding> xBinding = frontend.globals.globalScope->linearSearchForBinding("x");
REQUIRE(bool(xBinding));
// note: loadDefinition uses the @test package name.
CHECK_EQ(xBinding->documentationSymbol, "@test/global/x");
std::optional<TypeFun> fooTy = frontend.globals.globalScope->lookupType("Foo");
REQUIRE(bool(fooTy));
CHECK_EQ(fooTy->type->documentationSymbol, "@test/globaltype/Foo");
std::optional<TypeFun> barTy = frontend.globals.globalScope->lookupType("Bar");
REQUIRE(bool(barTy));
CHECK_EQ(barTy->type->documentationSymbol, "@test/globaltype/Bar");
ClassType* barClass = getMutable<ClassType>(barTy->type);
REQUIRE(bool(barClass));
REQUIRE_EQ(barClass->props.count("prop"), 1);
CHECK_EQ(barClass->props["prop"].documentationSymbol, "@test/globaltype/Bar.prop");
std::optional<Binding> yBinding = frontend.globals.globalScope->linearSearchForBinding("y");
REQUIRE(bool(yBinding));
CHECK_EQ(yBinding->documentationSymbol, "@test/global/y");
TableType* yTtv = getMutable<TableType>(yBinding->typeId);
REQUIRE(bool(yTtv));
REQUIRE_EQ(yTtv->props.count("x"), 1);
CHECK_EQ(yTtv->props["x"].documentationSymbol, "@test/global/y.x");
}
TEST_CASE_FIXTURE(Fixture, "definitions_symbols_are_generated_for_recursively_referenced_types")
{
loadDefinition(R"(
declare class MyClass
function myMethod(self)
end
declare function myFunc(): MyClass
)");
std::optional<TypeFun> myClassTy = frontend.globals.globalScope->lookupType("MyClass");
REQUIRE(bool(myClassTy));
CHECK_EQ(myClassTy->type->documentationSymbol, "@test/globaltype/MyClass");
ClassType* cls = getMutable<ClassType>(myClassTy->type);
REQUIRE(bool(cls));
REQUIRE_EQ(cls->props.count("myMethod"), 1);
const auto& method = cls->props["myMethod"];
CHECK_EQ(method.documentationSymbol, "@test/globaltype/MyClass.myMethod");
FunctionType* function = getMutable<FunctionType>(method.type());
REQUIRE(function);
REQUIRE(function->definition.has_value());
CHECK(function->definition->definitionModuleName == "@test");
CHECK(function->definition->definitionLocation == Location({2, 12}, {2, 35}));
CHECK(!function->definition->varargLocation.has_value());
CHECK(function->definition->originalNameLocation == Location({2, 21}, {2, 29}));
}
TEST_CASE_FIXTURE(Fixture, "documentation_symbols_dont_attach_to_persistent_types")
{
loadDefinition(R"(
export type Evil = string
)");
std::optional<TypeFun> ty = frontend.globals.globalScope->lookupType("Evil");
REQUIRE(bool(ty));
CHECK_EQ(ty->type->documentationSymbol, std::nullopt);
}
TEST_CASE_FIXTURE(Fixture, "single_class_type_identity_in_global_types")
{
loadDefinition(R"(
declare class Cls
end
declare GetCls: () -> (Cls)
)");
CheckResult result = check(R"(
local s : Cls = GetCls()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "class_definition_overload_metamethods")
{
loadDefinition(R"(
declare class Vector3
end
declare class CFrame
function __mul(self, other: CFrame): CFrame
function __mul(self, other: Vector3): Vector3
end
declare function newVector3(): Vector3
declare function newCFrame(): CFrame
)");
CheckResult result = check(R"(
local base = newCFrame()
local shouldBeCFrame = base * newCFrame()
local shouldBeVector = base * newVector3()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("shouldBeCFrame")), "CFrame");
CHECK_EQ(toString(requireType("shouldBeVector")), "Vector3");
}
TEST_CASE_FIXTURE(Fixture, "class_definition_string_props")
{
loadDefinition(R"(
declare class Foo
["a property"]: string
end
)");
CheckResult result = check(R"(
local x: Foo
local y = x["a property"]
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("y")), "string");
}
TEST_CASE_FIXTURE(Fixture, "class_definition_malformed_string")
{
unfreeze(frontend.globals.globalTypes);
LoadDefinitionFileResult result = frontend.loadDefinitionFile(
frontend.globals,
frontend.globals.globalScope,
R"(
declare class Foo
["a\0property"]: string
end
)",
"@test",
/* captureComments */ false
);
freeze(frontend.globals.globalTypes);
REQUIRE(!result.success);
REQUIRE_EQ(result.parseResult.errors.size(), 1);
CHECK_EQ(result.parseResult.errors[0].getMessage(), "String literal contains malformed escape sequence or \\0");
}
TEST_CASE_FIXTURE(Fixture, "class_definition_indexer")
{
loadDefinition(R"(
declare class Foo
[number]: string
end
)");
CheckResult result = check(R"(
local x: Foo
local y = x[1]
)");
LUAU_REQUIRE_NO_ERRORS(result);
const ClassType* ctv = get<ClassType>(requireType("x"));
REQUIRE(ctv != nullptr);
REQUIRE(bool(ctv->indexer));
CHECK_EQ(*ctv->indexer->indexType, *builtinTypes->numberType);
CHECK_EQ(*ctv->indexer->indexResultType, *builtinTypes->stringType);
CHECK_EQ(toString(requireType("y")), "string");
}
TEST_CASE_FIXTURE(Fixture, "class_definitions_reference_other_classes")
{
ScopedFastFlag _{FFlag::LuauNewSolverPrePopulateClasses, true};
loadDefinition(R"(
declare class Channel
Messages: { Message }
OnMessage: (message: Message) -> ()
end
declare class Message
Text: string
Channel: Channel
end
)");
CheckResult result = check(R"(
local a: Channel
local b = a.Messages[1]
local c = b.Channel
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a")), "Channel");
CHECK_EQ(toString(requireType("b")), "Message");
CHECK_EQ(toString(requireType("c")), "Channel");
}
TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set")
{
LoadDefinitionFileResult result = loadDefinition(R"(
declare class Foo
end
)");
REQUIRE(result.success);
CHECK_EQ(result.sourceModule.name, "@test");
CHECK_EQ(result.sourceModule.humanReadableName, "@test");
std::optional<TypeFun> fooTy = frontend.globals.globalScope->lookupType("Foo");
REQUIRE(fooTy);
const ClassType* ctv = get<ClassType>(fooTy->type);
REQUIRE(ctv);
CHECK_EQ(ctv->definitionModuleName, "@test");
}
TEST_SUITE_END();