luau/tests/Frontend.test.cpp
Aviral Goel ee1c6bf0db
Sync to upstream/release/668 (#1760)
## New Type Solver

1. Update resolved types for singleton unions and intersections to avoid
crashing when type checking type assertions.
2. Generalize free return type pack of a function type inferred at call
site to ensure that the free type does not leak to another module.
3. Fix crash from cyclic indexers by reducing if possible or producing
an error otherwise.
4. Fix handling of irreducible type functions to prevent type inference
from failing.
5. Fix handling of recursive metatables to avoid infinite recursion.

## New and Old Type Solver

Fix accidental capture of all exceptions in multi-threaded typechecking
by converting all typechecking exceptions to `InternalCompilerError` and
only capturing those.

## Fragment Autocomplete

1. Add a block based diff algorithm based on class index and span for
re-typechecking. This reduces the granularity of fragment autocomplete
to avoid flakiness when the fragment does not have enough type
information.
2. Fix bugs arising from incorrect scope selection for autocompletion.

## Roundtrippable AST

Store type alias location in `TypeFun` class to ensure it is accessible
for exported types as part of the public interface.

## Build System

1. Bump minimum supported CMake version to 3.10 since GitHub is phasing
out the currently supported minimum version 3.0, released 11 years ago.
2. Fix compilation when `HARDSTACKTESTS` is enabled.

## Miscellaneous

Flag removals and cleanup of unused code.

## Internal Contributors

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: Talha Pathan <tpathan@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

## External Contributors

Thanks to [@grh-official](https://github.com/grh-official) for PR #1759 

**Full Changelog**:
https://github.com/luau-lang/luau/compare/0.667...0.668

---------

Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com>
Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com>
Co-authored-by: Menarul Alam <malam@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
2025-04-04 14:11:51 -07:00

1886 lines
57 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/DenseHash.h"
#include "Luau/Frontend.h"
#include "Luau/RequireTracer.h"
#include "Fixture.h"
#include "doctest.h"
#include <algorithm>
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
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.globals, "game", builtinTypes->anyType, "@test");
addGlobalBinding(frontend.globals, "script", builtinTypes->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.getModule("game/Gui/Modules/B");
REQUIRE(bModule != nullptr);
CHECK(bModule->errors.empty());
Luau::dumpErrors(bModule);
auto bExports = first(bModule->returnType);
REQUIRE(!!bExports);
if (FFlag::LuauSolverV2)
CHECK_EQ("{ b_value: number }", toString(*bExports));
else
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.getModule("game/Gui/Modules/A");
REQUIRE(bool(aModule));
std::optional<TypeId> aExports = first(aModule->returnType);
REQUIRE(bool(aExports));
ModulePtr bModule = frontend.moduleResolver.getModule("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"(
--!strict
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.getModule("game/Gui/Modules/C");
REQUIRE(bool(cModule));
std::optional<TypeId> cExports = first(cModule->returnType);
REQUIRE(bool(cExports));
if (FFlag::LuauSolverV2)
CHECK("{ a: { hello: any }, b: { hello: any } }" == toString(*cExports));
else
CHECK("{| a: {| hello: any |}, b: {| hello: 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, "cycle_incremental_type_surface_exports")
{
fileResolver.source["game/A"] = R"(
local b = require(game.B)
export type atype = { x: b.btype }
return {mod_a = 1}
)";
fileResolver.source["game/B"] = R"(
export type btype = { x: number }
local function bf()
local a = require(game.A)
local bfl : a.atype = nil
return {bfl.x}
end
return {mod_b = 2}
)";
ToStringOptions opts;
opts.exhaustive = true;
CheckResult resultA = frontend.check("game/A");
LUAU_REQUIRE_ERRORS(resultA);
CheckResult resultB = frontend.check("game/B");
LUAU_REQUIRE_ERRORS(resultB);
TypeId tyB = requireExportedType("game/B", "btype");
if (FFlag::LuauSolverV2)
CHECK_EQ(toString(tyB, opts), "{ x: number }");
else
CHECK_EQ(toString(tyB, opts), "{| x: number |}");
TypeId tyA = requireExportedType("game/A", "atype");
if (FFlag::LuauSolverV2)
CHECK_EQ(toString(tyA, opts), "{ x: any }");
else
CHECK_EQ(toString(tyA, opts), "{| x: any |}");
frontend.markDirty("game/B");
resultB = frontend.check("game/B");
LUAU_REQUIRE_ERRORS(resultB);
tyB = requireExportedType("game/B", "btype");
if (FFlag::LuauSolverV2)
CHECK_EQ(toString(tyB, opts), "{ x: number }");
else
CHECK_EQ(toString(tyB, opts), "{| x: number |}");
tyA = requireExportedType("game/A", "atype");
if (FFlag::LuauSolverV2)
CHECK_EQ(toString(tyA, opts), "{ x: any }");
else
CHECK_EQ(toString(tyA, opts), "{| x: 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
)";
configResolver.defaultConfig.enabledLint.enableWarning(LintWarning::Code_ForRange);
lintModule("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 results from previous lint.
)";
LintResult lintResult = lintModule("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.getModule("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.getModule("game/Gui/Modules/B");
CHECK(bModule->errors.empty());
Luau::dumpErrors(bModule);
auto bExports = first(bModule->returnType);
REQUIRE(!!bExports);
if (FFlag::LuauSolverV2)
CHECK_EQ("{ b_value: string }", toString(*bExports));
else
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 = lintModule("Module/A");
CHECK_EQ(1, result.warnings.size());
configResolver.configFiles["Module/A"].enabledLint.disableWarning(LintWarning::Code_ForRange);
frontend.markDirty("Module/A");
auto result2 = lintModule("Module/A");
CHECK_EQ(0, result2.warnings.size());
LintOptions overrideOptions;
overrideOptions.enableWarning(LintWarning::Code_ForRange);
frontend.markDirty("Module/A");
auto result3 = lintModule("Module/A", overrideOptions);
CHECK_EQ(1, result3.warnings.size());
overrideOptions.disableWarning(LintWarning::Code_ForRange);
frontend.markDirty("Module/A");
auto result4 = lintModule("Module/A", overrideOptions);
CHECK_EQ(0, result4.warnings.size());
}
TEST_CASE_FIXTURE(FrontendFixture, "lint_results_are_only_for_checked_module")
{
fileResolver.source["Module/A"] = R"(
local _ = 0b10000000000000000000000000000000000000000000000000000000000000000
)";
fileResolver.source["Module/B"] = R"(
require(script.Parent.A)
local _ = 0x10000000000000000
)";
LintResult lintResult = lintModule("Module/B");
CHECK_EQ(1, lintResult.warnings.size());
// Check cached result
lintResult = lintModule("Module/B");
CHECK_EQ(1, lintResult.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>
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
REQUIRE_EQ(
"Type\n\t"
"'{ count: string }'"
"\ncould not be converted into\n\t"
"'{ Count: number }'",
toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
REQUIRE_EQ(
R"(Type
'{ count: string }'
could not be converted into
'{ Count: number }')",
toString(result.errors[0])
);
else
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")
{
// The new non-strict mode is not currently expected to signal any errors here.
if (FFlag::LuauSolverV2)
return;
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(frontend.globals.globalTypes);
frontend.loadDefinitionFile(
frontend.globals,
testScope,
R"(
export type Foo = number | string
)",
"@test",
/* captureComments */ false
);
freeze(frontend.globals.globalTypes);
fileResolver.source["A"] = R"(
--!nonstrict
local foo: Foo = 1
)";
fileResolver.source["B"] = R"(
--!nonstrict
local foo: Foo = 1
)";
fileResolver.source["C"] = R"(
--!strict
local foo: Foo = 1
)";
fileResolver.environments["A"] = "test";
CheckResult resultA = frontend.check("A");
LUAU_REQUIRE_NO_ERRORS(resultA);
CheckResult resultB = frontend.check("B");
// In the new non-strict mode, we do not currently support error reporting for unknown symbols in type positions.
if (FFlag::LuauSolverV2)
LUAU_REQUIRE_NO_ERRORS(resultB);
else
LUAU_REQUIRE_ERROR_COUNT(1, resultB);
CheckResult resultC = frontend.check("C");
LUAU_REQUIRE_ERROR_COUNT(1, resultC);
}
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")
{
// This test describes non-strict mode behavior that is just not currently present in the new non-strict mode.
if (FFlag::LuauSolverV2)
return;
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(FFlag::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_CASE_FIXTURE(BuiltinsFixture, "module_scope_check")
{
frontend.prepareModuleScope = [this](const ModuleName& name, const ScopePtr& scope, bool forAutocomplete)
{
scope->bindings[Luau::AstName{"x"}] = Luau::Binding{frontend.globals.builtinTypes->numberType};
};
fileResolver.source["game/A"] = R"(
local a = x
)";
CheckResult result = frontend.check("game/A");
LUAU_REQUIRE_NO_ERRORS(result);
auto ty = requireType("game/A", "a");
CHECK_EQ(toString(ty), "number");
}
TEST_CASE_FIXTURE(FrontendFixture, "parse_only")
{
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 = 2
)";
frontend.parse("game/Gui/Modules/B");
REQUIRE(frontend.sourceNodes.count("game/Gui/Modules/A"));
REQUIRE(frontend.sourceNodes.count("game/Gui/Modules/B"));
auto node = frontend.sourceNodes["game/Gui/Modules/B"];
CHECK(node->requireSet.contains("game/Gui/Modules/A"));
REQUIRE_EQ(node->requireLocations.size(), 1);
CHECK_EQ(node->requireLocations[0].second, Luau::Location(Position(2, 18), Position(2, 36)));
// Early parse doesn't cause typechecking to be skipped
CheckResult result = frontend.check("game/Gui/Modules/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("game/Gui/Modules/A", result.errors[0].moduleName);
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(FrontendFixture, "markdirty_early_return")
{
constexpr char moduleName[] = "game/Gui/Modules/A";
fileResolver.source[moduleName] = R"(
return 1
)";
{
std::vector<ModuleName> markedDirty;
frontend.markDirty(moduleName, &markedDirty);
CHECK(markedDirty.empty());
}
frontend.parse(moduleName);
{
std::vector<ModuleName> markedDirty;
frontend.markDirty(moduleName, &markedDirty);
CHECK(!markedDirty.empty());
}
}
TEST_CASE_FIXTURE(FrontendFixture, "attribute_ices_to_the_correct_module")
{
ScopedFastFlag sff{FFlag::DebugLuauMagicTypes, true};
fileResolver.source["game/one"] = R"(
require(game.two)
)";
fileResolver.source["game/two"] = R"(
local a: _luau_ice
)";
try
{
frontend.check("game/one");
}
catch (InternalCompilerError& err)
{
CHECK("game/two" == err.moduleName);
return;
}
FAIL("Expected an InternalCompilerError!");
}
TEST_CASE_FIXTURE(FrontendFixture, "checked_modules_have_the_correct_mode")
{
fileResolver.source["game/A"] = R"(
--!nocheck
local a: number = "five"
)";
fileResolver.source["game/B"] = R"(
--!nonstrict
local a = math.abs("five")
)";
fileResolver.source["game/C"] = R"(
--!strict
local a = 10
)";
frontend.check("game/A");
frontend.check("game/B");
frontend.check("game/C");
ModulePtr moduleA = frontend.moduleResolver.getModule("game/A");
REQUIRE(moduleA);
CHECK(moduleA->mode == Mode::NoCheck);
ModulePtr moduleB = frontend.moduleResolver.getModule("game/B");
REQUIRE(moduleB);
CHECK(moduleB->mode == Mode::Nonstrict);
ModulePtr moduleC = frontend.moduleResolver.getModule("game/C");
REQUIRE(moduleC);
CHECK(moduleC->mode == Mode::Strict);
}
TEST_CASE_FIXTURE(FrontendFixture, "separate_caches_for_autocomplete")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
fileResolver.source["game/A"] = R"(
--!nonstrict
local exports = {}
function exports.hello() end
return exports
)";
FrontendOptions opts;
opts.forAutocomplete = true;
frontend.check("game/A", opts);
CHECK(nullptr == frontend.moduleResolver.getModule("game/A"));
ModulePtr acModule = frontend.moduleResolverForAutocomplete.getModule("game/A");
REQUIRE(acModule != nullptr);
CHECK(acModule->mode == Mode::Strict);
frontend.check("game/A");
ModulePtr module = frontend.moduleResolver.getModule("game/A");
REQUIRE(module != nullptr);
CHECK(module->mode == Mode::Nonstrict);
}
TEST_CASE_FIXTURE(FrontendFixture, "no_separate_caches_with_the_new_solver")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
fileResolver.source["game/A"] = R"(
--!nonstrict
local exports = {}
function exports.hello() end
return exports
)";
FrontendOptions opts;
opts.forAutocomplete = true;
frontend.check("game/A", opts);
CHECK(nullptr == frontend.moduleResolverForAutocomplete.getModule("game/A"));
ModulePtr module = frontend.moduleResolver.getModule("game/A");
REQUIRE(module != nullptr);
CHECK(module->mode == Mode::Nonstrict);
}
TEST_CASE_FIXTURE(Fixture, "exported_tables_have_position_metadata")
{
CheckResult result = check(R"(
return { abc = 22 }
)");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr mm = getMainModule();
TypePackId retTp = mm->getModuleScope()->returnType;
auto retHead = flatten(retTp).first;
REQUIRE(1 == retHead.size());
const TableType* tt = get<TableType>(retHead[0]);
REQUIRE(tt);
CHECK("MainModule" == tt->definitionModuleName);
CHECK(1 == tt->props.size());
CHECK(tt->props.count("abc"));
const Property& prop = tt->props.find("abc")->second;
CHECK(Location{Position{1, 17}, Position{1, 20}} == prop.location);
}
TEST_CASE_FIXTURE(FrontendFixture, "get_required_scripts")
{
fileResolver.source["game/workspace/MyScript"] = R"(
local MyModuleScript = require(game.workspace.MyModuleScript)
local MyModuleScript2 = require(game.workspace.MyModuleScript2)
MyModuleScript.myPrint()
)";
fileResolver.source["game/workspace/MyModuleScript"] = R"(
local module = {}
function module.myPrint()
print("Hello World")
end
return module
)";
fileResolver.source["game/workspace/MyModuleScript2"] = R"(
local module = {}
return module
)";
// isDirty(name) is true, getRequiredScripts should not hit the cache.
frontend.markDirty("game/workspace/MyScript");
std::vector<ModuleName> requiredScripts = frontend.getRequiredScripts("game/workspace/MyScript");
REQUIRE(requiredScripts.size() == 2);
CHECK(requiredScripts[0] == "game/workspace/MyModuleScript");
CHECK(requiredScripts[1] == "game/workspace/MyModuleScript2");
// Call frontend.check first, then getRequiredScripts should hit the cache because isDirty(name) is false.
frontend.check("game/workspace/MyScript");
requiredScripts = frontend.getRequiredScripts("game/workspace/MyScript");
REQUIRE(requiredScripts.size() == 2);
CHECK(requiredScripts[0] == "game/workspace/MyModuleScript");
CHECK(requiredScripts[1] == "game/workspace/MyModuleScript2");
}
TEST_CASE_FIXTURE(FrontendFixture, "get_required_scripts_dirty")
{
fileResolver.source["game/workspace/MyScript"] = R"(
print("Hello World")
)";
fileResolver.source["game/workspace/MyModuleScript"] = R"(
local module = {}
function module.myPrint()
print("Hello World")
end
return module
)";
frontend.check("game/workspace/MyScript");
std::vector<ModuleName> requiredScripts = frontend.getRequiredScripts("game/workspace/MyScript");
REQUIRE(requiredScripts.size() == 0);
fileResolver.source["game/workspace/MyScript"] = R"(
local MyModuleScript = require(game.workspace.MyModuleScript)
MyModuleScript.myPrint()
)";
requiredScripts = frontend.getRequiredScripts("game/workspace/MyScript");
REQUIRE(requiredScripts.size() == 0);
frontend.markDirty("game/workspace/MyScript");
requiredScripts = frontend.getRequiredScripts("game/workspace/MyScript");
REQUIRE(requiredScripts.size() == 1);
CHECK(requiredScripts[0] == "game/workspace/MyModuleScript");
}
TEST_CASE_FIXTURE(FrontendFixture, "check_module_references_allocator")
{
fileResolver.source["game/workspace/MyScript"] = R"(
print("Hello World")
)";
frontend.check("game/workspace/MyScript");
ModulePtr module = frontend.moduleResolver.getModule("game/workspace/MyScript");
SourceModule* source = frontend.getSourceModule("game/workspace/MyScript");
CHECK(module);
CHECK(source);
CHECK_EQ(module->allocator.get(), source->allocator.get());
CHECK_EQ(module->names.get(), source->names.get());
}
TEST_CASE_FIXTURE(FrontendFixture, "check_module_references_correct_ast_root")
{
fileResolver.source["game/workspace/MyScript"] = R"(
print("Hello World")
)";
frontend.check("game/workspace/MyScript");
ModulePtr module = frontend.moduleResolver.getModule("game/workspace/MyScript");
SourceModule* source = frontend.getSourceModule("game/workspace/MyScript");
CHECK(module);
CHECK(source);
CHECK_EQ(module->root, source->root);
}
TEST_CASE_FIXTURE(FrontendFixture, "dfg_data_cleared_on_retain_type_graphs_unset")
{
ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauSelectivelyRetainDFGArena, true}};
fileResolver.source["game/A"] = R"(
local a = 1
local b = 2
local c = 3
return {x = a, y = b, z = c}
)";
frontend.options.retainFullTypeGraphs = true;
frontend.check("game/A");
auto mod = frontend.moduleResolver.getModule("game/A");
CHECK(!mod->defArena.allocator.empty());
CHECK(!mod->keyArena.allocator.empty());
// We should check that the dfg arena is empty once retainFullTypeGraphs is unset
frontend.options.retainFullTypeGraphs = false;
frontend.markDirty("game/A");
frontend.check("game/A");
mod = frontend.moduleResolver.getModule("game/A");
CHECK(mod->defArena.allocator.empty());
CHECK(mod->keyArena.allocator.empty());
}
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents")
{
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}
)";
fileResolver.source["game/Gui/Modules/D"] = R"(
local Modules = game:GetService('Gui').Modules
local C = require(Modules.C)
return {d_value = C.c_value}
)";
frontend.check("game/Gui/Modules/D");
std::vector<ModuleName> visited;
frontend.traverseDependents(
"game/Gui/Modules/B",
[&visited](SourceNode& node)
{
visited.push_back(node.name);
return true;
}
);
CHECK_EQ(std::vector<ModuleName>{"game/Gui/Modules/B", "game/Gui/Modules/C", "game/Gui/Modules/D"}, visited);
}
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit")
{
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<ModuleName> visited;
frontend.traverseDependents(
"game/Gui/Modules/A",
[&visited](SourceNode& node)
{
visited.push_back(node.name);
return node.name != "game/Gui/Modules/B";
}
);
CHECK_EQ(std::vector<ModuleName>{"game/Gui/Modules/A", "game/Gui/Modules/B"}, visited);
}
TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_updates")
{
auto updateSource = [&](const std::string& name, const std::string& source)
{
fileResolver.source[name] = source;
frontend.markDirty(name);
};
auto validateMatchesRequireLists = [&](const std::string& message)
{
DenseHashMap<ModuleName, std::vector<ModuleName>> dependents{{}};
for (const auto& module : frontend.sourceNodes)
{
for (const auto& dep : module.second->requireSet)
dependents[dep].push_back(module.first);
}
for (const auto& module : frontend.sourceNodes)
{
Set<ModuleName>& dependentsForModule = module.second->dependents;
for (const auto& dep : dependents[module.first])
CHECK_MESSAGE(1 == dependentsForModule.count(dep), "Mismatch in dependents for " << module.first << ": " << message);
}
};
auto validateSecondDependsOnFirst = [&](const std::string& from, const std::string& to, bool expected)
{
SourceNode& fromNode = *frontend.sourceNodes[from];
CHECK_MESSAGE(
fromNode.dependents.count(to) == int(expected),
"Expected " << from << " to " << (expected ? std::string() : std::string("not ")) << "have a reverse dependency on " << to
);
};
// C -> B -> A
{
updateSource("game/Gui/Modules/A", "return {hello=5, world=true}");
updateSource("game/Gui/Modules/B", R"(
return require(game:GetService('Gui').Modules.A)
)");
updateSource("game/Gui/Modules/C", R"(
local Modules = game:GetService('Gui').Modules
local B = require(Modules.B)
return {c_value = B}
)");
frontend.check("game/Gui/Modules/C");
validateMatchesRequireLists("Initial check");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", true);
validateSecondDependsOnFirst("game/Gui/Modules/B", "game/Gui/Modules/C", true);
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/A", false);
}
// C -> B, A
{
updateSource("game/Gui/Modules/B", R"(
return 1
)");
frontend.check("game/Gui/Modules/C");
validateMatchesRequireLists("Removing dependency B->A");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", false);
}
// C -> B -> A
{
updateSource("game/Gui/Modules/B", R"(
return require(game:GetService('Gui').Modules.A)
)");
frontend.check("game/Gui/Modules/C");
validateMatchesRequireLists("Adding back B->A");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", true);
}
// C -> B -> A, D -> (C,B,A)
{
updateSource("game/Gui/Modules/D", R"(
local C = require(game:GetService('Gui').Modules.C)
local B = require(game:GetService('Gui').Modules.B)
local A = require(game:GetService('Gui').Modules.A)
return {d_value = C.c_value}
)");
frontend.check("game/Gui/Modules/D");
validateMatchesRequireLists("Adding D->C, D->B, D->A");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/D", true);
validateSecondDependsOnFirst("game/Gui/Modules/B", "game/Gui/Modules/D", true);
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", true);
}
// B -> A, C <-> D
{
updateSource("game/Gui/Modules/D", "return require(game:GetService('Gui').Modules.C)");
updateSource("game/Gui/Modules/C", "return require(game:GetService('Gui').Modules.D)");
frontend.check("game/Gui/Modules/D");
validateMatchesRequireLists("Adding cycle D->C, C->D");
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", true);
validateSecondDependsOnFirst("game/Gui/Modules/D", "game/Gui/Modules/C", true);
}
// B -> A, C -> D, D -> error
{
updateSource("game/Gui/Modules/D", "return require(game:GetService('Gui').Modules.C.)");
frontend.check("game/Gui/Modules/D");
validateMatchesRequireLists("Adding error dependency D->C.");
validateSecondDependsOnFirst("game/Gui/Modules/D", "game/Gui/Modules/C", true);
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", false);
}
}
TEST_CASE_FIXTURE(FrontendFixture, "test_invalid_dependency_tracking_per_module_resolver")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, false};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = "return require(game:GetService('Gui').Modules.A)";
FrontendOptions opts;
opts.forAutocomplete = false;
frontend.check("game/Gui/Modules/B", opts);
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/B", opts.forAutocomplete));
CHECK(!frontend.allModuleDependenciesValid("game/Gui/Modules/B", !opts.forAutocomplete));
opts.forAutocomplete = true;
frontend.check("game/Gui/Modules/A", opts);
CHECK(!frontend.allModuleDependenciesValid("game/Gui/Modules/B", opts.forAutocomplete));
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/B", !opts.forAutocomplete));
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/A", !opts.forAutocomplete));
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/A", opts.forAutocomplete));
}
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_simple")
{
fileResolver.source["game/Gui/Modules/A"] = R"(
--!strict
return {hello=5, world=true}
)";
fileResolver.source["game/Gui/Modules/B"] = R"(
--!strict
local Modules = game:GetService('Gui').Modules
local A = require(Modules.A)
return {b_value = A.hello}
)";
frontend.queueModuleCheck("game/Gui/Modules/B");
frontend.checkQueuedModules();
auto result = frontend.getCheckResult("game/Gui/Modules/B", true);
REQUIRE(result);
LUAU_REQUIRE_NO_ERRORS(*result);
}
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_instant")
{
fileResolver.source["game/Gui/Modules/A"] = R"(
--!strict
local Modules = game:GetService('Gui').Modules
local B = require(Modules.B)
return {a_value = B.hello}
)";
fileResolver.source["game/Gui/Modules/B"] = R"(
--!strict
local Modules = game:GetService('Gui').Modules
local A = require(Modules.A)
return {b_value = A.hello}
)";
frontend.queueModuleCheck("game/Gui/Modules/B");
frontend.checkQueuedModules();
auto result = frontend.getCheckResult("game/Gui/Modules/B", true);
REQUIRE(result);
LUAU_REQUIRE_ERROR_COUNT(2, *result);
CHECK(toString(result->errors[0]) == "Cyclic module dependency: game/Gui/Modules/B -> game/Gui/Modules/A");
CHECK(toString(result->errors[1]) == "Cyclic module dependency: game/Gui/Modules/A -> game/Gui/Modules/B");
}
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_delayed")
{
fileResolver.source["game/Gui/Modules/C"] = R"(
--!strict
return {c_value = 5}
)";
fileResolver.source["game/Gui/Modules/A"] = R"(
--!strict
local Modules = game:GetService('Gui').Modules
local C = require(Modules.C)
local B = require(Modules.B)
return {a_value = B.hello + C.c_value}
)";
fileResolver.source["game/Gui/Modules/B"] = R"(
--!strict
local Modules = game:GetService('Gui').Modules
local C = require(Modules.C)
local A = require(Modules.A)
return {b_value = A.hello + C.c_value}
)";
frontend.queueModuleCheck("game/Gui/Modules/B");
frontend.checkQueuedModules();
auto result = frontend.getCheckResult("game/Gui/Modules/B", true);
REQUIRE(result);
LUAU_REQUIRE_ERROR_COUNT(2, *result);
CHECK(toString(result->errors[0]) == "Cyclic module dependency: game/Gui/Modules/B -> game/Gui/Modules/A");
CHECK(toString(result->errors[1]) == "Cyclic module dependency: game/Gui/Modules/A -> game/Gui/Modules/B");
}
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_propagates_ice")
{
ScopedFastFlag sffs{FFlag::DebugLuauMagicTypes, true};
ModuleName mm = fromString("MainModule");
fileResolver.source[mm] = R"(
--!strict
local a: _luau_ice = 55
)";
frontend.markDirty(mm);
frontend.queueModuleCheck("MainModule");
CHECK_THROWS_AS(frontend.checkQueuedModules(), InternalCompilerError);
}
TEST_SUITE_END();