mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-14 06:00:39 +00:00
c5089def6e
* Fix a bug where reading a property from an unsealed table caused inference to improperly infer the existence of that property. * Fix #827 We have also made a lot of progress on the new solver and the JIT. Both projects are still in the process of being built out. Neither are ready for general use yet. We are mostly working to tighten up how the new solver handles refinements and updates to unsealed tables to bring it up to the same level as the old solver. --------- Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
1108 lines
33 KiB
C++
1108 lines
33 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/Frontend.h"
|
|
#include "Luau/RequireTracer.h"
|
|
|
|
#include "Fixture.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
#include <algorithm>
|
|
|
|
using namespace Luau;
|
|
|
|
namespace
|
|
{
|
|
|
|
struct NaiveModuleResolver : ModuleResolver
|
|
{
|
|
std::optional<ModuleInfo> resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) override
|
|
{
|
|
if (auto name = pathExprToModuleName(currentModuleName, pathExpr))
|
|
return {{*name, false}};
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
const ModulePtr getModule(const ModuleName& moduleName) const override
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
bool moduleExists(const ModuleName& moduleName) const override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::string getHumanReadableModuleName(const ModuleName& moduleName) const override
|
|
{
|
|
return moduleName;
|
|
}
|
|
};
|
|
|
|
NaiveModuleResolver naiveModuleResolver;
|
|
|
|
struct NaiveFileResolver : NullFileResolver
|
|
{
|
|
std::optional<ModuleInfo> resolveModule(const ModuleInfo* context, AstExpr* expr) override
|
|
{
|
|
if (AstExprGlobal* g = expr->as<AstExprGlobal>())
|
|
{
|
|
if (g->name == "Modules")
|
|
return ModuleInfo{"Modules"};
|
|
|
|
if (g->name == "game")
|
|
return ModuleInfo{"game"};
|
|
}
|
|
else if (AstExprIndexName* i = expr->as<AstExprIndexName>())
|
|
{
|
|
if (context)
|
|
return ModuleInfo{context->name + '/' + i->index.value, context->optional};
|
|
}
|
|
else if (AstExprCall* call = expr->as<AstExprCall>(); call && call->self && call->args.size >= 1 && context)
|
|
{
|
|
if (AstExprConstantString* index = call->args.data[0]->as<AstExprConstantString>())
|
|
{
|
|
AstName func = call->func->as<AstExprIndexName>()->index;
|
|
|
|
if (func == "GetService" && context->name == "game")
|
|
return ModuleInfo{"game/" + std::string(index->value.data, index->value.size)};
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
struct FrontendFixture : BuiltinsFixture
|
|
{
|
|
FrontendFixture()
|
|
{
|
|
addGlobalBinding(frontend, "game", frontend.typeChecker.anyType, "@test");
|
|
addGlobalBinding(frontend, "script", frontend.typeChecker.anyType, "@test");
|
|
}
|
|
};
|
|
|
|
TEST_SUITE_BEGIN("FrontendTest");
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "find_a_require")
|
|
{
|
|
AstStatBlock* program = parse(R"(
|
|
local M = require(Modules.Foo.Bar)
|
|
)");
|
|
|
|
NaiveFileResolver naiveFileResolver;
|
|
|
|
auto res = traceRequires(&naiveFileResolver, program, "");
|
|
CHECK_EQ(1, res.requireList.size());
|
|
CHECK_EQ(res.requireList[0].first, "Modules/Foo/Bar");
|
|
}
|
|
|
|
// It could be argued that this should not work.
|
|
TEST_CASE_FIXTURE(FrontendFixture, "find_a_require_inside_a_function")
|
|
{
|
|
AstStatBlock* program = parse(R"(
|
|
function foo()
|
|
local M = require(Modules.Foo.Bar)
|
|
end
|
|
)");
|
|
|
|
NaiveFileResolver naiveFileResolver;
|
|
|
|
auto res = traceRequires(&naiveFileResolver, program, "");
|
|
CHECK_EQ(1, res.requireList.size());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "real_source")
|
|
{
|
|
AstStatBlock* program = parse(R"(
|
|
return function()
|
|
local Modules = game:GetService("CoreGui").Gui.Modules
|
|
|
|
local Roact = require(Modules.Common.Roact)
|
|
local Rodux = require(Modules.Common.Rodux)
|
|
|
|
local AppReducer = require(Modules.LuaApp.AppReducer)
|
|
local AEAppReducer = require(Modules.LuaApp.Reducers.AEReducers.AEAppReducer)
|
|
local AETabList = require(Modules.LuaApp.Components.Avatar.UI.Views.Portrait.AETabList)
|
|
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
|
|
local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode)
|
|
local MockAvatarEditorTheme = require(Modules.LuaApp.TestHelpers.MockAvatarEditorTheming)
|
|
local FFlagAvatarEditorEnableThemes = settings():GetFFlag("AvatarEditorEnableThemes2")
|
|
end
|
|
)");
|
|
|
|
NaiveFileResolver naiveFileResolver;
|
|
|
|
auto res = traceRequires(&naiveFileResolver, program, "");
|
|
CHECK_EQ(8, res.requireList.size());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {b_value = A.hello}
|
|
)";
|
|
|
|
frontend.check("game/Gui/Modules/B");
|
|
|
|
ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"];
|
|
REQUIRE(bModule != nullptr);
|
|
CHECK(bModule->errors.empty());
|
|
Luau::dumpErrors(bModule);
|
|
|
|
auto bExports = first(bModule->returnType);
|
|
REQUIRE(!!bExports);
|
|
|
|
CHECK_EQ("{| b_value: number |}", toString(*bExports));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_cyclically_dependent_scripts")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
require(Modules.C)
|
|
return {}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/C"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
do local A = require(Modules.A) end
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["game/Gui/Modules/D"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
do local A = require(Modules.A) end
|
|
return {}
|
|
)";
|
|
|
|
CheckResult result1 = frontend.check("game/Gui/Modules/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(4, result1);
|
|
|
|
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[0]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[0]));
|
|
|
|
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[1]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[1]));
|
|
|
|
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[2]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[2]));
|
|
|
|
CheckResult result2 = frontend.check("game/Gui/Modules/D");
|
|
LUAU_REQUIRE_ERROR_COUNT(0, result2);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "any_annotation_breaks_cycle")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {hello = B.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A) :: any
|
|
return {hello = A.hello}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/A");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "nocheck_modules_are_typed")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
--!nocheck
|
|
export type Foo = number
|
|
return {hello = "hi"}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
--!nonstrict
|
|
export type Foo = number
|
|
return {hello = "hi"}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/C"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
local B = require(Modules.B)
|
|
local five : A.Foo = 5
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/C");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
ModulePtr aModule = frontend.moduleResolver.modules["game/Gui/Modules/A"];
|
|
REQUIRE(bool(aModule));
|
|
|
|
std::optional<TypeId> aExports = first(aModule->returnType);
|
|
REQUIRE(bool(aExports));
|
|
|
|
ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"];
|
|
REQUIRE(bool(bModule));
|
|
|
|
std::optional<TypeId> bExports = first(bModule->returnType);
|
|
REQUIRE(bool(bExports));
|
|
|
|
CHECK_EQ(toString(*aExports), toString(*bExports));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_between_check_and_nocheck")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {hello = B.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
--!nocheck
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {hello = A.hello}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/A");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
--!nocheck
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {hello = B.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
--!nocheck
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {hello = A.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/C"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
local B = require(Modules.B)
|
|
return {a=A, b=B}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/C");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
ModulePtr cModule = frontend.moduleResolver.modules["game/Gui/Modules/C"];
|
|
REQUIRE(bool(cModule));
|
|
|
|
std::optional<TypeId> cExports = first(cModule->returnType);
|
|
REQUIRE(bool(cExports));
|
|
CHECK_EQ("{| a: any, b: any |}", toString(*cExports));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_disabled_in_nocheck")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
--!nocheck
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {hello = B.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
--!nocheck
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {hello = A.hello}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/A");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "cycle_errors_can_be_fixed")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {hello = B.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {hello = A.hello}
|
|
)";
|
|
|
|
CheckResult result1 = frontend.check("game/Gui/Modules/A");
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result1);
|
|
|
|
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[0]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[0]));
|
|
|
|
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[1]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[1]));
|
|
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
return {hello = 42}
|
|
)";
|
|
frontend.markDirty("game/Gui/Modules/B");
|
|
|
|
CheckResult result2 = frontend.check("game/Gui/Modules/A");
|
|
LUAU_REQUIRE_NO_ERRORS(result2);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "cycle_error_paths")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {hello = B.hello}
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {hello = A.hello}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/A");
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
auto ce1 = get<ModuleHasCyclicDependency>(result.errors[0]);
|
|
REQUIRE(ce1);
|
|
CHECK_EQ(result.errors[0].moduleName, "game/Gui/Modules/B");
|
|
REQUIRE_EQ(ce1->cycle.size(), 2);
|
|
CHECK_EQ(ce1->cycle[0], "game/Gui/Modules/A");
|
|
CHECK_EQ(ce1->cycle[1], "game/Gui/Modules/B");
|
|
|
|
auto ce2 = get<ModuleHasCyclicDependency>(result.errors[1]);
|
|
REQUIRE(ce2);
|
|
CHECK_EQ(result.errors[1].moduleName, "game/Gui/Modules/A");
|
|
REQUIRE_EQ(ce2->cycle.size(), 2);
|
|
CHECK_EQ(ce2->cycle[0], "game/Gui/Modules/B");
|
|
CHECK_EQ(ce2->cycle[1], "game/Gui/Modules/A");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
return {hello = 2}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/A");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
local me = require(game.A)
|
|
return {hello = 2}
|
|
)";
|
|
frontend.markDirty("game/A");
|
|
|
|
result = frontend.check("game/A");
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
auto ty = requireType("game/A", "me");
|
|
CHECK_EQ(toString(ty), "any");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_longer")
|
|
{
|
|
fileResolver.source["game/A"] = R"(
|
|
return {mod_a = 2}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/A");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
local me = require(game.A)
|
|
return {mod_b = 4}
|
|
)";
|
|
|
|
result = frontend.check("game/B");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
local me = require(game.B)
|
|
return {mod_a_prime = 3}
|
|
)";
|
|
|
|
frontend.markDirty("game/A");
|
|
frontend.markDirty("game/B");
|
|
|
|
result = frontend.check("game/A");
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
TypeId tyA = requireType("game/A", "me");
|
|
CHECK_EQ(toString(tyA), "any");
|
|
|
|
result = frontend.check("game/B");
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
TypeId tyB = requireType("game/B", "me");
|
|
CHECK_EQ(toString(tyB), "any");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "dont_reparse_clean_file_when_linting")
|
|
{
|
|
fileResolver.source["Modules/A"] = R"(
|
|
local t = {}
|
|
|
|
for i=#t,1 do
|
|
end
|
|
|
|
for i=#t,1,-1 do
|
|
end
|
|
)";
|
|
|
|
frontend.check("Modules/A");
|
|
|
|
fileResolver.source["Modules/A"] = R"(
|
|
-- We have fixed the lint error, but we did not tell the Frontend that the file is changed!
|
|
-- Therefore, we expect Frontend to reuse the parse tree.
|
|
)";
|
|
|
|
configResolver.defaultConfig.enabledLint.enableWarning(LintWarning::Code_ForRange);
|
|
|
|
LintResult lintResult = frontend.lint("Modules/A");
|
|
|
|
CHECK_EQ(1, lintResult.warnings.size());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "dont_recheck_script_that_hasnt_been_marked_dirty")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {b_value = A.hello}
|
|
)";
|
|
|
|
frontend.check("game/Gui/Modules/B");
|
|
|
|
fileResolver.source["game/Gui/Modules/A"] =
|
|
"Massively incorrect syntax haha oops! However! The frontend doesn't know that this file needs reparsing!";
|
|
|
|
frontend.check("game/Gui/Modules/B");
|
|
|
|
ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"];
|
|
CHECK(bModule->errors.empty());
|
|
Luau::dumpErrors(bModule);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local A = require(Modules.A)
|
|
return {b_value = A.hello}
|
|
)";
|
|
|
|
frontend.check("game/Gui/Modules/B");
|
|
|
|
fileResolver.source["game/Gui/Modules/A"] = "return {hello='hi!'}";
|
|
frontend.markDirty("game/Gui/Modules/A");
|
|
|
|
frontend.check("game/Gui/Modules/B");
|
|
|
|
ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"];
|
|
CHECK(bModule->errors.empty());
|
|
Luau::dumpErrors(bModule);
|
|
|
|
auto bExports = first(bModule->returnType);
|
|
REQUIRE(!!bExports);
|
|
|
|
CHECK_EQ("{| b_value: string |}", toString(*bExports));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "mark_non_immediate_reverse_deps_as_dirty")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
return require(game:GetService('Gui').Modules.A)
|
|
)";
|
|
fileResolver.source["game/Gui/Modules/C"] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return {c_value = B.hello}
|
|
)";
|
|
|
|
frontend.check("game/Gui/Modules/C");
|
|
|
|
std::vector<Luau::ModuleName> markedDirty;
|
|
frontend.markDirty("game/Gui/Modules/A", &markedDirty);
|
|
|
|
REQUIRE(markedDirty.size() == 3);
|
|
CHECK(std::find(markedDirty.begin(), markedDirty.end(), "game/Gui/Modules/A") != markedDirty.end());
|
|
CHECK(std::find(markedDirty.begin(), markedDirty.end(), "game/Gui/Modules/B") != markedDirty.end());
|
|
CHECK(std::find(markedDirty.begin(), markedDirty.end(), "game/Gui/Modules/C") != markedDirty.end());
|
|
}
|
|
|
|
#if 0
|
|
// Does not work yet. :(
|
|
TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_has_a_parse_error")
|
|
{
|
|
fileResolver.source["Modules/A"] = "oh no a syntax error";
|
|
fileResolver.source["Modules/B"] = R"(
|
|
local Modules = {}
|
|
local A = require(Modules.A)
|
|
return {}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Modules/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Modules/A", result.errors[0].moduleName);
|
|
|
|
CheckResult result2 = frontend.check("Modules/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result2);
|
|
CHECK_EQ(result2.errors[0], result.errors[0]);
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "produce_errors_for_unchanged_file_with_a_syntax_error")
|
|
{
|
|
fileResolver.source["Modules/A"] = "oh no a blatant syntax error!!";
|
|
|
|
CheckResult one = frontend.check("Modules/A");
|
|
CheckResult two = frontend.check("Modules/A");
|
|
|
|
CHECK(!one.errors.empty());
|
|
CHECK(!two.errors.empty());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "produce_errors_for_unchanged_file_with_errors")
|
|
{
|
|
fileResolver.source["Modules/A"] = "local p: number = 'oh no a type error'";
|
|
|
|
frontend.check("Modules/A");
|
|
|
|
fileResolver.source["Modules/A"] = "local p = 4 -- We have fixed the problem, but we didn't tell the frontend, so it will not recheck this file!";
|
|
CheckResult secondResult = frontend.check("Modules/A");
|
|
|
|
CHECK_EQ(1, secondResult.errors.size());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "reports_errors_from_multiple_sources")
|
|
{
|
|
fileResolver.source["game/Gui/Modules/A"] = R"(
|
|
local a: number = 'oh no a type error'
|
|
return {a=a}
|
|
)";
|
|
|
|
fileResolver.source["game/Gui/Modules/B"] = R"(
|
|
local Modules = script.Parent
|
|
local A = require(Modules.A)
|
|
local b: number = 'another one! This is quite distressing!'
|
|
)";
|
|
|
|
CheckResult result = frontend.check("game/Gui/Modules/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
CHECK_EQ("game/Gui/Modules/A", result.errors[0].moduleName);
|
|
CHECK_EQ("game/Gui/Modules/B", result.errors[1].moduleName);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "report_require_to_nonexistent_file")
|
|
{
|
|
fileResolver.source["Modules/A"] = R"(
|
|
local Modules = script
|
|
local B = require(Modules.B)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Modules/A");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
std::string s = toString(result.errors[0]);
|
|
CHECK_MESSAGE(get<UnknownRequire>(result.errors[0]), "Should have been an UnknownRequire: " << toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "ignore_require_to_nonexistent_file")
|
|
{
|
|
fileResolver.source["Modules/A"] = R"(
|
|
local Modules = script
|
|
local B = require(Modules.B) :: any
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Modules/A");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "report_syntax_error_in_required_file")
|
|
{
|
|
fileResolver.source["Modules/A"] = "oh no a gross breach of syntax";
|
|
fileResolver.source["Modules/B"] = R"(
|
|
local Modules = script.Parent
|
|
local A = require(Modules.A)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Modules/B");
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
CHECK_EQ("Modules/A", result.errors[0].moduleName);
|
|
|
|
bool b = std::any_of(begin(result.errors), end(result.errors), [](auto&& e) -> bool {
|
|
return get<SyntaxError>(e);
|
|
});
|
|
if (!b)
|
|
{
|
|
CHECK_MESSAGE(false, "Expected a syntax error!");
|
|
dumpErrors(result);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "re_report_type_error_in_required_file")
|
|
{
|
|
fileResolver.source["Modules/A"] = R"(
|
|
local n: number = 'five'
|
|
return {n=n}
|
|
)";
|
|
|
|
fileResolver.source["Modules/B"] = R"(
|
|
local Modules = script.Parent
|
|
local A = require(Modules.A)
|
|
print(A.n)
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Modules/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CheckResult result2 = frontend.check("Modules/B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result2);
|
|
|
|
CHECK_EQ("Modules/A", result.errors[0].moduleName);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "accumulate_cached_errors")
|
|
{
|
|
fileResolver.source["Modules/A"] = R"(
|
|
local n: number = 'five'
|
|
return {n=n}
|
|
)";
|
|
|
|
fileResolver.source["Modules/B"] = R"(
|
|
local Modules = script.Parent
|
|
local A = require(Modules.A)
|
|
local b: number = 'seven'
|
|
print(A, b)
|
|
)";
|
|
|
|
CheckResult result1 = frontend.check("Modules/B");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result1);
|
|
|
|
CHECK_EQ("Modules/A", result1.errors[0].moduleName);
|
|
CHECK_EQ("Modules/B", result1.errors[1].moduleName);
|
|
|
|
CheckResult result2 = frontend.check("Modules/B");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result2);
|
|
|
|
CHECK_EQ("Modules/A", result2.errors[0].moduleName);
|
|
CHECK_EQ("Modules/B", result2.errors[1].moduleName);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "accumulate_cached_errors_in_consistent_order")
|
|
{
|
|
fileResolver.source["Modules/A"] = R"(
|
|
a = 1
|
|
b = 2
|
|
local Modules = script.Parent
|
|
local A = require(Modules.B)
|
|
)";
|
|
|
|
fileResolver.source["Modules/B"] = R"(
|
|
d = 3
|
|
e = 4
|
|
return {}
|
|
)";
|
|
|
|
CheckResult result1 = frontend.check("Modules/A");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(4, result1);
|
|
|
|
CHECK_EQ("Modules/A", result1.errors[2].moduleName);
|
|
CHECK_EQ("Modules/A", result1.errors[3].moduleName);
|
|
|
|
CHECK_EQ("Modules/B", result1.errors[0].moduleName);
|
|
CHECK_EQ("Modules/B", result1.errors[1].moduleName);
|
|
|
|
CheckResult result2 = frontend.check("Modules/A");
|
|
CHECK_EQ(4, result2.errors.size());
|
|
|
|
for (size_t i = 0; i < result1.errors.size(); ++i)
|
|
CHECK_EQ(result1.errors[i], result2.errors[i]);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "test_pruneParentSegments")
|
|
{
|
|
CHECK_EQ(std::optional<std::string>{"Modules/Enum/ButtonState"},
|
|
pathExprToModuleName("", {"Modules", "LuaApp", "DeprecatedDarkTheme", "Parent", "Parent", "Enum", "ButtonState"}));
|
|
CHECK_EQ(std::optional<std::string>{"workspace/Foo/Bar/Baz"}, pathExprToModuleName("workspace/Foo/Quux", {"script", "Parent", "Bar", "Baz"}));
|
|
CHECK_EQ(std::nullopt, pathExprToModuleName("", {}));
|
|
CHECK_EQ(std::optional<std::string>{"script"}, pathExprToModuleName("", {"script"}));
|
|
CHECK_EQ(std::optional<std::string>{"script/Parent"}, pathExprToModuleName("", {"script", "Parent"}));
|
|
CHECK_EQ(std::optional<std::string>{"script"}, pathExprToModuleName("", {"script", "Parent", "Parent"}));
|
|
CHECK_EQ(std::optional<std::string>{"script"}, pathExprToModuleName("", {"script", "Test", "Parent"}));
|
|
CHECK_EQ(std::optional<std::string>{"script/Parent"}, pathExprToModuleName("", {"script", "Test", "Parent", "Parent"}));
|
|
CHECK_EQ(std::optional<std::string>{"script/Parent"}, pathExprToModuleName("", {"script", "Test", "Parent", "Test", "Parent", "Parent"}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "test_lint_uses_correct_config")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
local t = {}
|
|
|
|
for i=#t,1 do
|
|
end
|
|
)";
|
|
|
|
configResolver.configFiles["Module/A"].enabledLint.enableWarning(LintWarning::Code_ForRange);
|
|
|
|
auto result = frontend.lint("Module/A");
|
|
CHECK_EQ(1, result.warnings.size());
|
|
|
|
configResolver.configFiles["Module/A"].enabledLint.disableWarning(LintWarning::Code_ForRange);
|
|
|
|
auto result2 = frontend.lint("Module/A");
|
|
CHECK_EQ(0, result2.warnings.size());
|
|
|
|
LintOptions overrideOptions;
|
|
|
|
overrideOptions.enableWarning(LintWarning::Code_ForRange);
|
|
auto result3 = frontend.lint("Module/A", overrideOptions);
|
|
CHECK_EQ(1, result3.warnings.size());
|
|
|
|
overrideOptions.disableWarning(LintWarning::Code_ForRange);
|
|
auto result4 = frontend.lint("Module/A", overrideOptions);
|
|
CHECK_EQ(0, result4.warnings.size());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "discard_type_graphs")
|
|
{
|
|
Frontend fe{&fileResolver, &configResolver, {false}};
|
|
|
|
fileResolver.source["Module/A"] = R"(
|
|
local a = {1,2,3,4,5}
|
|
)";
|
|
|
|
CheckResult result = fe.check("Module/A");
|
|
|
|
ModulePtr module = fe.moduleResolver.getModule("Module/A");
|
|
|
|
CHECK_EQ(0, module->internalTypes.types.size());
|
|
CHECK_EQ(0, module->internalTypes.typePacks.size());
|
|
CHECK_EQ(0, module->astTypes.size());
|
|
CHECK_EQ(0, module->astResolvedTypes.size());
|
|
CHECK_EQ(0, module->astResolvedTypePacks.size());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded")
|
|
{
|
|
Frontend fe{&fileResolver, &configResolver, {false}};
|
|
|
|
fileResolver.source["Module/A"] = R"(
|
|
--!strict
|
|
local a: {Count: number} = {count='five'}
|
|
)";
|
|
|
|
CheckResult result = fe.check("Module/A");
|
|
|
|
REQUIRE_EQ(1, result.errors.size());
|
|
|
|
// When this test fails, it is because the TypeIds needed by the error have been deallocated.
|
|
// It is thus basically impossible to predict what will happen when this assert is evaluated.
|
|
// It could segfault, or you could see weird type names like the empty string or <VALUELESS BY EXCEPTION>
|
|
REQUIRE_EQ(
|
|
"Table type 'a' not compatible with type '{| Count: number |}' because the former is missing field 'Count'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "trace_requires_in_nonstrict_mode")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
--!nonstrict
|
|
local module = {}
|
|
|
|
function module.f(arg: number)
|
|
print('f', arg)
|
|
end
|
|
|
|
return module
|
|
)";
|
|
|
|
fileResolver.source["Module/B"] = R"(
|
|
--!nonstrict
|
|
local A = require(script.Parent.A)
|
|
|
|
print(A.g(5)) -- Key 'g' not found
|
|
print(A.f('five')) -- Type mismatch number and string
|
|
print(A.f(5)) -- OK
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Module/B");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
CHECK_EQ(4, result.errors[0].location.begin.line);
|
|
CHECK_EQ(5, result.errors[1].location.begin.line);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "environments")
|
|
{
|
|
ScopePtr testScope = frontend.addEnvironment("test");
|
|
|
|
unfreeze(typeChecker.globalTypes);
|
|
loadDefinitionFile(typeChecker, testScope, R"(
|
|
export type Foo = number | string
|
|
)",
|
|
"@test");
|
|
freeze(typeChecker.globalTypes);
|
|
|
|
fileResolver.source["A"] = R"(
|
|
--!nonstrict
|
|
local foo: Foo = 1
|
|
)";
|
|
|
|
fileResolver.source["B"] = R"(
|
|
--!nonstrict
|
|
local foo: Foo = 1
|
|
)";
|
|
|
|
fileResolver.environments["A"] = "test";
|
|
|
|
CheckResult resultA = frontend.check("A");
|
|
LUAU_REQUIRE_NO_ERRORS(resultA);
|
|
|
|
CheckResult resultB = frontend.check("B");
|
|
LUAU_REQUIRE_ERROR_COUNT(1, resultB);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "ast_node_at_position")
|
|
{
|
|
check(R"(
|
|
local t = {}
|
|
|
|
function t:aa() end
|
|
|
|
t:
|
|
)");
|
|
|
|
SourceModule* module = getMainSourceModule();
|
|
Position pos = module->root->location.end;
|
|
AstNode* node = findNodeAtPosition(*module, pos);
|
|
|
|
REQUIRE(node);
|
|
REQUIRE(bool(node->asExpr()));
|
|
|
|
++pos.column;
|
|
AstNode* node2 = findNodeAtPosition(*module, pos);
|
|
CHECK_EQ(node, node2);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "stats_are_not_reset_between_checks")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
--!strict
|
|
local B = require(script.Parent.B)
|
|
local foo = B.foo + 1
|
|
)";
|
|
|
|
fileResolver.source["Module/B"] = R"(
|
|
--!strict
|
|
return {foo = 1}
|
|
)";
|
|
|
|
CheckResult r1 = frontend.check("Module/A");
|
|
LUAU_REQUIRE_NO_ERRORS(r1);
|
|
|
|
Frontend::Stats stats1 = frontend.stats;
|
|
CHECK_EQ(2, stats1.files);
|
|
|
|
frontend.markDirty("Module/A");
|
|
frontend.markDirty("Module/B");
|
|
|
|
CheckResult r2 = frontend.check("Module/A");
|
|
LUAU_REQUIRE_NO_ERRORS(r2);
|
|
Frontend::Stats stats2 = frontend.stats;
|
|
|
|
CHECK_EQ(4, stats2.files);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "clearStats")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
--!strict
|
|
local B = require(script.Parent.B)
|
|
local foo = B.foo + 1
|
|
)";
|
|
|
|
fileResolver.source["Module/B"] = R"(
|
|
--!strict
|
|
return {foo = 1}
|
|
)";
|
|
|
|
CheckResult r1 = frontend.check("Module/A");
|
|
LUAU_REQUIRE_NO_ERRORS(r1);
|
|
|
|
Frontend::Stats stats1 = frontend.stats;
|
|
CHECK_EQ(2, stats1.files);
|
|
|
|
frontend.markDirty("Module/A");
|
|
frontend.markDirty("Module/B");
|
|
|
|
frontend.clearStats();
|
|
CheckResult r2 = frontend.check("Module/A");
|
|
LUAU_REQUIRE_NO_ERRORS(r2);
|
|
Frontend::Stats stats2 = frontend.stats;
|
|
|
|
CHECK_EQ(2, stats2.files);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "typecheck_twice_for_ast_types")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
local a = 1
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Module/A");
|
|
|
|
ModulePtr module = frontend.moduleResolver.getModule("Module/A");
|
|
|
|
REQUIRE_EQ(module->astTypes.size(), 1);
|
|
auto it = module->astTypes.begin();
|
|
CHECK_EQ(toString(it->second), "number");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FrontendFixture, "imported_table_modification_2")
|
|
{
|
|
frontend.options.retainFullTypeGraphs = false;
|
|
|
|
fileResolver.source["Module/A"] = R"(
|
|
--!nonstrict
|
|
local a = {}
|
|
a.x = 1
|
|
return a;
|
|
)";
|
|
|
|
fileResolver.source["Module/B"] = R"(
|
|
--!nonstrict
|
|
local a = require(script.Parent.A)
|
|
local b = {}
|
|
function a:b() end -- this should error, since A doesn't define a:b()
|
|
return b
|
|
)";
|
|
|
|
fileResolver.source["Module/C"] = R"(
|
|
--!nonstrict
|
|
local a = require(script.Parent.A)
|
|
local b = require(script.Parent.B)
|
|
a:b() -- this should error, since A doesn't define a:b()
|
|
)";
|
|
|
|
CheckResult resultA = frontend.check("Module/A");
|
|
LUAU_REQUIRE_NO_ERRORS(resultA);
|
|
|
|
CheckResult resultB = frontend.check("Module/B");
|
|
LUAU_REQUIRE_ERRORS(resultB);
|
|
|
|
CheckResult resultC = frontend.check("Module/C");
|
|
LUAU_REQUIRE_ERRORS(resultC);
|
|
}
|
|
|
|
// This test does not use TEST_CASE_FIXTURE because we need to set a flag before
|
|
// the fixture is constructed.
|
|
TEST_CASE("no_use_after_free_with_type_fun_instantiation")
|
|
{
|
|
// This flag forces this test to crash if there's a UAF in this code.
|
|
ScopedFastFlag sff_DebugLuauFreezeArena("DebugLuauFreezeArena", true);
|
|
|
|
FrontendFixture fix;
|
|
|
|
fix.fileResolver.source["Module/A"] = R"(
|
|
export type Foo<V> = typeof(setmetatable({}, {}))
|
|
return false;
|
|
)";
|
|
|
|
fix.fileResolver.source["Module/B"] = R"(
|
|
local A = require(script.Parent.A)
|
|
export type Foo<V> = A.Foo<V>
|
|
return false;
|
|
)";
|
|
|
|
// We don't care about the result. That we haven't crashed is enough.
|
|
fix.frontend.check("Module/B");
|
|
}
|
|
|
|
TEST_CASE("check_without_builtin_next")
|
|
{
|
|
TestFileResolver fileResolver;
|
|
TestConfigResolver configResolver;
|
|
Frontend frontend(&fileResolver, &configResolver);
|
|
|
|
fileResolver.source["Module/A"] = "for k,v in 2 do end";
|
|
fileResolver.source["Module/B"] = "return next";
|
|
|
|
// We don't care about the result. That we haven't crashed is enough.
|
|
frontend.check("Module/A");
|
|
frontend.check("Module/B");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "reexport_cyclic_type")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
type F<T> = (set: G<T>) -> ()
|
|
|
|
export type G<T> = {
|
|
forEach: (a: F<T>) -> (),
|
|
}
|
|
|
|
function X<T>(a: F<T>): ()
|
|
end
|
|
|
|
return X
|
|
)";
|
|
|
|
fileResolver.source["Module/B"] = R"(
|
|
--!strict
|
|
local A = require(script.Parent.A)
|
|
|
|
export type G<T> = A.G<T>
|
|
|
|
return {
|
|
A = A,
|
|
}
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Module/B");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "reexport_type_alias")
|
|
{
|
|
fileResolver.source["Module/A"] = R"(
|
|
type KeyOfTestEvents = "test-file-start" | "test-file-success" | "test-file-failure" | "test-case-result"
|
|
type MyAny = any
|
|
|
|
export type TestFileEvent<T = KeyOfTestEvents> = (
|
|
eventName: T,
|
|
args: any --[[ ROBLOX TODO: Unhandled node for type: TSIndexedAccessType ]] --[[ TestEvents[T] ]]
|
|
) -> MyAny
|
|
|
|
return {}
|
|
)";
|
|
|
|
fileResolver.source["Module/B"] = R"(
|
|
--!strict
|
|
local A = require(script.Parent.A)
|
|
|
|
export type TestFileEvent = A.TestFileEvent
|
|
)";
|
|
|
|
CheckResult result = frontend.check("Module/B");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_SUITE_END();
|