luau/tests/FragmentAutocomplete.test.cpp
menarulalam f8a1e0129d
Sync to upstream/release/659 (#1637)
## What's Changed

General performance improvements and bug fixes. `lua_clonetable` was
added too.

### General

## Runtime
- Improvements were made to Luau's performance, including a
`lua_clonetable` function and optimizations to string caching. Buffer
read/write operations were optimized for big-endian machines.
## New Solver
- Crashes related to duplicate keys in table literals, fragment AC
crashes, and potential hash collisions in the StringCache.
- We now handle user-defined type functions as opaque and track interior
free table types.
## Require By String
- Require-by-string path resolution was simplified.

**Full Changelog**:
https://github.com/luau-lang/luau/compare/0.658...0.659

---

Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Varun Saini <vsaini@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Yohoo Lin <yohoo@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>
2025-01-31 18:58:36 -08:00

1680 lines
43 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 <algorithm>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <optional>
using namespace Luau;
LUAU_FASTFLAG(LuauAllowFragmentParsing);
LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete)
LUAU_FASTFLAG(LuauSymbolEquality);
LUAU_FASTFLAG(LuauStoreSolverTypeOnModule);
LUAU_FASTFLAG(LexerResumesFromPosition2)
LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection)
LUAU_FASTINT(LuauParseErrorLimit)
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;
}
template<class BaseType>
struct FragmentAutocompleteFixtureImpl : BaseType
{
ScopedFastFlag sffs[5] = {
{FFlag::LuauAllowFragmentParsing, true},
{FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true},
{FFlag::LuauStoreSolverTypeOnModule, true},
{FFlag::LuauSymbolEquality, true},
{FFlag::LexerResumesFromPosition2, 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);
}
};
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);
}
};
//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_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};
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", "{ }", "{ 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 we shouldn't mutate stale module in autocompleteFragment
// early return since the following checking will fail, which it shouldn't!
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, "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_SUITE_END();