mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-12 21:10:37 +00:00
e491128f95
## What's new * Added `math.map` function to the standard library, based on https://rfcs.luau-lang.org/function-math-map.html * `FileResolver` can provide an implementation of `getRequireSuggestions` to provide auto-complete suggestions for require-by-string ## New Solver * In user-defined type functions, `readproperty` and `writeproperty` will return `nil` instead of erroring if property is not found * Fixed incorrect scope of variadic arguments in the data-flow graph * Fixed multiple assertion failures --- Internal Contributors: Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
451 lines
12 KiB
C++
451 lines
12 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
|
|
#include "Repl.h"
|
|
#include "ScopedFlags.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
LUAU_FASTFLAG(LuauMathMap)
|
|
|
|
struct Completion
|
|
{
|
|
std::string completion;
|
|
std::string display;
|
|
|
|
bool operator<(Completion const& other) const
|
|
{
|
|
return std::tie(completion, display) < std::tie(other.completion, other.display);
|
|
}
|
|
};
|
|
|
|
using CompletionSet = std::set<Completion>;
|
|
|
|
class ReplFixture
|
|
{
|
|
public:
|
|
ReplFixture()
|
|
: luaState(luaL_newstate(), lua_close)
|
|
{
|
|
L = luaState.get();
|
|
setupState(L);
|
|
luaL_sandboxthread(L);
|
|
|
|
std::string result = runCode(L, prettyPrintSource);
|
|
}
|
|
|
|
// Returns all of the output captured from the pretty printer
|
|
std::string getCapturedOutput()
|
|
{
|
|
lua_getglobal(L, "capturedoutput");
|
|
const char* str = lua_tolstring(L, -1, nullptr);
|
|
std::string result(str);
|
|
lua_pop(L, 1);
|
|
return result;
|
|
}
|
|
|
|
CompletionSet getCompletionSet(const char* inputPrefix)
|
|
{
|
|
CompletionSet result;
|
|
int top = lua_gettop(L);
|
|
getCompletions(
|
|
L,
|
|
inputPrefix,
|
|
[&result](const std::string& completion, const std::string& display)
|
|
{
|
|
result.insert(Completion{completion, display});
|
|
}
|
|
);
|
|
// Ensure that generating completions doesn't change the position of luau's stack top.
|
|
CHECK(top == lua_gettop(L));
|
|
|
|
return result;
|
|
}
|
|
|
|
bool checkCompletion(const CompletionSet& completions, const std::string& prefix, const std::string& expected)
|
|
{
|
|
std::string expectedDisplay(expected.substr(0, expected.find_first_of('(')));
|
|
Completion expectedCompletion{prefix + expected, expectedDisplay};
|
|
return completions.count(expectedCompletion) == 1;
|
|
}
|
|
|
|
lua_State* L;
|
|
|
|
private:
|
|
std::unique_ptr<lua_State, void (*)(lua_State*)> luaState;
|
|
|
|
// This is a simplistic and incomplete pretty printer.
|
|
// It is included here to test that the pretty printer hook is being called.
|
|
// More elaborate tests to ensure correct output can be added if we introduce
|
|
// a more feature rich pretty printer.
|
|
std::string prettyPrintSource = R"(
|
|
-- Accumulate pretty printer output in `capturedoutput`
|
|
capturedoutput = ""
|
|
|
|
function arraytostring(arr)
|
|
local strings = {}
|
|
table.foreachi(arr, function(k,v) table.insert(strings, pptostring(v)) end )
|
|
return "{" .. table.concat(strings, ", ") .. "}"
|
|
end
|
|
|
|
function pptostring(x)
|
|
if type(x) == "table" then
|
|
-- Just assume array-like tables for now.
|
|
return arraytostring(x)
|
|
elseif type(x) == "string" then
|
|
return '"' .. x .. '"'
|
|
else
|
|
return tostring(x)
|
|
end
|
|
end
|
|
|
|
-- Note: Instead of calling print, the pretty printer just stores the output
|
|
-- in `capturedoutput` so we can check for the correct results.
|
|
function _PRETTYPRINT(...)
|
|
local args = table.pack(...)
|
|
local strings = {}
|
|
for i=1, args.n do
|
|
local item = args[i]
|
|
local str = pptostring(item, customoptions)
|
|
if i == 1 then
|
|
capturedoutput = capturedoutput .. str
|
|
else
|
|
capturedoutput = capturedoutput .. "\t" .. str
|
|
end
|
|
end
|
|
end
|
|
)";
|
|
};
|
|
|
|
TEST_SUITE_BEGIN("ReplPrettyPrint");
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "AdditionStatement")
|
|
{
|
|
runCode(L, "return 30 + 12");
|
|
CHECK(getCapturedOutput() == "42");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableLiteral")
|
|
{
|
|
runCode(L, "return {1, 2, 3, 4}");
|
|
CHECK(getCapturedOutput() == "{1, 2, 3, 4}");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "StringLiteral")
|
|
{
|
|
runCode(L, "return 'str'");
|
|
CHECK(getCapturedOutput() == "\"str\"");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithStringLiterals")
|
|
{
|
|
runCode(L, "return {1, 'two', 3, 'four'}");
|
|
CHECK(getCapturedOutput() == "{1, \"two\", 3, \"four\"}");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "MultipleArguments")
|
|
{
|
|
runCode(L, "return 3, 'three'");
|
|
CHECK(getCapturedOutput() == "3\t\"three\"");
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
TEST_SUITE_BEGIN("ReplCodeCompletion");
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "CompleteGlobalVariables")
|
|
{
|
|
runCode(L, R"(
|
|
myvariable1 = 5
|
|
myvariable2 = 5
|
|
)");
|
|
{
|
|
// Try to complete globals that are added by the user's script
|
|
CompletionSet completions = getCompletionSet("myvar");
|
|
|
|
std::string prefix = "";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "myvariable1"));
|
|
CHECK(checkCompletion(completions, prefix, "myvariable2"));
|
|
}
|
|
if (FFlag::LuauMathMap)
|
|
{
|
|
// Try completing some builtin functions
|
|
CompletionSet completions = getCompletionSet("math.m");
|
|
|
|
std::string prefix = "math.";
|
|
CHECK(completions.size() == 4);
|
|
CHECK(checkCompletion(completions, prefix, "max("));
|
|
CHECK(checkCompletion(completions, prefix, "min("));
|
|
CHECK(checkCompletion(completions, prefix, "modf("));
|
|
CHECK(checkCompletion(completions, prefix, "map("));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys")
|
|
{
|
|
runCode(L, R"(
|
|
t = { color = "red", size = 1, shape = "circle" }
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 3);
|
|
CHECK(checkCompletion(completions, prefix, "color"));
|
|
CHECK(checkCompletion(completions, prefix, "size"));
|
|
CHECK(checkCompletion(completions, prefix, "shape"));
|
|
}
|
|
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.s");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "size"));
|
|
CHECK(checkCompletion(completions, prefix, "shape"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "StringMethods")
|
|
{
|
|
runCode(L, R"(
|
|
s = ""
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("s:l");
|
|
|
|
std::string prefix = "s:";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "len("));
|
|
CHECK(checkCompletion(completions, prefix, "lower("));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexTable")
|
|
{
|
|
runCode(L, R"(
|
|
-- Create 't' which is a table with a metatable with an __index table
|
|
mt = {}
|
|
mt.__index = mt
|
|
|
|
t = {}
|
|
setmetatable(t, mt)
|
|
|
|
mt.mtkey1 = {x="x value", y="y value", 1, 2}
|
|
mt.mtkey2 = 2
|
|
|
|
t.tkey1 = {data1 = 2, data2 = "str", 3, 4}
|
|
t.tkey2 = 4
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.t");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "tkey1"));
|
|
CHECK(checkCompletion(completions, prefix, "tkey2"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.tkey1.data2:re");
|
|
|
|
std::string prefix = "t.tkey1.data2:";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "rep("));
|
|
CHECK(checkCompletion(completions, prefix, "reverse("));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.mtk");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "mtkey1"));
|
|
CHECK(checkCompletion(completions, prefix, "mtkey2"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.mtkey1.");
|
|
|
|
std::string prefix = "t.mtkey1.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "x"));
|
|
CHECK(checkCompletion(completions, prefix, "y"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexFunction")
|
|
{
|
|
runCode(L, R"(
|
|
-- Create 't' which is a table with a metatable with an __index function
|
|
mt = {}
|
|
mt.__index = function(table, key)
|
|
print("mt.__index called")
|
|
if key == "foo" then
|
|
return "FOO"
|
|
elseif key == "bar" then
|
|
return "BAR"
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
t = {}
|
|
setmetatable(t, mt)
|
|
t.tkey = 0
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.t");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 1);
|
|
CHECK(checkCompletion(completions, prefix, "tkey"));
|
|
}
|
|
{
|
|
// t.foo is a valid key, but should not be completed because it requires calling an __index function
|
|
CompletionSet completions = getCompletionSet("t.foo");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
{
|
|
// t.foo is a valid key, but should not be found because it requires calling an __index function
|
|
CompletionSet completions = getCompletionSet("t.foo:");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithMultipleMetatableIndexTables")
|
|
{
|
|
runCode(L, R"(
|
|
-- Create a table with a chain of metatables
|
|
mt2 = {}
|
|
mt2.__index = mt2
|
|
|
|
mt = {}
|
|
mt.__index = mt
|
|
setmetatable(mt, mt2)
|
|
|
|
t = {}
|
|
setmetatable(t, mt)
|
|
|
|
mt2.mt2key = {x=1, y=2}
|
|
mt.mtkey = 2
|
|
t.tkey = 3
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 4);
|
|
CHECK(checkCompletion(completions, prefix, "__index"));
|
|
CHECK(checkCompletion(completions, prefix, "tkey"));
|
|
CHECK(checkCompletion(completions, prefix, "mtkey"));
|
|
CHECK(checkCompletion(completions, prefix, "mt2key"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.__index.");
|
|
|
|
std::string prefix = "t.__index.";
|
|
CHECK(completions.size() == 3);
|
|
CHECK(checkCompletion(completions, prefix, "__index"));
|
|
CHECK(checkCompletion(completions, prefix, "mtkey"));
|
|
CHECK(checkCompletion(completions, prefix, "mt2key"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.mt2key.");
|
|
|
|
std::string prefix = "t.mt2key.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "x"));
|
|
CHECK(checkCompletion(completions, prefix, "y"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithDeepMetatableIndexTables")
|
|
{
|
|
runCode(L, R"(
|
|
-- Creates a table with a chain of metatables of length `count`
|
|
function makeChainedTable(count)
|
|
local result = {}
|
|
result.__index = result
|
|
result[string.format("entry%d", count)] = { count = count }
|
|
if count == 0 then
|
|
return result
|
|
else
|
|
return setmetatable(result, makeChainedTable(count - 1))
|
|
end
|
|
end
|
|
|
|
t30 = makeChainedTable(30)
|
|
t60 = makeChainedTable(60)
|
|
)");
|
|
{
|
|
// Check if entry0 exists
|
|
CompletionSet completions = getCompletionSet("t30.entry0");
|
|
|
|
std::string prefix = "t30.";
|
|
CHECK(checkCompletion(completions, prefix, "entry0"));
|
|
}
|
|
{
|
|
// Check if entry0.count exists
|
|
CompletionSet completions = getCompletionSet("t30.entry0.co");
|
|
|
|
std::string prefix = "t30.entry0.";
|
|
CHECK(checkCompletion(completions, prefix, "count"));
|
|
}
|
|
{
|
|
// Check if entry0 exists. With the max traversal limit of 50 in the repl, this should fail.
|
|
CompletionSet completions = getCompletionSet("t60.entry0");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
{
|
|
// Check if entry0.count exists. With the max traversal limit of 50 in the repl, this should fail.
|
|
CompletionSet completions = getCompletionSet("t60.entry0.co");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
TEST_SUITE_BEGIN("RegressionTests");
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "InfiniteRecursion")
|
|
{
|
|
// If the infinite recrusion is not caught, test will fail
|
|
runCode(L, R"(
|
|
local NewProxyOne = newproxy(true)
|
|
local MetaTableOne = getmetatable(NewProxyOne)
|
|
MetaTableOne.__index = function()
|
|
return NewProxyOne.Game
|
|
end
|
|
print(NewProxyOne.HelloICauseACrash)
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve1")
|
|
{
|
|
// Reset stack reservation
|
|
lua_resume(L, nullptr, 0);
|
|
|
|
runCode(L, R"(
|
|
local t = {}
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve2")
|
|
{
|
|
// Reset stack reservation
|
|
lua_resume(L, nullptr, 0);
|
|
|
|
getCompletionSet("a");
|
|
}
|
|
|
|
TEST_SUITE_END();
|