mirror of
https://github.com/luau-lang/luau.git
synced 2025-03-05 11:41:40 +00:00

Hey folks, another week means another Luau release! This one features a number of bug fixes in the New Type Solver including improvements to user-defined type functions and a bunch of work to untangle some of the outstanding issues we've been seeing with constraint solving not completing in real world use. We're also continuing to make progress on crashes and other problems that affect the stability of fragment autocomplete, as we work towards delivering consistent, low-latency autocomplete for any editor environment. ## New Type Solver - Fix a bug in user-defined type functions where `print` would incorrectly insert `\1` a number of times. - Fix a bug where attempting to refine an optional generic with a type test will cause a false positive type error (fixes #1666) - Fix a bug where the `refine` type family would not skip over `*no-refine*` discriminants (partial resolution for #1424) - Fix a constraint solving bug where recursive function calls would consistently produce cyclic constraints leading to incomplete or inaccurate type inference. - Implement `readparent` and `writeparent` for class types in user-defined type functions, replacing the incorrectly included `parent` method. - Add initial groundwork (under a debug flag) for eager free type generalization, moving us towards further improvements to constraint solving incomplete errors. ## Fragment Autocomplete - Ease up some assertions to improve stability of mixed-mode use of the two type solvers (i.e. using Fragment Autocomplete on a type graph originally produced by the old type solver) - Resolve a bug with type compatibility checks causing internal compiler errors in autocomplete. ## Lexer and Parser - Improve the accuracy of the roundtrippable AST parsing mode by correctly placing closing parentheses on type groupings. - Add a getter for `offset` in the Lexer by @aduermael in #1688 - Add a second entry point to the parser to parse an expression, `parseExpr` ## Internal Contributors Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: James McNellis <jmcnellis@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> --------- 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: Aviral Goel <agoel@roblox.com> Co-authored-by: Vighnesh <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2105 lines
58 KiB
C++
2105 lines
58 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#include "Luau/FragmentAutocomplete.h"
|
|
#include "Fixture.h"
|
|
#include "Luau/Ast.h"
|
|
#include "Luau/AstQuery.h"
|
|
#include "Luau/Autocomplete.h"
|
|
#include "Luau/BuiltinDefinitions.h"
|
|
#include "Luau/Common.h"
|
|
#include "Luau/Frontend.h"
|
|
#include "Luau/AutocompleteTypes.h"
|
|
#include "Luau/Type.h"
|
|
#include "ScopedFlags.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <optional>
|
|
|
|
|
|
using namespace Luau;
|
|
|
|
LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete)
|
|
LUAU_FASTFLAG(LuauSymbolEquality);
|
|
LUAU_FASTFLAG(LuauStoreSolverTypeOnModule);
|
|
LUAU_FASTFLAG(LexerResumesFromPosition2)
|
|
LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection)
|
|
LUAU_FASTINT(LuauParseErrorLimit)
|
|
LUAU_FASTFLAG(LuauCloneIncrementalModule)
|
|
|
|
LUAU_FASTFLAG(LuauIncrementalAutocompleteBugfixes)
|
|
LUAU_FASTFLAG(LuauMixedModeDefFinderTraversesTypeOf)
|
|
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
|
|
|
|
LUAU_FASTFLAG(LuauBetterReverseDependencyTracking)
|
|
LUAU_FASTFLAG(LuauAutocompleteUsesModuleForTypeCompatibility)
|
|
|
|
static std::optional<AutocompleteEntryMap> nullCallback(std::string tag, std::optional<const ClassType*> ptr, std::optional<std::string> contents)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
static FrontendOptions getOptions()
|
|
{
|
|
FrontendOptions options;
|
|
options.retainFullTypeGraphs = true;
|
|
|
|
if (!FFlag::LuauSolverV2)
|
|
options.forAutocomplete = true;
|
|
|
|
options.runLintChecks = false;
|
|
|
|
return options;
|
|
}
|
|
|
|
static ModuleResolver& getModuleResolver(Luau::Frontend& frontend)
|
|
{
|
|
return FFlag::LuauSolverV2 ? frontend.moduleResolver : frontend.moduleResolverForAutocomplete;
|
|
}
|
|
|
|
template<class BaseType>
|
|
struct FragmentAutocompleteFixtureImpl : BaseType
|
|
{
|
|
static_assert(std::is_base_of_v<Fixture, BaseType>, "BaseType must be a descendant of Fixture");
|
|
|
|
ScopedFastFlag sffs[6] = {
|
|
{FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true},
|
|
{FFlag::LuauStoreSolverTypeOnModule, true},
|
|
{FFlag::LuauSymbolEquality, true},
|
|
{FFlag::LexerResumesFromPosition2, true},
|
|
{FFlag::LuauIncrementalAutocompleteBugfixes, true},
|
|
{FFlag::LuauBetterReverseDependencyTracking, true},
|
|
};
|
|
|
|
FragmentAutocompleteFixtureImpl()
|
|
: BaseType(true)
|
|
{
|
|
}
|
|
|
|
FragmentAutocompleteAncestryResult runAutocompleteVisitor(const std::string& source, const Position& cursorPos)
|
|
{
|
|
ParseResult p = this->tryParse(source); // We don't care about parsing incomplete asts
|
|
REQUIRE(p.root);
|
|
return findAncestryForFragmentParse(p.root, cursorPos);
|
|
}
|
|
|
|
|
|
std::optional<FragmentParseResult> parseFragment(
|
|
const std::string& document,
|
|
const Position& cursorPos,
|
|
std::optional<Position> fragmentEndPosition = std::nullopt
|
|
)
|
|
{
|
|
SourceModule* srcModule = this->getMainSourceModule();
|
|
std::string_view srcString = document;
|
|
return Luau::parseFragment(*srcModule, srcString, cursorPos, fragmentEndPosition);
|
|
}
|
|
|
|
CheckResult checkOldSolver(const std::string& source)
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
return this->check(Mode::Strict, source, getOptions());
|
|
}
|
|
|
|
FragmentTypeCheckResult checkFragment(
|
|
const std::string& document,
|
|
const Position& cursorPos,
|
|
std::optional<Position> fragmentEndPosition = std::nullopt
|
|
)
|
|
{
|
|
auto [_, result] = Luau::typecheckFragment(this->frontend, "MainModule", cursorPos, getOptions(), document, fragmentEndPosition);
|
|
return result;
|
|
}
|
|
|
|
FragmentAutocompleteResult autocompleteFragment(
|
|
const std::string& document,
|
|
Position cursorPos,
|
|
std::optional<Position> fragmentEndPosition = std::nullopt
|
|
)
|
|
{
|
|
FrontendOptions options;
|
|
return Luau::fragmentAutocomplete(this->frontend, document, "MainModule", cursorPos, getOptions(), nullCallback, fragmentEndPosition);
|
|
}
|
|
|
|
|
|
void autocompleteFragmentInBothSolvers(
|
|
const std::string& document,
|
|
const std::string& updated,
|
|
Position cursorPos,
|
|
std::function<void(FragmentAutocompleteResult& result)> assertions,
|
|
std::optional<Position> fragmentEndPosition = std::nullopt
|
|
)
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
this->check(document);
|
|
|
|
FragmentAutocompleteResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition);
|
|
assertions(result);
|
|
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, false};
|
|
this->check(document, getOptions());
|
|
|
|
result = autocompleteFragment(updated, cursorPos, fragmentEndPosition);
|
|
assertions(result);
|
|
}
|
|
|
|
std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragmentForModule(
|
|
const ModuleName& module,
|
|
const std::string& document,
|
|
Position cursorPos,
|
|
std::optional<Position> fragmentEndPosition = std::nullopt
|
|
)
|
|
{
|
|
return Luau::typecheckFragment(this->frontend, module, cursorPos, getOptions(), document, fragmentEndPosition);
|
|
}
|
|
|
|
FragmentAutocompleteResult autocompleteFragmentForModule(
|
|
const ModuleName& module,
|
|
const std::string& document,
|
|
Position cursorPos,
|
|
std::optional<Position> fragmentEndPosition = std::nullopt
|
|
)
|
|
{
|
|
return Luau::fragmentAutocomplete(this->frontend, document, module, cursorPos, getOptions(), nullCallback, fragmentEndPosition);
|
|
}
|
|
};
|
|
|
|
struct FragmentAutocompleteFixture : FragmentAutocompleteFixtureImpl<Fixture>
|
|
{
|
|
FragmentAutocompleteFixture()
|
|
: FragmentAutocompleteFixtureImpl<Fixture>()
|
|
{
|
|
addGlobalBinding(frontend.globals, "table", Binding{builtinTypes->anyType});
|
|
addGlobalBinding(frontend.globals, "math", Binding{builtinTypes->anyType});
|
|
addGlobalBinding(frontend.globalsForAutocomplete, "table", Binding{builtinTypes->anyType});
|
|
addGlobalBinding(frontend.globalsForAutocomplete, "math", Binding{builtinTypes->anyType});
|
|
}
|
|
};
|
|
|
|
struct FragmentAutocompleteBuiltinsFixture : FragmentAutocompleteFixtureImpl<BuiltinsFixture>
|
|
{
|
|
FragmentAutocompleteBuiltinsFixture()
|
|
: FragmentAutocompleteFixtureImpl<BuiltinsFixture>()
|
|
{
|
|
const std::string fakeVecDecl = R"(
|
|
declare class FakeVec
|
|
function dot(self, x: FakeVec) : FakeVec
|
|
zero : FakeVec
|
|
end
|
|
)";
|
|
// The old solver always performs a strict mode check and populates the module resolver and globals
|
|
// for autocomplete.
|
|
// The new solver just populates the globals and the moduleResolver.
|
|
// Because these tests run in both the old solver and the new solver, and the test suite
|
|
// now picks the module resolver as appropriate in order to better mimic the studio code path,
|
|
// we have to load the definition file into both the 'globals'/'resolver' and the equivalent
|
|
// 'for autocomplete'.
|
|
loadDefinition(fakeVecDecl);
|
|
loadDefinition(fakeVecDecl, /* For Autocomplete Module */ true);
|
|
|
|
addGlobalBinding(frontend.globals, "game", Binding{builtinTypes->anyType});
|
|
addGlobalBinding(frontend.globalsForAutocomplete, "game", Binding{builtinTypes->anyType});
|
|
}
|
|
};
|
|
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
|
TEST_SUITE_BEGIN("FragmentAutocompleteTraversalTests");
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "just_two_locals")
|
|
{
|
|
auto result = runAutocompleteVisitor(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)",
|
|
{2, 11}
|
|
);
|
|
|
|
CHECK_EQ(3, result.ancestry.size());
|
|
CHECK_EQ(1, result.localStack.size());
|
|
CHECK_EQ(result.localMap.size(), result.localStack.size());
|
|
REQUIRE(result.nearestStatement);
|
|
|
|
AstStatLocal* local = result.nearestStatement->as<AstStatLocal>();
|
|
REQUIRE(local);
|
|
CHECK(1 == local->vars.size);
|
|
CHECK_EQ("y", std::string(local->vars.data[0]->name.value));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "cursor_within_scope_tracks_locals_from_previous_scope")
|
|
{
|
|
auto result = runAutocompleteVisitor(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
if x == 4 then
|
|
local e = y
|
|
end
|
|
)",
|
|
{4, 15}
|
|
);
|
|
|
|
CHECK_EQ(5, result.ancestry.size());
|
|
CHECK_EQ(2, result.localStack.size());
|
|
CHECK_EQ(result.localMap.size(), result.localStack.size());
|
|
REQUIRE(result.nearestStatement);
|
|
CHECK_EQ("y", std::string(result.localStack.back()->name.value));
|
|
|
|
AstStatLocal* local = result.nearestStatement->as<AstStatLocal>();
|
|
REQUIRE(local);
|
|
CHECK(1 == local->vars.size);
|
|
CHECK_EQ("e", std::string(local->vars.data[0]->name.value));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "cursor_that_comes_later_shouldnt_capture_locals_in_unavailable_scope")
|
|
{
|
|
auto result = runAutocompleteVisitor(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
if x == 4 then
|
|
local e = y
|
|
end
|
|
local z = x + x
|
|
if y == 5 then
|
|
local q = x + y + z
|
|
end
|
|
)",
|
|
{8, 23}
|
|
);
|
|
|
|
CHECK_EQ(6, result.ancestry.size());
|
|
CHECK_EQ(3, result.localStack.size());
|
|
CHECK_EQ(result.localMap.size(), result.localStack.size());
|
|
REQUIRE(result.nearestStatement);
|
|
CHECK_EQ("z", std::string(result.localStack.back()->name.value));
|
|
|
|
AstStatLocal* local = result.nearestStatement->as<AstStatLocal>();
|
|
REQUIRE(local);
|
|
CHECK(1 == local->vars.size);
|
|
CHECK_EQ("q", std::string(local->vars.data[0]->name.value));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nearest_enclosing_statement_can_be_non_local")
|
|
{
|
|
auto result = runAutocompleteVisitor(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
if x == 4 then
|
|
)",
|
|
{3, 4}
|
|
);
|
|
|
|
CHECK_EQ(4, result.ancestry.size());
|
|
CHECK_EQ(2, result.localStack.size());
|
|
CHECK_EQ(result.localMap.size(), result.localStack.size());
|
|
REQUIRE(result.nearestStatement);
|
|
CHECK_EQ("y", std::string(result.localStack.back()->name.value));
|
|
|
|
AstStatIf* ifS = result.nearestStatement->as<AstStatIf>();
|
|
CHECK(ifS != nullptr);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_funcs_show_up_in_local_stack")
|
|
{
|
|
auto result = runAutocompleteVisitor(
|
|
R"(
|
|
local function foo() return 4 end
|
|
local x = foo()
|
|
local function bar() return x + foo() end
|
|
)",
|
|
{3, 32}
|
|
);
|
|
|
|
CHECK_EQ(8, result.ancestry.size());
|
|
CHECK_EQ(2, result.localStack.size());
|
|
CHECK_EQ(result.localMap.size(), result.localStack.size());
|
|
CHECK_EQ("x", std::string(result.localStack.back()->name.value));
|
|
auto returnSt = result.nearestStatement->as<AstStatReturn>();
|
|
CHECK(returnSt != nullptr);
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
|
|
TEST_SUITE_BEGIN("FragmentAutocompleteParserTests");
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "thrown_parse_error_leads_to_null_root")
|
|
{
|
|
check("type A = ");
|
|
ScopedFastInt sfi{FInt::LuauParseErrorLimit, 1};
|
|
auto fragment = parseFragment("type A = <>function<> more garbage here", Position(0, 39));
|
|
CHECK(fragment == std::nullopt);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
check("local a =");
|
|
auto fragment = parseFragment("local a =", Position(0, 10));
|
|
|
|
REQUIRE(fragment.has_value());
|
|
CHECK_EQ("local a =", fragment->fragmentToParse);
|
|
CHECK_EQ(Location{Position{0, 0}, 9}, fragment->root->location);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "statement_in_empty_fragment_is_non_null")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
auto res = check(R"(
|
|
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = parseFragment(
|
|
R"(
|
|
|
|
)",
|
|
Position(1, 0)
|
|
);
|
|
REQUIRE(fragment.has_value());
|
|
CHECK_EQ("\n", fragment->fragmentToParse);
|
|
CHECK_EQ(2, fragment->ancestry.size());
|
|
REQUIRE(fragment->root);
|
|
CHECK_EQ(0, fragment->root->body.size);
|
|
auto statBody = fragment->root->as<AstStatBlock>();
|
|
CHECK(statBody != nullptr);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_complete_fragments")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
auto res = check(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)"
|
|
);
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = parseFragment(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
local z = x + y
|
|
)",
|
|
Position{3, 15}
|
|
);
|
|
|
|
REQUIRE(fragment.has_value());
|
|
|
|
CHECK_EQ(Location{Position{2, 0}, Position{3, 15}}, fragment->root->location);
|
|
|
|
CHECK_EQ("local y = 5\nlocal z = x + y", fragment->fragmentToParse);
|
|
CHECK_EQ(5, fragment->ancestry.size());
|
|
REQUIRE(fragment->root);
|
|
CHECK_EQ(2, fragment->root->body.size);
|
|
auto stat = fragment->root->body.data[1]->as<AstStatLocal>();
|
|
REQUIRE(stat);
|
|
CHECK_EQ(1, stat->vars.size);
|
|
CHECK_EQ(1, stat->values.size);
|
|
CHECK_EQ("z", std::string(stat->vars.data[0]->name.value));
|
|
|
|
auto bin = stat->values.data[0]->as<AstExprBinary>();
|
|
REQUIRE(bin);
|
|
CHECK_EQ(AstExprBinary::Op::Add, bin->op);
|
|
|
|
auto lhs = bin->left->as<AstExprLocal>();
|
|
auto rhs = bin->right->as<AstExprLocal>();
|
|
REQUIRE(lhs);
|
|
REQUIRE(rhs);
|
|
CHECK_EQ("x", std::string(lhs->local->name.value));
|
|
CHECK_EQ("y", std::string(rhs->local->name.value));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_fragments_in_line")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
auto res = check(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)"
|
|
);
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = parseFragment(
|
|
R"(
|
|
local x = 4
|
|
local z = x + y
|
|
local y = 5
|
|
)",
|
|
Position{2, 15}
|
|
);
|
|
|
|
REQUIRE(fragment.has_value());
|
|
|
|
CHECK_EQ("local z = x + y", fragment->fragmentToParse);
|
|
CHECK_EQ(5, fragment->ancestry.size());
|
|
REQUIRE(fragment->root);
|
|
CHECK_EQ(Location{Position{2, 0}, Position{2, 15}}, fragment->root->location);
|
|
CHECK_EQ(1, fragment->root->body.size);
|
|
auto stat = fragment->root->body.data[0]->as<AstStatLocal>();
|
|
REQUIRE(stat);
|
|
CHECK_EQ(1, stat->vars.size);
|
|
CHECK_EQ(1, stat->values.size);
|
|
CHECK_EQ("z", std::string(stat->vars.data[0]->name.value));
|
|
|
|
auto bin = stat->values.data[0]->as<AstExprBinary>();
|
|
REQUIRE(bin);
|
|
CHECK_EQ(AstExprBinary::Op::Add, bin->op);
|
|
|
|
auto lhs = bin->left->as<AstExprLocal>();
|
|
auto rhs = bin->right->as<AstExprGlobal>();
|
|
REQUIRE(lhs);
|
|
REQUIRE(rhs);
|
|
CHECK_EQ("x", std::string(lhs->local->name.value));
|
|
CHECK_EQ("y", std::string(rhs->name.value));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
check(R"(
|
|
local myLocal = 4
|
|
function abc()
|
|
local myInnerLocal = 1
|
|
|
|
end
|
|
)");
|
|
|
|
auto fragment = parseFragment(
|
|
R"(
|
|
local myLocal = 4
|
|
function abc()
|
|
local myInnerLocal = 1
|
|
|
|
end
|
|
)",
|
|
Position{6, 0}
|
|
);
|
|
|
|
REQUIRE(fragment.has_value());
|
|
|
|
CHECK_EQ("\n ", fragment->fragmentToParse);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_single_line_fragment_override")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
auto res = check("function abc(foo: string) end");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto callFragment = parseFragment(
|
|
R"(function abc(foo: string) end
|
|
abc("foo")
|
|
abc("bar")
|
|
)",
|
|
Position{1, 6},
|
|
Position{1, 10}
|
|
);
|
|
|
|
REQUIRE(callFragment.has_value());
|
|
|
|
CHECK_EQ("function abc(foo: string) end\nabc(\"foo\")", callFragment->fragmentToParse);
|
|
CHECK(callFragment->nearestStatement->is<AstStatFunction>());
|
|
|
|
CHECK_GE(callFragment->ancestry.size(), 2);
|
|
|
|
AstNode* back = callFragment->ancestry.back();
|
|
CHECK(back->is<AstExprConstantString>());
|
|
CHECK_EQ(Position{1, 4}, back->location.begin);
|
|
CHECK_EQ(Position{1, 9}, back->location.end);
|
|
|
|
AstNode* parent = callFragment->ancestry.rbegin()[1];
|
|
CHECK(parent->is<AstExprCall>());
|
|
CHECK_EQ(Position{1, 0}, parent->location.begin);
|
|
CHECK_EQ(Position{1, 10}, parent->location.end);
|
|
|
|
|
|
auto stringFragment = parseFragment(
|
|
R"(function abc(foo: string) end
|
|
abc("foo")
|
|
abc("bar")
|
|
)",
|
|
Position{1, 6},
|
|
Position{1, 9}
|
|
);
|
|
|
|
REQUIRE(stringFragment.has_value());
|
|
|
|
CHECK_EQ("function abc(foo: string) end\nabc(\"foo\")", stringFragment->fragmentToParse);
|
|
CHECK(stringFragment->nearestStatement->is<AstStatFunction>());
|
|
|
|
CHECK_GE(stringFragment->ancestry.size(), 1);
|
|
|
|
back = stringFragment->ancestry.back();
|
|
|
|
auto asString = back->as<AstExprConstantString>();
|
|
CHECK(asString);
|
|
|
|
CHECK_EQ(Position{1, 4}, asString->location.begin);
|
|
CHECK_EQ(Position{1, 9}, asString->location.end);
|
|
CHECK_EQ("foo", std::string{asString->value.data});
|
|
CHECK_EQ(AstExprConstantString::QuotedSimple, asString->quoteStyle);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_multi_line_fragment_override")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
auto res = check("function abc(foo: string) end");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = parseFragment(
|
|
R"(function abc(foo: string) end
|
|
abc(
|
|
"foo"
|
|
)
|
|
abc("bar")
|
|
)",
|
|
Position{2, 5},
|
|
Position{3, 1}
|
|
);
|
|
|
|
REQUIRE(fragment.has_value());
|
|
|
|
CHECK_EQ("function abc(foo: string) end\nabc(\n\"foo\"\n)", fragment->fragmentToParse);
|
|
CHECK(fragment->nearestStatement->is<AstStatFunction>());
|
|
|
|
CHECK_GE(fragment->ancestry.size(), 2);
|
|
|
|
AstNode* back = fragment->ancestry.back();
|
|
CHECK(back->is<AstExprConstantString>());
|
|
CHECK_EQ(Position{2, 0}, back->location.begin);
|
|
CHECK_EQ(Position{2, 5}, back->location.end);
|
|
|
|
AstNode* parent = fragment->ancestry.rbegin()[1];
|
|
CHECK(parent->is<AstExprCall>());
|
|
CHECK_EQ(Position{1, 0}, parent->location.begin);
|
|
CHECK_EQ(Position{3, 1}, parent->location.end);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "respects_frontend_options")
|
|
{
|
|
DOES_NOT_PASS_NEW_SOLVER_GUARD();
|
|
|
|
std::string source = R"(
|
|
local tbl = { abc = 1234}
|
|
t
|
|
)";
|
|
fileResolver.source["game/A"] = source;
|
|
|
|
FrontendOptions opts;
|
|
opts.forAutocomplete = true;
|
|
|
|
frontend.check("game/A", opts);
|
|
CHECK_NE(frontend.moduleResolverForAutocomplete.getModule("game/A"), nullptr);
|
|
CHECK_EQ(frontend.moduleResolver.getModule("game/A"), nullptr);
|
|
|
|
|
|
FragmentAutocompleteResult result = Luau::fragmentAutocomplete(frontend, source, "game/A", Position{2, 1}, opts, nullCallback);
|
|
CHECK_EQ("game/A", result.incrementalModule->name);
|
|
CHECK_NE(frontend.moduleResolverForAutocomplete.getModule("game/A"), nullptr);
|
|
CHECK_EQ(frontend.moduleResolver.getModule("game/A"), nullptr);
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
|
|
TEST_SUITE_BEGIN("FragmentAutocompleteTypeCheckerTests");
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_typecheck_simple_fragment")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
auto res = check(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)"
|
|
);
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = checkFragment(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
local z = x + y
|
|
)",
|
|
Position{3, 15}
|
|
);
|
|
|
|
auto opt = linearSearchForBinding(fragment.freshScope.get(), "z");
|
|
REQUIRE(opt);
|
|
CHECK_EQ("number", toString(*opt));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_typecheck_fragment_inserted_inline")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
auto res = check(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)"
|
|
);
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
auto fragment = checkFragment(
|
|
R"(
|
|
local x = 4
|
|
local z = x
|
|
local y = 5
|
|
)",
|
|
Position{2, 11}
|
|
);
|
|
|
|
auto correct = linearSearchForBinding(fragment.freshScope.get(), "z");
|
|
REQUIRE(correct);
|
|
CHECK_EQ("number", toString(*correct));
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
|
|
TEST_SUITE_BEGIN("MixedModeTests");
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "mixed_mode_basic_example_append")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
auto res = checkOldSolver(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)"
|
|
);
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = checkFragment(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
local z = x + y
|
|
)",
|
|
Position{3, 15}
|
|
);
|
|
|
|
auto opt = linearSearchForBinding(fragment.freshScope.get(), "z");
|
|
REQUIRE(opt);
|
|
CHECK_EQ("number", toString(*opt));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "mixed_mode_basic_example_inlined")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
auto res = checkOldSolver(
|
|
R"(
|
|
local x = 4
|
|
local y = 5
|
|
)"
|
|
);
|
|
|
|
auto fragment = checkFragment(
|
|
R"(
|
|
local x = 4
|
|
local z = x
|
|
local y = 5
|
|
)",
|
|
Position{2, 11}
|
|
);
|
|
|
|
auto correct = linearSearchForBinding(fragment.freshScope.get(), "z");
|
|
REQUIRE(correct);
|
|
CHECK_EQ("number", toString(*correct));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "mixed_mode_can_autocomplete_simple_property_access")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
auto res = checkOldSolver(
|
|
R"(
|
|
local tbl = { abc = 1234}
|
|
)"
|
|
);
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(res);
|
|
|
|
auto fragment = autocompleteFragment(
|
|
R"(
|
|
local tbl = { abc = 1234}
|
|
tbl.
|
|
)",
|
|
Position{2, 5}
|
|
);
|
|
|
|
LUAU_ASSERT(fragment.freshScope);
|
|
|
|
CHECK_EQ(1, fragment.acResults.entryMap.size());
|
|
CHECK(fragment.acResults.entryMap.count("abc"));
|
|
CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "typecheck_fragment_handles_stale_module")
|
|
{
|
|
const std::string sourceName = "MainModule";
|
|
fileResolver.source[sourceName] = "local x = 5";
|
|
|
|
CheckResult checkResult = frontend.check(sourceName, getOptions());
|
|
LUAU_REQUIRE_NO_ERRORS(checkResult);
|
|
|
|
auto [result, _] = typecheckFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0));
|
|
CHECK_EQ(result, FragmentTypeCheckStatus::Success);
|
|
|
|
frontend.markDirty(sourceName);
|
|
frontend.parse(sourceName);
|
|
|
|
CHECK_NE(frontend.getSourceModule(sourceName), nullptr);
|
|
|
|
auto [result2, __] = typecheckFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0));
|
|
CHECK_EQ(result2, FragmentTypeCheckStatus::SkipAutocomplete);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "typecheck_fragment_handles_unusable_module")
|
|
{
|
|
const std::string sourceA = "MainModule";
|
|
fileResolver.source[sourceA] = R"(
|
|
local Modules = game:GetService('Gui').Modules
|
|
local B = require(Modules.B)
|
|
return { hello = B }
|
|
)";
|
|
|
|
const std::string sourceB = "game/Gui/Modules/B";
|
|
fileResolver.source[sourceB] = R"(return {hello = "hello"})";
|
|
|
|
CheckResult result = frontend.check(sourceA, getOptions());
|
|
CHECK(!frontend.isDirty(sourceA, getOptions().forAutocomplete));
|
|
|
|
std::weak_ptr<Module> weakModule = getModuleResolver(frontend).getModule(sourceB);
|
|
REQUIRE(!weakModule.expired());
|
|
|
|
frontend.markDirty(sourceB);
|
|
CHECK(frontend.isDirty(sourceA, getOptions().forAutocomplete));
|
|
|
|
frontend.check(sourceB, getOptions());
|
|
CHECK(weakModule.expired());
|
|
|
|
auto [status, _] = typecheckFragmentForModule(sourceA, fileResolver.source[sourceA], Luau::Position(0, 0));
|
|
CHECK_EQ(status, FragmentTypeCheckStatus::SkipAutocomplete);
|
|
|
|
auto [status2, _2] = typecheckFragmentForModule(sourceB, fileResolver.source[sourceB], Luau::Position(3, 20));
|
|
CHECK_EQ(status2, FragmentTypeCheckStatus::Success);
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
TEST_SUITE_BEGIN("FragmentAutocompleteTests");
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "multiple_fragment_autocomplete")
|
|
{
|
|
ToStringOptions opt;
|
|
opt.exhaustive = true;
|
|
opt.exhaustive = true;
|
|
opt.functionTypeArguments = true;
|
|
opt.maxTableLength = 0;
|
|
opt.maxTypeLength = 0;
|
|
|
|
auto checkAndExamine = [&](const std::string& src, const std::string& idName, const std::string& idString)
|
|
{
|
|
check(src, getOptions());
|
|
auto id = getType(idName, true);
|
|
LUAU_ASSERT(id);
|
|
CHECK_EQ(Luau::toString(*id, opt), idString);
|
|
};
|
|
|
|
auto getTypeFromModule = [](ModulePtr module, const std::string& name) -> std::optional<TypeId>
|
|
{
|
|
if (!module->hasModuleScope())
|
|
return std::nullopt;
|
|
return lookupName(module->getModuleScope(), name);
|
|
};
|
|
|
|
auto fragmentACAndCheck = [&](const std::string& updated,
|
|
const Position& pos,
|
|
const std::string& idName,
|
|
const std::string& srcIdString,
|
|
const std::string& fragIdString)
|
|
{
|
|
FragmentAutocompleteResult result = autocompleteFragment(updated, pos, std::nullopt);
|
|
auto fragId = getTypeFromModule(result.incrementalModule, idName);
|
|
LUAU_ASSERT(fragId);
|
|
CHECK_EQ(Luau::toString(*fragId, opt), fragIdString);
|
|
|
|
auto srcId = getType(idName, true);
|
|
LUAU_ASSERT(srcId);
|
|
CHECK_EQ(Luau::toString(*srcId, opt), srcIdString);
|
|
};
|
|
|
|
const std::string source = R"(local module = {}
|
|
f
|
|
return module)";
|
|
|
|
const std::string updated1 = R"(local module = {}
|
|
function module.a
|
|
return module)";
|
|
|
|
const std::string updated2 = R"(local module = {}
|
|
function module.ab
|
|
return module)";
|
|
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
ScopedFastFlag sff2{FFlag::LuauCloneIncrementalModule, true};
|
|
ScopedFastFlag sff3{FFlag::LuauFreeTypesMustHaveBounds, true};
|
|
checkAndExamine(source, "module", "{ }");
|
|
fragmentACAndCheck(updated1, Position{1, 17}, "module", "{ }", "{ a: (%error-id%: unknown) -> () }");
|
|
fragmentACAndCheck(updated2, Position{1, 18}, "module", "{ }", "{ ab: (%error-id%: unknown) -> () }");
|
|
}
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
checkAndExamine(source, "module", "{ }");
|
|
// [TODO] CLI-140762 Fragment autocomplete still doesn't return correct result when LuauSolverV2 is on
|
|
return;
|
|
fragmentACAndCheck(updated1, Position{1, 17}, "module", "{ }", "{ a: (%error-id%: unknown) -> () }");
|
|
fragmentACAndCheck(updated2, Position{1, 18}, "module", "{ }", "{ ab: (%error-id%: unknown) -> () }");
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_autocomplete_simple_property_access")
|
|
{
|
|
|
|
const std::string source = R"(
|
|
local tbl = { abc = 1234}
|
|
)";
|
|
const std::string updated = R"(
|
|
local tbl = { abc = 1234}
|
|
tbl.
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{2, 5},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
LUAU_ASSERT(fragment.freshScope);
|
|
|
|
CHECK_EQ(1, fragment.acResults.entryMap.size());
|
|
CHECK(fragment.acResults.entryMap.count("abc"));
|
|
CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_autocomplete_nested_property_access")
|
|
{
|
|
const std::string source = R"(
|
|
local tbl = { abc = { def = 1234, egh = false } }
|
|
)";
|
|
const std::string updated = R"(
|
|
local tbl = { abc = { def = 1234, egh = false } }
|
|
tbl.abc.
|
|
)";
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{2, 8},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
LUAU_ASSERT(fragment.freshScope);
|
|
|
|
CHECK_EQ(2, fragment.acResults.entryMap.size());
|
|
CHECK(fragment.acResults.entryMap.count("def"));
|
|
CHECK(fragment.acResults.entryMap.count("egh"));
|
|
CHECK_EQ(fragment.acResults.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "multiple_functions_complex")
|
|
{
|
|
const std::string text = R"( local function f1(a1)
|
|
local l1 = 1;"
|
|
g1 = 1;"
|
|
end
|
|
|
|
local function f2(a2)
|
|
local l2 = 1;
|
|
g2 = 1;
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{0, 0},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") == 0);
|
|
CHECK(strings.count("a1") == 0);
|
|
CHECK(strings.count("l1") == 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") == 0);
|
|
CHECK(strings.count("a2") == 0);
|
|
CHECK(strings.count("l2") == 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{0, 22},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") != 0);
|
|
CHECK(strings.count("a1") != 0);
|
|
CHECK(strings.count("l1") == 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") == 0);
|
|
CHECK(strings.count("a2") == 0);
|
|
CHECK(strings.count("l2") == 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{1, 17},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") != 0);
|
|
CHECK(strings.count("a1") != 0);
|
|
CHECK(strings.count("l1") != 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") == 0);
|
|
CHECK(strings.count("a2") == 0);
|
|
CHECK(strings.count("l2") == 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{2, 11},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") != 0);
|
|
CHECK(strings.count("a1") != 0);
|
|
CHECK(strings.count("l1") != 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") == 0);
|
|
CHECK(strings.count("a2") == 0);
|
|
CHECK(strings.count("l2") == 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{4, 0},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") != 0);
|
|
// FIXME: RIDE-11123: This should be zero counts of `a1`.
|
|
CHECK(strings.count("a1") != 0);
|
|
CHECK(strings.count("l1") == 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") == 0);
|
|
CHECK(strings.count("a2") == 0);
|
|
CHECK(strings.count("l2") == 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{6, 17},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") != 0);
|
|
CHECK(strings.count("a1") == 0);
|
|
CHECK(strings.count("l1") == 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") != 0);
|
|
CHECK(strings.count("a2") != 0);
|
|
CHECK(strings.count("l2") != 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
text,
|
|
text,
|
|
Position{8, 4},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto strings = fragment.acResults.entryMap;
|
|
CHECK(strings.count("f1") != 0);
|
|
CHECK(strings.count("a1") == 0);
|
|
CHECK(strings.count("l1") == 0);
|
|
CHECK(strings.count("g1") != 0);
|
|
CHECK(strings.count("f2") != 0);
|
|
// FIXME: RIDE-11123: This should be zero counts of `a2`.
|
|
CHECK(strings.count("a2") != 0);
|
|
CHECK(strings.count("l2") == 0);
|
|
CHECK(strings.count("g2") != 0);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inline_autocomplete_picks_the_right_scope")
|
|
{
|
|
const std::string source = R"(
|
|
type Table = { a: number, b: number }
|
|
do
|
|
type Table = { x: string, y: string }
|
|
end
|
|
)";
|
|
|
|
const std::string updated = R"(
|
|
type Table = { a: number, b: number }
|
|
do
|
|
type Table = { x: string, y: string }
|
|
local a : T
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{4, 15},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
LUAU_ASSERT(fragment.freshScope);
|
|
|
|
REQUIRE(fragment.acResults.entryMap.count("Table"));
|
|
REQUIRE(fragment.acResults.entryMap["Table"].type);
|
|
const TableType* tv = get<TableType>(follow(*fragment.acResults.entryMap["Table"].type));
|
|
REQUIRE(tv);
|
|
CHECK(tv->props.count("x"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nested_recursive_function")
|
|
{
|
|
const std::string source = R"(
|
|
function foo()
|
|
end
|
|
)";
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{2, 0},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
CHECK(fragment.acResults.entryMap.count("foo"));
|
|
CHECK_EQ(AutocompleteContext::Statement, fragment.acResults.context);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "string_literal_with_override")
|
|
{
|
|
const std::string source = R"(
|
|
function foo(bar: string) end
|
|
foo("abc")
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{2, 6},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
CHECK(fragment.acResults.entryMap.empty());
|
|
CHECK_EQ(AutocompleteContext::String, fragment.acResults.context);
|
|
},
|
|
Position{2, 9}
|
|
);
|
|
}
|
|
|
|
// Start compatibility tests!
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "empty_program")
|
|
{
|
|
autocompleteFragmentInBothSolvers(
|
|
"",
|
|
"",
|
|
Position{0, 1},
|
|
[](FragmentAutocompleteResult& frag)
|
|
{
|
|
auto ac = frag.acResults;
|
|
CHECK(ac.entryMap.count("table"));
|
|
CHECK(ac.entryMap.count("math"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Statement);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer")
|
|
{
|
|
const std::string source = "local a =";
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{0, 9},
|
|
[](FragmentAutocompleteResult& frag)
|
|
{
|
|
auto ac = frag.acResults;
|
|
|
|
CHECK(ac.entryMap.count("table"));
|
|
CHECK(ac.entryMap.count("math"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Expression);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "leave_numbers_alone")
|
|
{
|
|
const std::string source = "local a = 3.";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{0, 12},
|
|
[](FragmentAutocompleteResult& frag)
|
|
{
|
|
auto ac = frag.acResults;
|
|
CHECK(ac.entryMap.empty());
|
|
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "user_defined_globals")
|
|
{
|
|
const std::string source = "local myLocal = 4; ";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{0, 18},
|
|
[](FragmentAutocompleteResult& frag)
|
|
{
|
|
auto ac = frag.acResults;
|
|
|
|
CHECK(ac.entryMap.count("myLocal"));
|
|
CHECK(ac.entryMap.count("table"));
|
|
CHECK(ac.entryMap.count("math"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Statement);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "dont_suggest_local_before_its_definition")
|
|
{
|
|
const std::string source = R"(
|
|
local myLocal = 4
|
|
function abc()
|
|
local myInnerLocal = 1
|
|
|
|
end
|
|
)";
|
|
|
|
// autocomplete after abc but before myInnerLocal
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 0},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto ac = fragment.acResults;
|
|
CHECK(ac.entryMap.count("myLocal"));
|
|
LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal");
|
|
}
|
|
);
|
|
// autocomplete after my inner local
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 0},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto ac = fragment.acResults;
|
|
CHECK(ac.entryMap.count("myLocal"));
|
|
CHECK(ac.entryMap.count("myInnerLocal"));
|
|
}
|
|
);
|
|
|
|
// autocomplete after abc, but don't include myInnerLocal(in the hidden scope)
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{6, 0},
|
|
[](FragmentAutocompleteResult& fragment)
|
|
{
|
|
auto ac = fragment.acResults;
|
|
CHECK(ac.entryMap.count("myLocal"));
|
|
LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal");
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nested_recursive_function")
|
|
{
|
|
const std::string source = R"(
|
|
local function outer()
|
|
local function inner()
|
|
end
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK(ac.entryMap.count("inner"));
|
|
CHECK(ac.entryMap.count("outer"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "user_defined_local_functions_in_own_definition")
|
|
{
|
|
const std::string source = R"(
|
|
local function abc()
|
|
|
|
end
|
|
)";
|
|
// Autocomplete inside of abc
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{2, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK(ac.entryMap.count("abc"));
|
|
CHECK(ac.entryMap.count("table"));
|
|
CHECK(ac.entryMap.count("math"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "global_functions_are_not_scoped_lexically")
|
|
{
|
|
const std::string source = R"(
|
|
if true then
|
|
function abc()
|
|
|
|
end
|
|
end
|
|
)";
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{6, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK(!ac.entryMap.empty());
|
|
CHECK(ac.entryMap.count("abc"));
|
|
CHECK(ac.entryMap.count("table"));
|
|
CHECK(ac.entryMap.count("math"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_functions_fall_out_of_scope")
|
|
{
|
|
const std::string source = R"(
|
|
if true then
|
|
local function abc()
|
|
|
|
end
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{6, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK_NE(0, ac.entryMap.size());
|
|
LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "abc");
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "function_parameters")
|
|
{
|
|
const std::string source = R"(
|
|
function abc(test)
|
|
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK(ac.entryMap.count("test"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "unsealed_table")
|
|
{
|
|
const std::string source = R"(
|
|
local tbl = {}
|
|
tbl.prop = 5
|
|
tbl.
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 12},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK_EQ(1, ac.entryMap.size());
|
|
CHECK(ac.entryMap.count("prop"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "unsealed_table_2")
|
|
{
|
|
const std::string source = R"(
|
|
local tbl = {}
|
|
local inner = { prop = 5 }
|
|
tbl.inner = inner
|
|
tbl.inner.
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 18},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK_EQ(1, ac.entryMap.size());
|
|
CHECK(ac.entryMap.count("prop"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "cyclic_table")
|
|
{
|
|
const std::string source = R"(
|
|
local abc = {}
|
|
local def = { abc = abc }
|
|
abc.def = def
|
|
abc.def.
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 16},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK(ac.entryMap.count("abc"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "table_union")
|
|
{
|
|
const std::string source = R"(
|
|
type t1 = { a1 : string, b2 : number }
|
|
type t2 = { b2 : string, c3 : string }
|
|
function func(abc : t1 | t2)
|
|
|
|
end
|
|
)";
|
|
const std::string updated = R"(
|
|
type t1 = { a1 : string, b2 : number }
|
|
type t2 = { b2 : string, c3 : string }
|
|
function func(abc : t1 | t2)
|
|
abc.
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{4, 16},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK_EQ(1, ac.entryMap.size());
|
|
CHECK(ac.entryMap.count("b2"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "table_intersection")
|
|
{
|
|
const std::string source = R"(
|
|
type t1 = { a1 : string, b2 : number }
|
|
type t2 = { b2 : number, c3 : string }
|
|
function func(abc : t1 & t2)
|
|
|
|
end
|
|
)";
|
|
const std::string updated = R"(
|
|
type t1 = { a1 : string, b2 : number }
|
|
type t2 = { b2 : number, c3 : string }
|
|
function func(abc : t1 & t2)
|
|
abc.
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{4, 16},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK_EQ(3, ac.entryMap.size());
|
|
CHECK(ac.entryMap.count("a1"));
|
|
CHECK(ac.entryMap.count("b2"));
|
|
CHECK(ac.entryMap.count("c3"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "get_suggestions_for_the_very_start_of_the_script")
|
|
{
|
|
const std::string source = R"(
|
|
|
|
function aaa() end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{0, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK(ac.entryMap.count("table"));
|
|
CHECK_EQ(ac.context, AutocompleteContext::Statement);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "studio_ice_1")
|
|
{
|
|
const std::string source = R"(
|
|
--Woop
|
|
@native
|
|
local function test()
|
|
|
|
end
|
|
)";
|
|
|
|
const std::string updated = R"(
|
|
--Woop
|
|
@native
|
|
local function test()
|
|
|
|
end
|
|
function a
|
|
)";
|
|
autocompleteFragmentInBothSolvers(source, updated, Position{6, 10}, [](FragmentAutocompleteResult& result) {});
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "method_call_inside_function_body")
|
|
{
|
|
const std::string source = R"(
|
|
local game = { GetService=function(s) return 'hello' end }
|
|
|
|
function a()
|
|
|
|
end
|
|
)";
|
|
|
|
const std::string updated = R"(
|
|
local game = { GetService=function(s) return 'hello' end }
|
|
|
|
function a()
|
|
game:
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{4, 17},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto ac = result.acResults;
|
|
CHECK_NE(0, ac.entryMap.size());
|
|
|
|
LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "math");
|
|
CHECK_EQ(ac.context, AutocompleteContext::Property);
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "tbl_function_parameter")
|
|
{
|
|
const std::string source = R"(
|
|
--!strict
|
|
type Foo = {x : number, y : number}
|
|
local function func(abc : Foo)
|
|
abc.
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 7},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK_EQ(2, result.acResults.entryMap.size());
|
|
CHECK(result.acResults.entryMap.count("x"));
|
|
CHECK(result.acResults.entryMap.count("y"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "tbl_local_function_parameter")
|
|
{
|
|
const std::string source = R"(
|
|
--!strict
|
|
type Foo = {x : number, y : number}
|
|
local function func(abc : Foo)
|
|
abc.
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 7},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK_EQ(2, result.acResults.entryMap.size());
|
|
CHECK(result.acResults.entryMap.count("x"));
|
|
CHECK(result.acResults.entryMap.count("y"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "vec3_function_parameter")
|
|
{
|
|
const std::string source = R"(
|
|
--!strict
|
|
local function func(abc : FakeVec)
|
|
abc.
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 7},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK_EQ(2, result.acResults.entryMap.size());
|
|
CHECK(result.acResults.entryMap.count("zero"));
|
|
CHECK(result.acResults.entryMap.count("dot"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "vec3_local_function_parameter")
|
|
{
|
|
const std::string source = R"(
|
|
--!strict
|
|
local function func(abc : FakeVec)
|
|
abc.
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 7},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK_EQ(2, result.acResults.entryMap.size());
|
|
CHECK(result.acResults.entryMap.count("zero"));
|
|
CHECK(result.acResults.entryMap.count("dot"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "function_parameter_not_recommending_out_of_scope_argument")
|
|
{
|
|
const std::string source = R"(
|
|
--!strict
|
|
local function foo(abd: FakeVec)
|
|
end
|
|
local function bar(abc : FakeVec)
|
|
a
|
|
end
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{5, 5},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.count("abc"));
|
|
CHECK(!result.acResults.entryMap.count("abd"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "bad_range")
|
|
{
|
|
const std::string source = R"(
|
|
l
|
|
)";
|
|
const std::string updated = R"(
|
|
local t = 1
|
|
t
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{2, 1},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
auto opt = linearSearchForBinding(result.freshScope, "t");
|
|
REQUIRE(opt);
|
|
CHECK_EQ("number", toString(*opt));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments_simple")
|
|
{
|
|
const std::string source = R"(
|
|
-- sel
|
|
-- retur
|
|
-- fo
|
|
-- if
|
|
-- end
|
|
-- the
|
|
)";
|
|
ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true};
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 6},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments_blocks")
|
|
{
|
|
const std::string source = R"(
|
|
--[[
|
|
comment 1
|
|
]] local
|
|
-- [[ comment 2]]
|
|
--
|
|
-- sdfsdfsdf
|
|
--[[comment 3]]
|
|
--[[
|
|
foo
|
|
bar
|
|
baz
|
|
]]
|
|
)";
|
|
ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true};
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 2},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(!result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{8, 6},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{10, 0},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments")
|
|
{
|
|
const std::string source = R"(
|
|
-- sel
|
|
-- retur
|
|
-- fo
|
|
--[[ sel ]]
|
|
local -- hello
|
|
)";
|
|
ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true};
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{1, 7},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{2, 9},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{3, 6},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{4, 9},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{5, 6},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(!result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
source,
|
|
Position{5, 14},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments_in_incremental_fragment")
|
|
{
|
|
const std::string source = R"(
|
|
local x = 5
|
|
if x == 5
|
|
)";
|
|
const std::string updated = R"(
|
|
local x = 5
|
|
if x == 5 then -- a comment
|
|
)";
|
|
ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true};
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{2, 28},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_handles_parse_errors")
|
|
{
|
|
|
|
ScopedFastInt sfi{FInt::LuauParseErrorLimit, 1};
|
|
const std::string source = R"(
|
|
|
|
)";
|
|
const std::string updated = R"(
|
|
type A = <>random non code text here
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
source,
|
|
updated,
|
|
Position{1, 38},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.empty());
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_handles_stale_module")
|
|
{
|
|
const std::string sourceName = "MainModule";
|
|
fileResolver.source[sourceName] = "local x = 5";
|
|
|
|
frontend.check(sourceName, getOptions());
|
|
frontend.markDirty(sourceName);
|
|
frontend.parse(sourceName);
|
|
|
|
FragmentAutocompleteResult result = autocompleteFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0));
|
|
CHECK(result.acResults.entryMap.empty());
|
|
CHECK_EQ(result.incrementalModule, nullptr);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "require_tracing")
|
|
{
|
|
fileResolver.source["MainModule/A"] = R"(
|
|
return { x = 0 }
|
|
)";
|
|
|
|
fileResolver.source["MainModule"] = R"(
|
|
local result = require(script.A)
|
|
local x = 1 + result.
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(
|
|
fileResolver.source["MainModule"],
|
|
fileResolver.source["MainModule"],
|
|
Position{2, 21},
|
|
[](FragmentAutocompleteResult& result)
|
|
{
|
|
CHECK(result.acResults.entryMap.size() == 1);
|
|
CHECK(result.acResults.entryMap.count("x"));
|
|
}
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "fragment_ac_must_traverse_typeof_and_not_ice")
|
|
{
|
|
// This test ensures that we traverse typeof expressions for defs that are being referred to in the fragment
|
|
// In this case, we want to ensure we populate the incremental environment with the reference to `m`
|
|
// Without this, we would ice as we will refer to the local `m` before it's declaration
|
|
ScopedFastFlag sff{FFlag::LuauMixedModeDefFinderTraversesTypeOf, true};
|
|
const std::string source = R"(
|
|
--!strict
|
|
local m = {}
|
|
-- and here
|
|
function m:m1() end
|
|
type nt = typeof(m)
|
|
|
|
return m
|
|
)";
|
|
const std::string updated = R"(
|
|
--!strict
|
|
local m = {}
|
|
-- and here
|
|
function m:m1() end
|
|
type nt = typeof(m)
|
|
l
|
|
return m
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(source, updated, Position{6, 2}, [](auto& _) {});
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "generalization_crash_when_old_solver_freetypes_have_no_bounds_set")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauFreeTypesMustHaveBounds, true};
|
|
const std::string source = R"(
|
|
local UserInputService = game:GetService("UserInputService");
|
|
|
|
local Camera = workspace.CurrentCamera;
|
|
|
|
UserInputService.InputBegan:Connect(function(Input)
|
|
if (Input.KeyCode == Enum.KeyCode.One) then
|
|
local Up = Input.Foo
|
|
local Vector = -(Up:Unit)
|
|
end
|
|
end)
|
|
)";
|
|
|
|
const std::string dest = R"(
|
|
local UserInputService = game:GetService("UserInputService");
|
|
|
|
local Camera = workspace.CurrentCamera;
|
|
|
|
UserInputService.InputBegan:Connect(function(Input)
|
|
if (Input.KeyCode == Enum.KeyCode.One) then
|
|
local Up = Input.Foo
|
|
local Vector = -(Up:Unit())
|
|
end
|
|
end)
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(source, dest, Position{8, 36}, [](auto& _) {});
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_ensures_memory_isolation")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauCloneIncrementalModule, true};
|
|
ToStringOptions opt;
|
|
opt.exhaustive = true;
|
|
opt.exhaustive = true;
|
|
opt.functionTypeArguments = true;
|
|
opt.maxTableLength = 0;
|
|
opt.maxTypeLength = 0;
|
|
|
|
auto checkAndExamine = [&](const std::string& src, const std::string& idName, const std::string& idString)
|
|
{
|
|
check(src, getOptions());
|
|
auto id = getType(idName, true);
|
|
LUAU_ASSERT(id);
|
|
CHECK_EQ(Luau::toString(*id, opt), idString);
|
|
};
|
|
|
|
auto getTypeFromModule = [](ModulePtr module, const std::string& name) -> std::optional<TypeId>
|
|
{
|
|
if (!module->hasModuleScope())
|
|
return std::nullopt;
|
|
return lookupName(module->getModuleScope(), name);
|
|
};
|
|
|
|
auto fragmentACAndCheck = [&](const std::string& updated, const Position& pos, const std::string& idName)
|
|
{
|
|
FragmentAutocompleteResult result = autocompleteFragment(updated, pos, std::nullopt);
|
|
auto fragId = getTypeFromModule(result.incrementalModule, idName);
|
|
LUAU_ASSERT(fragId);
|
|
|
|
auto srcId = getType(idName, true);
|
|
LUAU_ASSERT(srcId);
|
|
|
|
CHECK((*fragId)->owningArena != (*srcId)->owningArena);
|
|
CHECK(&(result.incrementalModule->internalTypes) == (*fragId)->owningArena);
|
|
};
|
|
|
|
const std::string source = R"(local module = {}
|
|
f
|
|
return module)";
|
|
|
|
const std::string updated1 = R"(local module = {}
|
|
function module.a
|
|
return module)";
|
|
|
|
const std::string updated2 = R"(local module = {}
|
|
function module.ab
|
|
return module)";
|
|
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
checkAndExamine(source, "module", "{ }");
|
|
// [TODO] CLI-140762 we shouldn't mutate stale module in autocompleteFragment
|
|
// early return since the following checking will fail, which it shouldn't!
|
|
fragmentACAndCheck(updated1, Position{1, 17}, "module");
|
|
fragmentACAndCheck(updated2, Position{1, 18}, "module");
|
|
}
|
|
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
checkAndExamine(source, "module", "{ }");
|
|
// [TODO] CLI-140762 we shouldn't mutate stale module in autocompleteFragment
|
|
// early return since the following checking will fail, which it shouldn't!
|
|
fragmentACAndCheck(updated1, Position{1, 17}, "module");
|
|
fragmentACAndCheck(updated2, Position{1, 18}, "module");
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_shouldnt_crash_on_cross_module_mutation")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauCloneIncrementalModule, true};
|
|
const std::string source = R"(local module = {}
|
|
function module.
|
|
return module
|
|
)";
|
|
|
|
const std::string updated = R"(local module = {}
|
|
function module.f
|
|
return module
|
|
)";
|
|
|
|
autocompleteFragmentInBothSolvers(source, updated, Position{1, 18}, [](FragmentAutocompleteResult& result) {});
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "ice_caused_by_mixed_mode_use")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauAutocompleteUsesModuleForTypeCompatibility, true};
|
|
const std::string source = "--[[\n\tPackage link auto-generated by Rotriever\n]]\nlocal PackageIndex = script.Parent._Index\n\nlocal Package = "
|
|
"require(PackageIndex[\"ReactOtter\"][\"ReactOtter\"])\n\nexport type Goal = Package.Goal\nexport type SpringOptions "
|
|
"= Package.SpringOptions\n\n\nreturn Pa";
|
|
autocompleteFragmentInBothSolvers(source, source, Position{11,9}, [](auto& _){
|
|
|
|
});
|
|
}
|
|
|
|
TEST_SUITE_END();
|