// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Autocomplete.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/Parser.h" #include "Luau/TypeInfer.h" #include "Luau/TypeVar.h" #include "Luau/VisitTypeVar.h" #include "Luau/StringUtils.h" #include "Fixture.h" #include "doctest.h" #include LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) using namespace Luau; static std::optional nullCallback(std::string tag, std::optional ptr) { return std::nullopt; } struct ACFixture : Fixture { AutocompleteResult autocomplete(unsigned row, unsigned column) { return Luau::autocomplete(frontend, "MainModule", Position{row, column}, nullCallback); } AutocompleteResult autocomplete(char marker) { auto i = markerPosition.find(marker); LUAU_ASSERT(i != markerPosition.end()); const Position& pos = i->second; return Luau::autocomplete(frontend, "MainModule", pos, nullCallback); } CheckResult check(const std::string& source) { markerPosition.clear(); std::string filteredSource; filteredSource.reserve(source.size()); Position curPos(0, 0); for (char c : source) { if (c == '@' && !filteredSource.empty()) { char prevChar = filteredSource.back(); filteredSource.pop_back(); curPos.column--; // Adjust column position since we removed a character from the output LUAU_ASSERT("Illegal marker character" && prevChar >= '0' && prevChar <= '9'); LUAU_ASSERT("Duplicate marker found" && markerPosition.count(prevChar) == 0); markerPosition.insert(std::pair{prevChar, curPos}); } else { filteredSource.push_back(c); if (c == '\n') { curPos.line++; curPos.column = 0; } else { curPos.column++; } } } return Fixture::check(filteredSource); } // Maps a marker character (0-9 inclusive) to a position in the source code. std::map markerPosition; }; TEST_SUITE_BEGIN("AutocompleteTest"); TEST_CASE_FIXTURE(ACFixture, "empty_program") { check(" "); auto ac = autocomplete(0, 1); CHECK(!ac.entryMap.empty()); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "local_initializer") { check("local a = "); auto ac = autocomplete(0, 10); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone") { check("local a = 3.1"); auto ac = autocomplete(0, 12); CHECK(ac.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "user_defined_globals") { check("local myLocal = 4; "); auto ac = autocomplete(0, 19); CHECK(ac.entryMap.count("myLocal")); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "dont_suggest_local_before_its_definition") { check(R"( local myLocal = 4 function abc() local myInnerLocal = 1 end )"); auto ac = autocomplete(3, 0); CHECK(ac.entryMap.count("myLocal")); CHECK(!ac.entryMap.count("myInnerLocal")); ac = autocomplete(4, 0); CHECK(ac.entryMap.count("myLocal")); CHECK(ac.entryMap.count("myInnerLocal")); ac = autocomplete(6, 0); CHECK(ac.entryMap.count("myLocal")); CHECK(!ac.entryMap.count("myInnerLocal")); } TEST_CASE_FIXTURE(ACFixture, "recursive_function") { check(R"( function foo() end )"); auto ac = autocomplete(2, 0); CHECK(ac.entryMap.count("foo")); } TEST_CASE_FIXTURE(ACFixture, "nested_recursive_function") { check(R"( local function outer() local function inner() end end )"); auto ac = autocomplete(3, 0); CHECK(ac.entryMap.count("inner")); CHECK(ac.entryMap.count("outer")); } TEST_CASE_FIXTURE(ACFixture, "user_defined_local_functions_in_own_definition") { check(R"( local function abc() end )"); auto ac = autocomplete(2, 0); CHECK(ac.entryMap.count("abc")); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); check(R"( local abc = function() end )"); ac = autocomplete(2, 0); CHECK(ac.entryMap.count("abc")); // FIXME: This is actually incorrect! CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "global_functions_are_not_scoped_lexically") { check(R"( if true then function abc() end end )"); auto ac = autocomplete(6, 0); CHECK(!ac.entryMap.empty()); CHECK(ac.entryMap.count("abc")); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "local_functions_fall_out_of_scope") { check(R"( if true then local function abc() end end )"); auto ac = autocomplete(6, 0); CHECK_NE(0, ac.entryMap.size()); CHECK(!ac.entryMap.count("abc")); } TEST_CASE_FIXTURE(ACFixture, "function_parameters") { check(R"( function abc(test) end )"); auto ac = autocomplete(3, 0); CHECK(ac.entryMap.count("test")); } TEST_CASE_FIXTURE(ACFixture, "get_member_completions") { check(R"( local a = table. -- Line 1 -- | Column 23 )"); auto ac = autocomplete(1, 24); CHECK_EQ(16, ac.entryMap.size()); CHECK(ac.entryMap.count("find")); CHECK(ac.entryMap.count("pack")); CHECK(!ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "nested_member_completions") { check(R"( local tbl = { abc = { def = 1234, egh = false } } tbl.abc. )"); auto ac = autocomplete(2, 17); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("def")); CHECK(ac.entryMap.count("egh")); } TEST_CASE_FIXTURE(ACFixture, "unsealed_table") { check(R"( local tbl = {} tbl.prop = 5 tbl. )"); auto ac = autocomplete(3, 12); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("prop")); } TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2") { check(R"( local tbl = {} local inner = { prop = 5 } tbl.inner = inner tbl.inner. )"); auto ac = autocomplete(4, 19); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("prop")); } TEST_CASE_FIXTURE(ACFixture, "cyclic_table") { check(R"( local abc = {} local def = { abc = abc } abc.def = def abc.def. )"); auto ac = autocomplete(4, 17); CHECK(ac.entryMap.count("abc")); } TEST_CASE_FIXTURE(ACFixture, "table_union") { check(R"( type t1 = { a1 : string, b2 : number } type t2 = { b2 : string, c3 : string } function func(abc : t1 | t2) abc. end )"); auto ac = autocomplete(4, 18); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("b2")); } TEST_CASE_FIXTURE(ACFixture, "table_intersection") { check(R"( type t1 = { a1 : string, b2 : number } type t2 = { b2 : string, c3 : string } function func(abc : t1 & t2) abc. end )"); auto ac = autocomplete(4, 18); CHECK_EQ(3, ac.entryMap.size()); CHECK(ac.entryMap.count("a1")); CHECK(ac.entryMap.count("b2")); CHECK(ac.entryMap.count("c3")); } TEST_CASE_FIXTURE(ACFixture, "get_string_completions") { check(R"( local a = ("foo"): -- Line 1 -- | Column 26 )"); auto ac = autocomplete(1, 26); CHECK_EQ(17, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_new_statement") { check(""); auto ac = autocomplete(0, 0); CHECK_NE(0, ac.entryMap.size()); CHECK(ac.entryMap.count("table")); } TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_the_very_start_of_the_script") { check(R"( function aaa() end )"); auto ac = autocomplete(0, 0); CHECK(ac.entryMap.count("table")); } TEST_CASE_FIXTURE(ACFixture, "method_call_inside_function_body") { check(R"( local game = { GetService=function(s) return 'hello' end } function a() game: end )"); auto ac = autocomplete(4, 19); CHECK_NE(0, ac.entryMap.size()); CHECK(!ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "method_call_inside_if_conditional") { check(R"( if table: )"); auto ac = autocomplete(1, 19); CHECK_NE(0, ac.entryMap.size()); CHECK(ac.entryMap.count("concat")); CHECK(!ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "statement_between_two_statements") { check(R"( function getmyscripts() end g getmyscripts() )"); auto ac = autocomplete(3, 9); CHECK_NE(0, ac.entryMap.size()); CHECK(ac.entryMap.count("getmyscripts")); } TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope") { check(R"( local A = {one=1} function B() local A = {two=2} A end )"); auto ac = autocomplete(6, 15); CHECK(ac.entryMap.count("A")); TypeId t = follow(*ac.entryMap["A"].type); const TableTypeVar* tt = get(t); REQUIRE(tt); CHECK(tt->props.count("two")); } TEST_CASE_FIXTURE(ACFixture, "recommend_statement_starting_keywords") { check(""); auto ac = autocomplete(0, 0); CHECK(ac.entryMap.count("local")); check("local i = "); auto ac2 = autocomplete(0, 10); CHECK(!ac2.entryMap.count("local")); } TEST_CASE_FIXTURE(ACFixture, "do_not_overwrite_context_sensitive_kws") { check(R"( local function continue() end )"); auto ac = autocomplete(5, 0); AutocompleteEntry entry = ac.entryMap["continue"]; CHECK(entry.kind == AutocompleteEntryKind::Binding); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_comment") { check(R"( --!strict local foo = {} function foo:bar() end --[[ foo: ]] )"); auto ac = autocomplete(6, 16); CHECK_EQ(0, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_the_end_of_a_comment") { check(R"( --!strict )"); auto ac = autocomplete(1, 17); CHECK_EQ(0, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment") { ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true}; check(R"( --[[ )"); auto ac = autocomplete(1, 13); CHECK_EQ(0, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment_at_the_very_end_of_the_file") { ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true}; check("--[["); auto ac = autocomplete(0, 4); CHECK_EQ(0, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") { check(R"( for x = )"); auto ac1 = autocomplete(1, 14); CHECK_EQ(ac1.entryMap.count("do"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0); check(R"( for x = 1 )"); auto ac2 = autocomplete(1, 15); CHECK_EQ(ac2.entryMap.count("do"), 0); CHECK_EQ(ac2.entryMap.count("end"), 0); check(R"( for x = 1, 2 )"); auto ac3 = autocomplete(1, 18); CHECK_EQ(1, ac3.entryMap.size()); CHECK_EQ(ac3.entryMap.count("do"), 1); check(R"( for x = 1, 2, )"); auto ac4 = autocomplete(1, 19); CHECK_EQ(ac4.entryMap.count("do"), 0); CHECK_EQ(ac4.entryMap.count("end"), 0); check(R"( for x = 1, 2, 5 )"); auto ac5 = autocomplete(1, 22); CHECK_EQ(ac5.entryMap.count("do"), 1); CHECK_EQ(ac5.entryMap.count("end"), 0); check(R"( for x = 1, 2, 5 f )"); auto ac6 = autocomplete(1, 25); CHECK_EQ(ac6.entryMap.size(), 1); CHECK_EQ(ac6.entryMap.count("do"), 1); check(R"( for x = 1, 2, 5 do )"); auto ac7 = autocomplete(1, 32); CHECK_EQ(ac7.entryMap.count("end"), 1); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") { check(R"( for )"); auto ac1 = autocomplete(1, 12); CHECK_EQ(0, ac1.entryMap.size()); check(R"( for x )"); auto ac2 = autocomplete(1, 13); CHECK_EQ(0, ac2.entryMap.size()); auto ac2a = autocomplete(1, 14); CHECK_EQ(1, ac2a.entryMap.size()); CHECK_EQ(1, ac2a.entryMap.count("in")); check(R"( for x in y )"); auto ac3 = autocomplete(1, 18); CHECK_EQ(ac3.entryMap.count("table"), 1); CHECK_EQ(ac3.entryMap.count("do"), 0); check(R"( for x in y )"); auto ac4 = autocomplete(1, 19); CHECK_EQ(ac4.entryMap.size(), 1); CHECK_EQ(ac4.entryMap.count("do"), 1); check(R"( for x in f f )"); auto ac5 = autocomplete(1, 20); CHECK_EQ(ac5.entryMap.size(), 1); CHECK_EQ(ac5.entryMap.count("do"), 1); check(R"( for x in y do )"); auto ac6 = autocomplete(1, 23); CHECK_EQ(ac6.entryMap.count("in"), 0); CHECK_EQ(ac6.entryMap.count("table"), 1); CHECK_EQ(ac6.entryMap.count("end"), 1); CHECK_EQ(ac6.entryMap.count("function"), 1); check(R"( for x in y do e )"); auto ac7 = autocomplete(1, 23); CHECK_EQ(ac7.entryMap.count("in"), 0); CHECK_EQ(ac7.entryMap.count("table"), 1); CHECK_EQ(ac7.entryMap.count("end"), 1); CHECK_EQ(ac7.entryMap.count("function"), 1); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") { check(R"( while )"); auto ac1 = autocomplete(1, 13); CHECK_EQ(ac1.entryMap.count("do"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0); check(R"( while true )"); auto ac2 = autocomplete(1, 19); CHECK_EQ(1, ac2.entryMap.size()); CHECK_EQ(ac2.entryMap.count("do"), 1); check(R"( while true do )"); auto ac3 = autocomplete(1, 23); CHECK_EQ(ac3.entryMap.count("end"), 1); check(R"( while true d )"); auto ac4 = autocomplete(1, 20); CHECK_EQ(1, ac4.entryMap.size()); CHECK_EQ(ac4.entryMap.count("do"), 1); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") { check(R"( if )"); auto ac1 = autocomplete(1, 13); CHECK_EQ(ac1.entryMap.count("then"), 0); CHECK_EQ(ac1.entryMap.count("function"), 1); // FIXME: This is kind of dumb. It is technically syntactically valid but you can never do anything interesting with this. CHECK_EQ(ac1.entryMap.count("table"), 1); CHECK_EQ(ac1.entryMap.count("else"), 0); CHECK_EQ(ac1.entryMap.count("elseif"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0); check(R"( if x )"); auto ac2 = autocomplete(1, 14); CHECK_EQ(ac2.entryMap.count("then"), 1); CHECK_EQ(ac2.entryMap.count("function"), 0); CHECK_EQ(ac2.entryMap.count("else"), 0); CHECK_EQ(ac2.entryMap.count("elseif"), 0); CHECK_EQ(ac2.entryMap.count("end"), 0); check(R"( if x t )"); auto ac3 = autocomplete(1, 14); CHECK_EQ(1, ac3.entryMap.size()); CHECK_EQ(ac3.entryMap.count("then"), 1); check(R"( if x then end )"); auto ac4 = autocomplete(2, 0); CHECK_EQ(ac4.entryMap.count("then"), 0); CHECK_EQ(ac4.entryMap.count("else"), 1); CHECK_EQ(ac4.entryMap.count("function"), 1); CHECK_EQ(ac4.entryMap.count("elseif"), 1); CHECK_EQ(ac4.entryMap.count("end"), 0); check(R"( if x then t end )"); auto ac4a = autocomplete(2, 13); CHECK_EQ(ac4a.entryMap.count("then"), 0); CHECK_EQ(ac4a.entryMap.count("table"), 1); CHECK_EQ(ac4a.entryMap.count("else"), 1); CHECK_EQ(ac4a.entryMap.count("elseif"), 1); check(R"( if x then elseif x then end )"); auto ac5 = autocomplete(2, 0); CHECK_EQ(ac5.entryMap.count("then"), 0); CHECK_EQ(ac5.entryMap.count("function"), 1); CHECK_EQ(ac5.entryMap.count("else"), 0); CHECK_EQ(ac5.entryMap.count("elseif"), 0); CHECK_EQ(ac5.entryMap.count("end"), 0); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat") { check(R"( repeat )"); auto ac = autocomplete(1, 16); CHECK_EQ(ac.entryMap.count("table"), 1); CHECK_EQ(ac.entryMap.count("until"), 1); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression") { check(R"( repeat until )"); auto ac = autocomplete(2, 16); CHECK_EQ(ac.entryMap.count("table"), 1); } TEST_CASE_FIXTURE(ACFixture, "local_names") { check(R"( local ab )"); auto ac1 = autocomplete(1, 16); CHECK_EQ(ac1.entryMap.size(), 1); CHECK_EQ(ac1.entryMap.count("function"), 1); check(R"( local ab, cd )"); auto ac2 = autocomplete(1, 20); CHECK(ac2.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_fn_exprs") { check(R"( local function f() )"); auto ac = autocomplete(1, 28); CHECK_EQ(ac.entryMap.count("end"), 1); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda") { check(R"( local a = function() local bar = foo en )"); auto ac = autocomplete(1, 47); CHECK_EQ(ac.entryMap.count("end"), 1); } TEST_CASE_FIXTURE(ACFixture, "stop_at_first_stat_when_recommending_keywords") { check(R"( repeat for x )"); auto ac1 = autocomplete(2, 18); CHECK_EQ(ac1.entryMap.count("in"), 1); CHECK_EQ(ac1.entryMap.count("until"), 0); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_repeat_middle_keyword") { check(R"( repeat )"); auto ac1 = autocomplete(1, 15); CHECK_EQ(ac1.entryMap.count("do"), 1); CHECK_EQ(ac1.entryMap.count("function"), 1); CHECK_EQ(ac1.entryMap.count("until"), 1); check(R"( repeat f f )"); auto ac2 = autocomplete(1, 18); CHECK_EQ(ac2.entryMap.count("function"), 1); CHECK_EQ(ac2.entryMap.count("until"), 1); check(R"( repeat u until )"); auto ac3 = autocomplete(2, 13); CHECK_EQ(ac3.entryMap.count("until"), 0); } TEST_CASE_FIXTURE(ACFixture, "local_function") { check(R"( local f )"); auto ac1 = autocomplete(1, 15); CHECK_EQ(ac1.entryMap.size(), 1); CHECK_EQ(ac1.entryMap.count("function"), 1); check(R"( local f, cd )"); auto ac2 = autocomplete(1, 15); CHECK(ac2.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "local_function") { check(R"( local function )"); auto ac = autocomplete(1, 23); CHECK(ac.entryMap.empty()); check(R"( local function s )"); ac = autocomplete(1, 23); CHECK(ac.entryMap.empty()); ac = autocomplete(1, 24); CHECK(ac.entryMap.empty()); check(R"( local function () )"); ac = autocomplete(1, 23); CHECK(ac.entryMap.empty()); ac = autocomplete(1, 25); CHECK(ac.entryMap.count("end")); check(R"( local function something )"); ac = autocomplete(1, 32); CHECK(ac.entryMap.empty()); check(R"( local tbl = {} function tbl.something() end )"); ac = autocomplete(2, 30); CHECK(ac.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "local_function_params") { check(R"( local function abc(def) )"); CHECK(autocomplete(1, 23).entryMap.empty()); CHECK(autocomplete(1, 24).entryMap.empty()); CHECK(autocomplete(1, 27).entryMap.empty()); CHECK(autocomplete(1, 28).entryMap.empty()); CHECK(!autocomplete(1, 31).entryMap.empty()); CHECK(!autocomplete(1, 32).entryMap.empty()); check(R"( local function abc(def) end )"); for (unsigned int i = 23; i < 31; ++i) { CHECK(autocomplete(1, i).entryMap.empty()); } CHECK(!autocomplete(1, 32).entryMap.empty()); auto ac2 = autocomplete(2, 0); CHECK_EQ(ac2.entryMap.count("abc"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1); check(R"( local function abc(def, ghi) end )"); auto ac3 = autocomplete(1, 35); CHECK(ac3.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "global_function_params") { check(R"( function abc(def) )"); for (unsigned int i = 17; i < 25; ++i) { CHECK(autocomplete(1, i).entryMap.empty()); } CHECK(!autocomplete(1, 26).entryMap.empty()); check(R"( function abc(def) end )"); for (unsigned int i = 17; i < 25; ++i) { CHECK(autocomplete(1, i).entryMap.empty()); } CHECK(!autocomplete(1, 26).entryMap.empty()); check(R"( function abc(def) end )"); auto ac2 = autocomplete(2, 0); CHECK_EQ(ac2.entryMap.count("abc"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1); check(R"( function abc(def, ghi) end )"); auto ac3 = autocomplete(1, 29); CHECK(ac3.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "arguments_to_global_lambda") { check(R"( abc = function(def, ghi) end )"); auto ac = autocomplete(1, 31); CHECK(ac.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "function_expr_params") { check(R"( abc = function(def) )"); for (unsigned int i = 20; i < 27; ++i) { CHECK(autocomplete(1, i).entryMap.empty()); } CHECK(!autocomplete(1, 28).entryMap.empty()); check(R"( abc = function(def) end )"); for (unsigned int i = 20; i < 27; ++i) { CHECK(autocomplete(1, i).entryMap.empty()); } CHECK(!autocomplete(1, 28).entryMap.empty()); check(R"( abc = function(def) end )"); auto ac2 = autocomplete(2, 0); CHECK_EQ(ac2.entryMap.count("def"), 1); } TEST_CASE_FIXTURE(ACFixture, "local_initializer") { check(R"( local a = t )"); auto ac = autocomplete(1, 19); CHECK_EQ(ac.entryMap.count("table"), 1); CHECK_EQ(ac.entryMap.count("true"), 1); } TEST_CASE_FIXTURE(ACFixture, "local_initializer_2") { check(R"( local a= )"); auto ac = autocomplete(1, 16); CHECK(ac.entryMap.count("table")); } TEST_CASE_FIXTURE(ACFixture, "get_member_completions") { check(R"( local a = 12.3 )"); auto ac = autocomplete(1, 21); CHECK(ac.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "sometimes_the_metatable_is_an_error") { check(R"( local T = {} T.__index = T function T.new() return setmetatable({x=6}, X) -- oops! end local t = T.new() t. )"); autocomplete(8, 12); // Don't crash! } TEST_CASE_FIXTURE(ACFixture, "local_types_builtin") { check(R"( local a: n local b: string = "don't trip" )"); auto ac = autocomplete(1, 10); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); } TEST_CASE_FIXTURE(ACFixture, "private_types") { check(R"( do type num = number local a: nu local b: num end local a: nu )"); auto ac = autocomplete(3, 14); CHECK(ac.entryMap.count("num")); CHECK(ac.entryMap.count("number")); ac = autocomplete(4, 15); CHECK(ac.entryMap.count("num")); CHECK(ac.entryMap.count("number")); ac = autocomplete(6, 11); CHECK(!ac.entryMap.count("num")); CHECK(ac.entryMap.count("number")); } TEST_CASE_FIXTURE(ACFixture, "type_scoping_easy") { check(R"( type Table = { a: number, b: number } do type Table = { x: string, y: string } local a: T end )"); auto ac = autocomplete(4, 14); REQUIRE(ac.entryMap.count("Table")); REQUIRE(ac.entryMap["Table"].type); const TableTypeVar* tv = get(follow(*ac.entryMap["Table"].type)); REQUIRE(tv); CHECK(tv->props.count("x")); } TEST_CASE_FIXTURE(ACFixture, "modules_with_types") { fileResolver.source["Module/A"] = R"( export type A = { x: number, y: number } export type B = { z: number, w: number } return {} )"; LUAU_REQUIRE_NO_ERRORS(frontend.check("Module/A")); fileResolver.source["Module/B"] = R"( local aaa = require(script.Parent.A) local a: aa )"; frontend.check("Module/B"); auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback); CHECK(ac.entryMap.count("aaa")); } TEST_CASE_FIXTURE(ACFixture, "module_type_members") { fileResolver.source["Module/A"] = R"( export type A = { x: number, y: number } export type B = { z: number, w: number } return {} )"; LUAU_REQUIRE_NO_ERRORS(frontend.check("Module/A")); fileResolver.source["Module/B"] = R"( local aaa = require(script.Parent.A) local a: aaa. )"; frontend.check("Module/B"); auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 13}, nullCallback); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("A")); CHECK(ac.entryMap.count("B")); } TEST_CASE_FIXTURE(ACFixture, "argument_types") { check(R"( local function f(a: n local b: string = "don't trip" )"); auto ac = autocomplete(1, 21); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); } TEST_CASE_FIXTURE(ACFixture, "return_types") { check(R"( local function f(a: number): n local b: string = "don't trip" )"); auto ac = autocomplete(1, 30); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); } TEST_CASE_FIXTURE(ACFixture, "as_types") { check(R"( local a: any = 5 local b: number = (a :: n )"); auto ac = autocomplete(2, 25); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); } TEST_CASE_FIXTURE(ACFixture, "function_type_types") { check(R"( local a: (n local b: (number, (n local c: (number, (number) -> n local d: (number, (number) -> (number, n local e: (n: n )"); auto ac = autocomplete(1, 11); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); ac = autocomplete(2, 20); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); ac = autocomplete(3, 31); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); ac = autocomplete(4, 40); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); ac = autocomplete(5, 14); CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); } TEST_CASE_FIXTURE(ACFixture, "generic_types") { ScopedFastFlag luauParseGenericFunctions("LuauParseGenericFunctions", true); ScopedFastFlag luauGenericFunctions("LuauGenericFunctions", true); check(R"( function f(a: T local b: string = "don't trip" )"); auto ac = autocomplete(1, 25); CHECK(ac.entryMap.count("Tee")); } TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_in_argument") { // local check(R"( local function target(a: number, b: string) return a + #b end local one = 4 local two = "hello" return target(o )"); auto ac = autocomplete(5, 15); CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); check(R"( local function target(a: number, b: string) return a + #b end local one = 4 local two = "hello" return target(one, t )"); ac = autocomplete(5, 20); CHECK(ac.entryMap.count("two")); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None); // member check(R"( local function target(a: number, b: string) return a + #b end local a = { one = 4, two = "hello" } return target(a. )"); ac = autocomplete(4, 16); CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); check(R"( local function target(a: number, b: string) return a + #b end local a = { one = 4, two = "hello" } return target(a.one, a. )"); ac = autocomplete(4, 23); CHECK(ac.entryMap.count("two")); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None); // union match check(R"( local function target(a: string?) return #b end local a = { one = 4, two = "hello" } return target(a. )"); ac = autocomplete(4, 16); CHECK(ac.entryMap.count("two")); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None); } TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_in_table") { check(R"( type Foo = { a: number, b: string } local a = { one = 4, two = "hello" } local b: Foo = { a = a. )"); auto ac = autocomplete(3, 23); CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); check(R"( type Foo = { a: number, b: string } local a = { one = 4, two = "hello" } local b: Foo = { b = a. )"); ac = autocomplete(3, 23); CHECK(ac.entryMap.count("two")); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None); } TEST_CASE_FIXTURE(ACFixture, "type_correct_function_return_types") { check(R"( local function target(a: number, b: string) return a + #b end local function bar1(a: number) return -a end local function bar2(a: string) reutrn a .. 'x' end return target(b )"); auto ac = autocomplete(5, 15); CHECK(ac.entryMap.count("bar1")); CHECK(ac.entryMap["bar1"].typeCorrect == TypeCorrectKind::CorrectFunctionResult); CHECK(ac.entryMap["bar2"].typeCorrect == TypeCorrectKind::None); check(R"( local function target(a: number, b: string) return a + #b end local function bar1(a: number) return -a end local function bar2(a: string) return a .. 'x' end return target(bar1, b )"); ac = autocomplete(5, 21); CHECK(ac.entryMap.count("bar2")); CHECK(ac.entryMap["bar2"].typeCorrect == TypeCorrectKind::CorrectFunctionResult); CHECK(ac.entryMap["bar1"].typeCorrect == TypeCorrectKind::None); check(R"( local function target(a: number, b: string) return a + #b end local function bar1(a: number): (...number) return -a, a end local function bar2(a: string) reutrn a .. 'x' end return target(b )"); ac = autocomplete(5, 15); CHECK(ac.entryMap.count("bar1")); CHECK(ac.entryMap["bar1"].typeCorrect == TypeCorrectKind::CorrectFunctionResult); CHECK(ac.entryMap["bar2"].typeCorrect == TypeCorrectKind::None); } TEST_CASE_FIXTURE(ACFixture, "type_correct_local_type_suggestion") { check(R"( local b: s = "str" )"); auto ac = autocomplete(1, 10); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function f() return "str" end local b: s = f() )"); ac = autocomplete(2, 10); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local b: s, c: n = "str", 2 )"); ac = autocomplete(1, 10); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(1, 16); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function f() return 1, "str", 3 end local a: b, b: n, c: s, d: n = false, f() )"); ac = autocomplete(2, 10); CHECK(ac.entryMap.count("boolean")); CHECK(ac.entryMap["boolean"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(2, 16); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(2, 22); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(2, 28); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function f(): ...number return 1, 2, 3 end local a: boolean, b: n = false, f() )"); ac = autocomplete(2, 22); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_function_type_suggestion") { check(R"( local b: (n) -> number = function(a: number, b: string) return a + #b end )"); auto ac = autocomplete(1, 11); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local b: (number, s = function(a: number, b: string) return a + #b end )"); ac = autocomplete(1, 19); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local b: (number, string) -> b = function(a: number, b: string): boolean return a + #b == 0 end )"); ac = autocomplete(1, 30); CHECK(ac.entryMap.count("boolean")); CHECK(ac.entryMap["boolean"].typeCorrect == TypeCorrectKind::Correct); check(R"( local b: (number, ...s) = function(a: number, ...: string) return a end )"); ac = autocomplete(1, 22); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local b: (number) -> ...s = function(a: number): ...string return "a", "b", "c" end )"); ac = autocomplete(1, 25); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_full_type_suggestion") { check(R"( local b: = "str" )"); auto ac = autocomplete(1, 8); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(1, 9); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local b: = function(a: number) return -a end )"); ac = autocomplete(1, 9); CHECK(ac.entryMap.count("(number) -> number")); CHECK(ac.entryMap["(number) -> number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_argument_type_suggestion") { check(R"( local function target(a: number, b: string) return a + #b end local function d(a: n, b) return target(a, b) end )"); auto ac = autocomplete(3, 21); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(a: number, b: string) return a + #b end local function d(a, b: s) return target(a, b) end )"); ac = autocomplete(3, 24); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(a: number, b: string) return a + #b end local function d(a: , b) return target(a, b) end )"); ac = autocomplete(3, 19); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(3, 20); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(a: number, b: string) return a + #b end local function d(a, b: ): number return target(a, b) end )"); ac = autocomplete(3, 23); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(3, 24); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::None); } TEST_CASE_FIXTURE(ACFixture, "type_correct_expected_argument_type_suggestion") { check(R"( local function target(callback: (a: number, b: string) -> number) return callback(4, "hello") end local x = target(function(a: )"); auto ac = autocomplete(3, 29); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(callback: (a: number, b: string) -> number) return callback(4, "hello") end local x = target(function(a: n )"); ac = autocomplete(3, 30); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(callback: (a: number, b: string) -> number) return callback(4, "hello") end local x = target(function(a: n, b: ) return a + #b end) )"); ac = autocomplete(3, 30); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(3, 35); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(callback: (...number) -> number) return callback(1, 2, 3) end local x = target(function(a: n) return a end )"); ac = autocomplete(3, 30); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_expected_argument_type_pack_suggestion") { check(R"( local function target(callback: (...number) -> number) return callback(1, 2, 3) end local x = target(function(...:n) return a end )"); auto ac = autocomplete(3, 31); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(callback: (...number) -> number) return callback(1, 2, 3) end local x = target(function(a:number, b:number, ...:) return a + b end )"); ac = autocomplete(3, 50); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_expected_return_type_suggestion") { check(R"( local function target(callback: () -> number) return callback() end local x = target(function(): n return 1 end )"); auto ac = autocomplete(3, 30); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(callback: () -> (number, number)) return callback() end local x = target(function(): (number, n return 1, 2 end )"); ac = autocomplete(3, 39); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_expected_return_type_pack_suggestion") { check(R"( local function target(callback: () -> ...number) return callback() end local x = target(function(): ...n return 1, 2, 3 end )"); auto ac = autocomplete(3, 33); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); check(R"( local function target(callback: () -> ...number) return callback() end local x = target(function(): (number, number, ...n return 1, 2, 3 end )"); ac = autocomplete(3, 50); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_expected_argument_type_suggestion_optional") { check(R"( local function target(callback: nil | (a: number, b: string) -> number) return callback(4, "hello") end local x = target(function(a: )"); auto ac = autocomplete(3, 29); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_expected_argument_type_suggestion_self") { check(R"( local t = {} t.x = 5 function t:target(callback: (a: number, b: string) -> number) return callback(self.x, "hello") end local x = t:target(function(a: , b: ) end) local y = t.target(t, function(a: number, b: ) end) )"); auto ac = autocomplete(5, 31); CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap["number"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(5, 35); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(6, 45); CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap["string"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "do_not_suggest_internal_module_type") { fileResolver.source["Module/A"] = R"( type done = { x: number, y: number } local function a(a: (done) -> number) return a({x=1, y=2}) end local function b(a: ((done) -> number) -> number) return a(function(done) return 1 end) end return {a = a, b = b} )"; LUAU_REQUIRE_NO_ERRORS(frontend.check("Module/A")); fileResolver.source["Module/B"] = R"( local ex = require(script.Parent.A) ex.a(function(x: )"; frontend.check("Module/B"); auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 16}, nullCallback); CHECK(!ac.entryMap.count("done")); fileResolver.source["Module/C"] = R"( local ex = require(script.Parent.A) ex.b(function(x: )"; frontend.check("Module/C"); ac = Luau::autocomplete(frontend, "Module/C", Position{2, 16}, nullCallback); CHECK(!ac.entryMap.count("(done) -> number")); } TEST_CASE_FIXTURE(ACFixture, "suggest_external_module_type") { fileResolver.source["Module/A"] = R"( export type done = { x: number, y: number } local function a(a: (done) -> number) return a({x=1, y=2}) end local function b(a: ((done) -> number) -> number) return a(function(done) return 1 end) end return {a = a, b = b} )"; LUAU_REQUIRE_NO_ERRORS(frontend.check("Module/A")); fileResolver.source["Module/B"] = R"( local ex = require(script.Parent.A) ex.a(function(x: )"; frontend.check("Module/B"); auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 16}, nullCallback); CHECK(!ac.entryMap.count("done")); CHECK(ac.entryMap.count("ex.done")); CHECK(ac.entryMap["ex.done"].typeCorrect == TypeCorrectKind::Correct); fileResolver.source["Module/C"] = R"( local ex = require(script.Parent.A) ex.b(function(x: )"; frontend.check("Module/C"); ac = Luau::autocomplete(frontend, "Module/C", Position{2, 16}, nullCallback); CHECK(!ac.entryMap.count("(done) -> number")); CHECK(ac.entryMap.count("(ex.done) -> number")); CHECK(ac.entryMap["(ex.done) -> number"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "do_not_suggest_synthetic_table_name") { check(R"( local foo = { a = 1, b = 2 } local bar: = foo )"); auto ac = autocomplete(2, 11); CHECK(!ac.entryMap.count("foo")); } // CLI-45692: Remove UnfrozenFixture here TEST_CASE_FIXTURE(UnfrozenFixture, "type_correct_function_no_parenthesis") { check(R"( local function target(a: (number) -> number) return a(4) end local function bar1(a: number) return -a end local function bar2(a: string) reutrn a .. 'x' end return target(b )"); auto ac = autocomplete(frontend, "MainModule", Position{5, 15}, nullCallback); CHECK(ac.entryMap.count("bar1")); CHECK(ac.entryMap["bar1"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["bar1"].parens == ParenthesesRecommendation::None); CHECK(ac.entryMap["bar2"].typeCorrect == TypeCorrectKind::None); } TEST_CASE_FIXTURE(ACFixture, "type_correct_sealed_table") { check(R"( local function f(a: { x: number, y: number }) return a.x + a.y end local fp: = f )"); auto ac = autocomplete(2, 10); CHECK(ac.entryMap.count("({ x: number, y: number }) -> number")); } // CLI-45692: Remove UnfrozenFixture here TEST_CASE_FIXTURE(UnfrozenFixture, "type_correct_keywords") { check(R"( local function a(x: boolean) end local function b(x: number?) end local function c(x: (number) -> string) end local function d(x: ((number) -> string)?) end local function e(x: ((number) -> string) & ((boolean) -> number)) end local tru = {} local ni = false local ac = a(t) local bc = b(n) local cc = c(f) local dc = d(f) local ec = e(f) )"); auto ac = autocomplete(frontend, "MainModule", Position{10, 14}, nullCallback); CHECK(ac.entryMap.count("tru")); CHECK(ac.entryMap["tru"].typeCorrect == TypeCorrectKind::None); CHECK(ac.entryMap["true"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["false"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(frontend, "MainModule", Position{11, 14}, nullCallback); CHECK(ac.entryMap.count("ni")); CHECK(ac.entryMap["ni"].typeCorrect == TypeCorrectKind::None); CHECK(ac.entryMap["nil"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(frontend, "MainModule", Position{12, 14}, nullCallback); CHECK(ac.entryMap.count("false")); CHECK(ac.entryMap["false"].typeCorrect == TypeCorrectKind::None); CHECK(ac.entryMap["function"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(frontend, "MainModule", Position{13, 14}, nullCallback); CHECK(ac.entryMap["function"].typeCorrect == TypeCorrectKind::Correct); ac = autocomplete(frontend, "MainModule", Position{14, 14}, nullCallback); CHECK(ac.entryMap["function"].typeCorrect == TypeCorrectKind::Correct); } TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_for_overloads") { check(R"( local target: ((number) -> string) & ((string) -> number)) local one = 4 local two = "hello" return target(o) )"); auto ac = autocomplete(5, 15); CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); check(R"( local target: ((number) -> string) & ((number) -> number)) local one = 4 local two = "hello" return target(o) )"); ac = autocomplete(5, 15); CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); check(R"( local target: ((number, number) -> string) & ((string) -> number)) local one = 4 local two = "hello" return target(1, o) )"); ac = autocomplete(5, 18); CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); } TEST_CASE_FIXTURE(ACFixture, "optional_members") { check(R"( local a = { x = 2, y = 3 } type A = typeof(a) local b: A? = a return b. )"); auto ac = autocomplete(4, 9); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("x")); CHECK(ac.entryMap.count("y")); check(R"( local a = { x = 2, y = 3 } type A = typeof(a) local b: nil | A = a return b. )"); ac = autocomplete(4, 9); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("x")); CHECK(ac.entryMap.count("y")); check(R"( local b: nil | nil return b. )"); ac = autocomplete(2, 9); CHECK_EQ(0, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "no_function_name_suggestions") { check(R"( function na )"); auto ac = autocomplete(1, 11); CHECK(ac.entryMap.empty()); check(R"( local function )"); ac = autocomplete(1, 15); CHECK(ac.entryMap.empty()); check(R"( local function na )"); ac = autocomplete(1, 17); CHECK(ac.entryMap.empty()); } TEST_CASE_FIXTURE(ACFixture, "skip_current_local") { check(R"( local other = 1 local name = na )"); auto ac = autocomplete(2, 15); CHECK(!ac.entryMap.count("name")); CHECK(ac.entryMap.count("other")); check(R"( local other = 1 local name, test = na )"); ac = autocomplete(2, 21); CHECK(!ac.entryMap.count("name")); CHECK(!ac.entryMap.count("test")); CHECK(ac.entryMap.count("other")); } TEST_CASE_FIXTURE(ACFixture, "keyword_members") { check(R"( local a = { done = 1, forever = 2 } local b = a.do local c = a.for local d = a. do end )"); auto ac = autocomplete(2, 14); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("done")); CHECK(ac.entryMap.count("forever")); ac = autocomplete(3, 15); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("done")); CHECK(ac.entryMap.count("forever")); ac = autocomplete(4, 12); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("done")); CHECK(ac.entryMap.count("forever")); } TEST_CASE_FIXTURE(ACFixture, "keyword_methods") { check(R"( local a = {} function a:done() end local b = a:do )"); auto ac = autocomplete(3, 14); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("done")); } TEST_CASE_FIXTURE(ACFixture, "keyword_types") { fileResolver.source["Module/A"] = R"( export type done = { x: number, y: number } export type other = { z: number, w: number } return {} )"; LUAU_REQUIRE_NO_ERRORS(frontend.check("Module/A")); fileResolver.source["Module/B"] = R"( local aaa = require(script.Parent.A) local a: aaa.do )"; frontend.check("Module/B"); auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 15}, nullCallback); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("done")); CHECK(ac.entryMap.count("other")); } TEST_CASE_FIXTURE(ACFixture, "autocompleteSource") { std::string_view source = R"( local a = table. -- Line 1 -- | Column 23 )"; auto ac = autocompleteSource(frontend, source, Position{1, 24}, nullCallback).result; CHECK_EQ(16, ac.entryMap.size()); CHECK(ac.entryMap.count("find")); CHECK(ac.entryMap.count("pack")); CHECK(!ac.entryMap.count("math")); } TEST_CASE_FIXTURE(ACFixture, "autocompleteSource_require") { ScopedFastFlag luauResolveModuleNameWithoutACurrentModule("LuauResolveModuleNameWithoutACurrentModule", true); std::string_view source = R"( local a = require(w -- Line 1 -- | Column 27 )"; // CLI-43699 require shouldn't crash inside autocompleteSource auto ac = autocompleteSource(frontend, source, Position{1, 27}, nullCallback).result; } TEST_CASE_FIXTURE(ACFixture, "autocompleteSource_comments") { std::string_view source = "--!str"; auto ac = autocompleteSource(frontend, source, Position{0, 6}, nullCallback).result; CHECK_EQ(0, ac.entryMap.size()); } TEST_CASE_FIXTURE(ACFixture, "autocompleteProp_index_function_metamethod_is_variadic") { std::string_view source = R"( type Foo = {x: number} local t = {} setmetatable(t, { __index = function(index: string): ...Foo return {x = 1}, {x = 2} end }) local a = t. -- Line 9 -- | Column 20 )"; auto ac = autocompleteSource(frontend, source, Position{9, 20}, nullCallback).result; REQUIRE_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("x")); } TEST_CASE_FIXTURE(ACFixture, "if_then_else_full_keywords") { check(R"( local thenceforth = false local elsewhere = false local doover = false local endurance = true if 1 then else end while false do end repeat until )"); auto ac = autocomplete(6, 9); CHECK(ac.entryMap.size() == 1); CHECK(ac.entryMap.count("then")); ac = autocomplete(7, 4); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); ac = autocomplete(10, 14); CHECK(ac.entryMap.count("do")); ac = autocomplete(13, 6); CHECK(ac.entryMap.count("do")); // FIXME: ideally we want to handle start and end of all statements as well } TEST_CASE_FIXTURE(ACFixture, "if_then_else_elseif_completions") { ScopedFastFlag sff{"ElseElseIfCompletionImprovements", true}; check(R"( local elsewhere = false if true then return 1 el end )"); auto ac = autocomplete(5, 2); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); CHECK(ac.entryMap.count("elsewhere") == 0); check(R"( local elsewhere = false if true then return 1 else return 2 el end )"); ac = autocomplete(7, 2); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elsewhere")); check(R"( local elsewhere = false if true then print("1") elif true then print("2") el end )"); ac = autocomplete(7, 2); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); CHECK(ac.entryMap.count("elsewhere")); } TEST_CASE_FIXTURE(ACFixture, "autocompleteSource_not_the_var_we_are_defining") { std::string_view source = "abc,de"; auto ac = autocompleteSource(frontend, source, Position{0, 6}, nullCallback).result; CHECK(!ac.entryMap.count("de")); } TEST_CASE_FIXTURE(ACFixture, "autocompleteSource_recursive_function") { { std::string_view global = R"(function abc() end )"; auto ac = autocompleteSource(frontend, global, Position{1, 0}, nullCallback).result; CHECK(ac.entryMap.count("abc")); } { std::string_view local = R"(local function abc() end )"; auto ac = autocompleteSource(frontend, local, Position{1, 0}, nullCallback).result; CHECK(ac.entryMap.count("abc")); } } TEST_CASE_FIXTURE(ACFixture, "suggest_table_keys") { check(R"( type Test = { first: number, second: number } local t: Test = { f } )"); auto ac = autocomplete(2, 19); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); // Intersection check(R"( type Test = { first: number } & { second: number } local t: Test = { f } )"); ac = autocomplete(2, 19); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); // Union check(R"( type Test = { first: number, second: number } | { second: number, third: number } local t: Test = { s } )"); ac = autocomplete(2, 19); CHECK(ac.entryMap.count("second")); CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("third")); // No parenthesis suggestion check(R"( type Test = { first: (number) -> number, second: number } local t: Test = { f } )"); ac = autocomplete(2, 19); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap["first"].parens == ParenthesesRecommendation::None); // When key is changed check(R"( type Test = { first: number, second: number } local t: Test = { f = 2 } )"); ac = autocomplete(2, 19); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); // Alternative key syntax check(R"( type Test = { first: number, second: number } local t: Test = { ["f"] } )"); ac = autocomplete(2, 21); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); // Not an alternative key syntax check(R"( type Test = { first: number, second: number } local t: Test = { "f" } )"); ac = autocomplete(2, 20); CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("second")); // Skip keys that are already defined check(R"( type Test = { first: number, second: number } local t: Test = { first = 2, s } )"); ac = autocomplete(2, 30); CHECK(!ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); // Don't skip active key check(R"( type Test = { first: number, second: number } local t: Test = { first } )"); ac = autocomplete(2, 23); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); // Inference after first key check(R"( local t = { { first = 5, second = 10 }, { f } } )"); ac = autocomplete(3, 7); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); check(R"( local t = { [2] = { first = 5, second = 10 }, [5] = { f } } )"); ac = autocomplete(3, 13); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); } TEST_CASE_FIXTURE(Fixture, "autocomplete_documentation_symbols") { loadDefinition(R"( declare y: { x: number, } )"); fileResolver.source["Module/A"] = R"( local a = y. )"; frontend.check("Module/A"); auto ac = autocomplete(frontend, "Module/A", Position{1, 21}, nullCallback); REQUIRE(ac.entryMap.count("x")); CHECK_EQ(ac.entryMap["x"].documentationSymbol, "@test/global/y.x"); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_ifelse_expressions") { ScopedFastFlag sff1{"LuauIfElseExpressionBaseSupport", true}; ScopedFastFlag sff2{"LuauIfElseExpressionAnalysisSupport", true}; { check(R"( local temp = false local even = true; local a = true a = if t1@emp then t a = if temp t2@ a = if temp then e3@ a = if temp then even e4@ a = if temp then even elseif t5@ a = if temp then even elseif true t6@ a = if temp then even elseif true then t7@ a = if temp then even elseif true then temp e8@ a = if temp then even elseif true then temp else e9@ )"); auto ac = autocomplete('1'); CHECK(ac.entryMap.count("temp")); CHECK(ac.entryMap.count("true")); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); ac = autocomplete('2'); CHECK(ac.entryMap.count("temp") == 0); CHECK(ac.entryMap.count("true") == 0); CHECK(ac.entryMap.count("then")); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); ac = autocomplete('3'); CHECK(ac.entryMap.count("even")); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); ac = autocomplete('4'); CHECK(ac.entryMap.count("even") == 0); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); ac = autocomplete('5'); CHECK(ac.entryMap.count("temp")); CHECK(ac.entryMap.count("true")); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); ac = autocomplete('6'); CHECK(ac.entryMap.count("temp") == 0); CHECK(ac.entryMap.count("true") == 0); CHECK(ac.entryMap.count("then")); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); ac = autocomplete('7'); CHECK(ac.entryMap.count("temp")); CHECK(ac.entryMap.count("true")); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); ac = autocomplete('8'); CHECK(ac.entryMap.count("even") == 0); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); ac = autocomplete('9'); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); } } TEST_SUITE_END();