mirror of
https://github.com/luau-lang/luau.git
synced 2025-05-04 10:33:46 +01:00
# General * Internally rename `ClassType` to `ExternType`. In definition files, the syntax to define these types has changed to `declare extern type Foo with prop: type end` * Add `luarequire_registermodule` to Luau.Require * Support yieldable Luau C functions calling other functions * Store return types as `AstTypePack*` on Ast nodes ## New Solver * Improve the logic that determines constraint dispatch ordering * Fix a crash in the type solver that arose when using multi-return functions with `string.format` * Fix https://github.com/luau-lang/luau/issues/1736 * Initial steps toward rethinking function generalization: * Instead of generalizing every type in a function all at once, we will instead generalize individual type variables once their bounds have been fully resolved. This will make it possible to properly interleave type function reduction and generalization. * Magic functions are no longer considered magical in cases where they are not explicitly called by the code. * The most prominent example of this is in `for..in` loops where the function call is part of the desugaring process. * Almost all magic functions work by directly inspecting the AST, so they can't work without an AST fragment anyway. * Further, none of the magic functions we have are usefully used in this way. Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Sora Kanosue <skanosue@roblox.com> Co-authored-by: Talha Pathan <tpathan@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
847 lines
21 KiB
C++
847 lines
21 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#include "Luau/AstQuery.h"
|
|
#include "Luau/BuiltinDefinitions.h"
|
|
#include "Luau/Scope.h"
|
|
#include "Luau/TypeInfer.h"
|
|
#include "Luau/Type.h"
|
|
|
|
#include "Fixture.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
|
|
LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions)
|
|
LUAU_FASTFLAG(DebugLuauGreedyGeneralization)
|
|
LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect)
|
|
|
|
using namespace Luau;
|
|
|
|
TEST_SUITE_BEGIN("TypeInferModules");
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "dcr_require_basic")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
--!strict
|
|
return {
|
|
a = 1,
|
|
}
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
--!strict
|
|
local A = require(game.A)
|
|
|
|
local b = A.a
|
|
)";
|
|
|
|
CheckResult aResult = frontend.check("game/A");
|
|
LUAU_REQUIRE_NO_ERRORS(aResult);
|
|
|
|
CheckResult bResult = frontend.check("game/B");
|
|
LUAU_REQUIRE_NO_ERRORS(bResult);
|
|
|
|
ModulePtr b = frontend.moduleResolver.getModule("game/B");
|
|
REQUIRE(b != nullptr);
|
|
std::optional<TypeId> bType = requireType(b, "b");
|
|
REQUIRE(bType);
|
|
CHECK(toString(*bType) == "number");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "require")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
local function hooty(x: number): string
|
|
return "Hi there!"
|
|
end
|
|
|
|
return {hooty=hooty}
|
|
)";
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
fileResolver.source["game/B"] = R"(
|
|
local Hooty = require(game.A)
|
|
|
|
local h = 4
|
|
local i = Hooty.hooty(h)
|
|
)";
|
|
}
|
|
else
|
|
{
|
|
fileResolver.source["game/B"] = R"(
|
|
local Hooty = require(game.A)
|
|
|
|
local h -- free!
|
|
local i = Hooty.hooty(h)
|
|
)";
|
|
}
|
|
|
|
CheckResult aResult = frontend.check("game/A");
|
|
dumpErrors(aResult);
|
|
LUAU_REQUIRE_NO_ERRORS(aResult);
|
|
|
|
CheckResult bResult = frontend.check("game/B");
|
|
dumpErrors(bResult);
|
|
LUAU_REQUIRE_NO_ERRORS(bResult);
|
|
|
|
ModulePtr b = frontend.moduleResolver.getModule("game/B");
|
|
|
|
REQUIRE(b != nullptr);
|
|
|
|
dumpErrors(bResult);
|
|
|
|
std::optional<TypeId> iType = requireType(b, "i");
|
|
REQUIRE_EQ("string", toString(*iType));
|
|
|
|
std::optional<TypeId> hType = requireType(b, "h");
|
|
REQUIRE_EQ("number", toString(*hType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "require_types")
|
|
{
|
|
fileResolver.source["workspace/A"] = R"(
|
|
export type Point = {x: number, y: number}
|
|
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["workspace/B"] = R"(
|
|
local Hooty = require(workspace.A)
|
|
|
|
local h: Hooty.Point
|
|
)";
|
|
|
|
CheckResult bResult = frontend.check("workspace/B");
|
|
LUAU_REQUIRE_NO_ERRORS(bResult);
|
|
|
|
ModulePtr b = frontend.moduleResolver.getModule("workspace/B");
|
|
REQUIRE(b != nullptr);
|
|
|
|
TypeId hType = requireType(b, "h");
|
|
REQUIRE_MESSAGE(bool(get<TableType>(hType)), "Expected table but got " << toString(hType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "require_a_variadic_function")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
local T = {}
|
|
function T.f(...) end
|
|
return T
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local A = require(game.A)
|
|
local f = A.f
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
|
|
ModulePtr bModule = frontend.moduleResolver.getModule("game/B");
|
|
REQUIRE(bModule != nullptr);
|
|
|
|
TypeId f = follow(requireType(bModule, "f"));
|
|
|
|
const FunctionType* ftv = get<FunctionType>(f);
|
|
REQUIRE(ftv);
|
|
|
|
auto iter = begin(ftv->argTypes);
|
|
auto endIter = end(ftv->argTypes);
|
|
|
|
REQUIRE(iter == endIter);
|
|
REQUIRE(iter.tail());
|
|
|
|
CHECK(get<VariadicTypePack>(*iter.tail()));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "cross_module_table_freeze")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
--!strict
|
|
return {
|
|
a = 1,
|
|
}
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
--!strict
|
|
return table.freeze(require(game.A))
|
|
)";
|
|
|
|
CheckResult aResult = frontend.check("game/A");
|
|
LUAU_REQUIRE_NO_ERRORS(aResult);
|
|
|
|
CheckResult bResult = frontend.check("game/B");
|
|
LUAU_REQUIRE_NO_ERRORS(bResult);
|
|
|
|
ModulePtr a = frontend.moduleResolver.getModule("game/A");
|
|
REQUIRE(a != nullptr);
|
|
// confirm that no cross-module mutation happened here!
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(toString(a->returnType) == "{ a: number }");
|
|
else
|
|
CHECK(toString(a->returnType) == "{| a: number |}");
|
|
|
|
ModulePtr b = frontend.moduleResolver.getModule("game/B");
|
|
REQUIRE(b != nullptr);
|
|
// confirm that no cross-module mutation happened here!
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(toString(b->returnType) == "{ read a: number }");
|
|
else
|
|
CHECK(toString(b->returnType) == "{| a: number |}");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "type_error_of_unknown_qualified_type")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local p: SomeModule.DoesNotExist
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
REQUIRE_EQ(result.errors[0], (TypeError{Location{{1, 17}, {1, 40}}, UnknownSymbol{"SomeModule.DoesNotExist"}}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "require_module_that_does_not_export")
|
|
{
|
|
const std::string sourceA = R"(
|
|
)";
|
|
|
|
const std::string sourceB = R"(
|
|
local Hooty = require(script.Parent.A)
|
|
)";
|
|
|
|
fileResolver.source["game/Workspace/A"] = sourceA;
|
|
fileResolver.source["game/Workspace/B"] = sourceB;
|
|
|
|
frontend.check("game/Workspace/A");
|
|
frontend.check("game/Workspace/B");
|
|
|
|
ModulePtr aModule = frontend.moduleResolver.getModule("game/Workspace/A");
|
|
ModulePtr bModule = frontend.moduleResolver.getModule("game/Workspace/B");
|
|
|
|
CHECK(aModule->errors.empty());
|
|
REQUIRE_EQ(1, bModule->errors.size());
|
|
CHECK_MESSAGE(get<IllegalRequire>(bModule->errors[0]), "Should be IllegalRequire: " << toString(bModule->errors[0]));
|
|
|
|
auto hootyType = requireType(bModule, "Hooty");
|
|
|
|
CHECK_EQ("*error-type*", toString(hootyType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "warn_if_you_try_to_require_a_non_modulescript")
|
|
{
|
|
fileResolver.source["Modules/A"] = "";
|
|
fileResolver.sourceTypes["Modules/A"] = SourceCode::Script;
|
|
|
|
fileResolver.source["Modules/B"] = R"(
|
|
local M = require(script.Parent.A)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Modules/B");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK(get<IllegalRequire>(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "general_require_call_expression")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
--!strict
|
|
return { def = 4 }
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
--!strict
|
|
local tbl = { abc = require(game.A) }
|
|
local a : string = ""
|
|
a = tbl.abc.def
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "general_require_type_mismatch")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
return { def = 4 }
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local tbl: string = require(game.A)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("Type '{ def: number }' could not be converted into 'string'", toString(result.errors[0]));
|
|
else
|
|
CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local n = {}
|
|
function n:Clone() end
|
|
|
|
local m = {}
|
|
|
|
function m.a(x)
|
|
x:Clone()
|
|
end
|
|
|
|
function m.b()
|
|
m.a(n)
|
|
end
|
|
|
|
return m
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "custom_require_global")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!nonstrict
|
|
require = function(a) end
|
|
|
|
local crash = require(game.A)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "require_failed_module")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
return unfortunately()
|
|
)";
|
|
|
|
CheckResult aResult = frontend.check("game/A");
|
|
LUAU_REQUIRE_ERRORS(aResult);
|
|
|
|
CheckResult result = check(R"(
|
|
local ModuleA = require(game.A)
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
std::optional<TypeId> oty = requireType("ModuleA");
|
|
|
|
CHECK_EQ("*error-type*", toString(*oty));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
export type Type = { unrelated: boolean }
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local types = require(game.A)
|
|
type Type = types.Type
|
|
local x: Type = {}
|
|
function x:Destroy(): () end
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_2")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
export type Type = { x: { a: number } }
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local types = require(game.A)
|
|
type Type = types.Type
|
|
local x: Type = { x = { a = 2 } }
|
|
type Rename = typeof(x.x)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_3")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
local y = setmetatable({}, {})
|
|
export type Type = { x: typeof(y) }
|
|
return { x = y }
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local types = require(game.A)
|
|
type Type = types.Type
|
|
local x: Type = types
|
|
type Rename = typeof(x.x)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_4")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
export type Array<T> = {T}
|
|
local arrayops = {}
|
|
function arrayops.foo(x: Array<any>) end
|
|
return arrayops
|
|
)";
|
|
|
|
CheckResult result = check(R"(
|
|
local arrayops = require(game.A)
|
|
|
|
local tbl = {}
|
|
tbl.a = 2
|
|
function tbl:foo(b: number, c: number)
|
|
-- introduce BoundType to imported type
|
|
arrayops.foo(self._regions)
|
|
end
|
|
-- this alias decreases function type level and causes a demotion of its type
|
|
type Table = typeof(tbl)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_5")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
export type Type = {x: number, y: number}
|
|
local arrayops = {}
|
|
function arrayops.foo(x: Type) end
|
|
return arrayops
|
|
)";
|
|
|
|
CheckResult result = check(R"(
|
|
local arrayops = require(game.A)
|
|
|
|
local tbl = {}
|
|
tbl.a = 2
|
|
function tbl:foo(b: number, c: number)
|
|
-- introduce boundTo TableType to imported type
|
|
self.x.a = 2
|
|
arrayops.foo(self.x)
|
|
end
|
|
-- this alias decreases function type level and causes a demotion of its type
|
|
type Table = typeof(tbl)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
export type T = { x: number }
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
export type T = { x: string }
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/C"] = R"(
|
|
local A = require(game.A)
|
|
local B = require(game.B)
|
|
local a: A.T = { x = 2 }
|
|
local b: B.T = a
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/C");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
|
|
{
|
|
const std::string expected = "Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'; \n"
|
|
"this is because accessing `x` results in `number` in the former type and `string` in the latter type, and "
|
|
"`number` is not exactly `string`";
|
|
CHECK(expected == toString(result.errors[0]));
|
|
}
|
|
else if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK(
|
|
toString(result.errors.at(0)) ==
|
|
"Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'; at [read \"x\"], number is not exactly string"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
|
|
caused by:
|
|
Property 'x' is not compatible.
|
|
Type 'number' could not be converted into 'string' in an invariant context)";
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
export type Wrap<T> = { x: T }
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local A = require(game.A)
|
|
export type T = A.Wrap<number>
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/C"] = R"(
|
|
local A = require(game.A)
|
|
export type T = A.Wrap<string>
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/D"] = R"(
|
|
local A = require(game.B)
|
|
local B = require(game.C)
|
|
local a: A.T = { x = 2 }
|
|
local b: B.T = a
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/D");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
|
|
{
|
|
const std::string expected = "Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'; \n"
|
|
"this is because accessing `x` results in `number` in the former type and `string` in the latter type, and "
|
|
"`number` is not exactly `string`";
|
|
CHECK(expected == toString(result.errors[0]));
|
|
}
|
|
else if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK(
|
|
toString(result.errors.at(0)) ==
|
|
"Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'; at [read \"x\"], number is not exactly string"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
|
|
caused by:
|
|
Property 'x' is not compatible.
|
|
Type 'number' could not be converted into 'string' in an invariant context)";
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
return function(...) end
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local l0 = require(game.A)
|
|
return l0
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/B");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_anyify_variadic_return_must_follow")
|
|
{
|
|
CheckResult result = check(R"(
|
|
return unpack(l0[_])
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "check_imported_module_names")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
return function(...) end
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local l0 = require(game.A)
|
|
return l0
|
|
)";
|
|
|
|
CheckResult result = check(R"(
|
|
local l0 = require(game.B)
|
|
if true then
|
|
local l1 = require(game.A)
|
|
end
|
|
return l0
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
ModulePtr mod = getMainModule();
|
|
REQUIRE(mod);
|
|
|
|
REQUIRE(mod->scopes.size() == 4);
|
|
CHECK(mod->scopes[0].second->importedModules["l0"] == "game/B");
|
|
CHECK(mod->scopes[3].second->importedModules["l1"] == "game/A");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_scope_is_nullptr_after_shallow_copy")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
frontend.options.retainFullTypeGraphs = false;
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
-- Roughly taken from ReactTypes.lua
|
|
type CoreBinding<T> = {}
|
|
type BindingMap = {}
|
|
export type Binding<T> = CoreBinding<T> & BindingMap
|
|
|
|
return {}
|
|
)";
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local Types = require(game.A)
|
|
type Binding<T> = Types.Binding<T>
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_free_variables_are_generialized_across_function_boundaries")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
-- Roughly taken from react-shallow-renderer
|
|
function createUpdater(renderer)
|
|
local updater = {
|
|
_renderer = renderer,
|
|
}
|
|
|
|
function updater.enqueueForceUpdate(publicInstance, callback, _callerName)
|
|
updater._renderer.render(
|
|
updater._renderer,
|
|
updater._renderer._element,
|
|
updater._renderer._context
|
|
)
|
|
end
|
|
|
|
function updater.enqueueReplaceState(
|
|
publicInstance,
|
|
completeState,
|
|
callback,
|
|
_callerName
|
|
)
|
|
updater._renderer.render(
|
|
updater._renderer,
|
|
updater._renderer._element,
|
|
updater._renderer._context
|
|
)
|
|
end
|
|
|
|
function updater.enqueueSetState(publicInstance, partialState, callback, _callerName)
|
|
local currentState = updater._renderer._newState or publicInstance.state
|
|
updater._renderer.render(
|
|
updater._renderer,
|
|
updater._renderer._element,
|
|
updater._renderer._context
|
|
)
|
|
end
|
|
|
|
return updater
|
|
end
|
|
|
|
local ReactShallowRenderer = {}
|
|
|
|
function ReactShallowRenderer:_reset()
|
|
self._updater = createUpdater(self)
|
|
end
|
|
|
|
return ReactShallowRenderer
|
|
)";
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local ReactShallowRenderer = require(game.A);
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "untitled_segfault_number_13")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
-- minimized from roblox-requests/http/src/response.lua
|
|
local Response = {}
|
|
Response.__index = Response
|
|
function Response.new(content_type)
|
|
-- creates response object from original request and roblox http response
|
|
local self = setmetatable({}, Response)
|
|
self.content_type = content_type
|
|
return self
|
|
end
|
|
|
|
function Response:xml(ignore_content_type)
|
|
if ignore_content_type or self.content_type:find("+xml") or self.content_type:find("/xml") then
|
|
else
|
|
end
|
|
end
|
|
|
|
---------------
|
|
|
|
return Response
|
|
)";
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local _ = require(game.A);
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "spooky_blocked_type_laundered_by_bound_type")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
local Cache = {}
|
|
|
|
Cache.settings = {}
|
|
|
|
Cache.data = {}
|
|
|
|
function Cache.should_cache(url)
|
|
url = url:split("?")[1]
|
|
|
|
for key, _ in pairs(Cache.settings) do
|
|
if url:match('') then
|
|
return key
|
|
end
|
|
end
|
|
|
|
return ""
|
|
end
|
|
|
|
function Cache.is_cached(url, req_id)
|
|
-- check local server cache first
|
|
|
|
local setting_key = Cache.should_cache(url)
|
|
local settings = Cache.settings[setting_key]
|
|
|
|
if not setting_key then
|
|
return false
|
|
end
|
|
|
|
if Cache.data[req_id] ~= nil then
|
|
return true
|
|
end
|
|
|
|
if Cache.settings[setting_key].cache_globally then
|
|
return false
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
function Cache.get_expire(url)
|
|
local setting_key = Cache.should_cache(url)
|
|
return Cache.settings[setting_key].expires or math.huge
|
|
end
|
|
|
|
return Cache
|
|
)";
|
|
|
|
auto result = check(R"(
|
|
local _ = require(game.A);
|
|
)");
|
|
|
|
if (FFlag::LuauAddCallConstraintForIterableFunctions && !FFlag::LuauOptimizeFalsyAndTruthyIntersect)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(3, result);
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "leaky_generics")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
|
|
auto result = check(R"(
|
|
local Cache = {}
|
|
|
|
Cache.settings = {}
|
|
|
|
function Cache.should_cache(url)
|
|
for key, _ in pairs(Cache.settings) do
|
|
return key
|
|
end
|
|
|
|
return ""
|
|
end
|
|
|
|
function Cache.is_cached(url)
|
|
local setting_key = Cache.should_cache(url)
|
|
local settings = Cache.settings[setting_key]
|
|
|
|
return settings
|
|
end
|
|
|
|
return Cache
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::DebugLuauGreedyGeneralization)
|
|
{
|
|
CHECK_EQ("(unknown) -> unknown", toString(requireTypeAtPosition({13, 23})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "cycles_dont_make_everything_any")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
--!strict
|
|
local module = {}
|
|
|
|
function module.foo()
|
|
return 2
|
|
end
|
|
|
|
function module.bar()
|
|
local m = require(game.B)
|
|
return m.foo() + 1
|
|
end
|
|
|
|
return module
|
|
)";
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
--!strict
|
|
local module = {}
|
|
|
|
function module.foo()
|
|
return 2
|
|
end
|
|
|
|
function module.bar()
|
|
local m = require(game.A)
|
|
return m.foo() + 1
|
|
end
|
|
|
|
return module
|
|
)";
|
|
|
|
frontend.check("game/A");
|
|
|
|
CHECK("module" == toString(frontend.moduleResolver.getModule("game/B")->returnType));
|
|
}
|
|
|
|
TEST_SUITE_END();
|