luau/tests/Linter.test.cpp
Arseny Kapoulkine 32fb6d10a7
Sync to upstream/release/506 (#270)
- Fix some cases where type checking would overflow the native stack
- Improve autocomplete behavior when assigning a partially written function call (not currently exposed through command line tools)
- Improve autocomplete type inference feedback for some expressions where previously the type would not be known
- Improve quantification performance during type checking for large types
- Improve type checking for table literals when the expected type of the table is known because of a type annotation
- Fix type checking errors in cases where required module has errors in the resulting type
- Fix debug line information for multi-line chained call sequences (Add function name information for "attempt to call a nil value" #255)
- lua_newuserdata now takes 2 arguments to match Lua/LuaJIT APIs better; lua_newuserdatatagged should be used if the third argument was non-0.
- lua_ref can no longer be used with LUA_REGISTRYINDEX to prevent mistakes when migrating Lua FFI (Inconsistency with lua_ref #247)
- Fix assertions and possible crashes when executing script code indirectly via metatable dispatch from lua_equal/lua_lessthan/lua_getfield/etc. (Hitting a crash in an assert after lua_equal is called. #259)
- Fix flamegraph scripts to run under Python 2
2021-12-02 22:41:04 -08:00

1512 lines
41 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Linter.h"
#include "Luau/BuiltinDefinitions.h"
#include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("Linter");
TEST_CASE_FIXTURE(Fixture, "CleanCode")
{
LintResult result = lint(R"(
function fib(n)
return n < 2 and 1 or fib(n-1) + fib(n-2)
end
return math.max(fib(5), 1)
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "UnknownGlobal")
{
LintResult result = lint("--!nocheck\nreturn foo");
REQUIRE_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Unknown global 'foo'");
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobal")
{
// Normally this would be defined externally, so hack it in for testing
addGlobalBinding(typeChecker, "Wait", Binding{typeChecker.anyType, {}, true, "wait", "@test/global/Wait"});
LintResult result = lintTyped("Wait(5)");
REQUIRE_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Global 'Wait' is deprecated, use 'wait' instead");
}
TEST_CASE_FIXTURE(Fixture, "PlaceholderRead")
{
LintResult result = lint(R"(
local _ = 5
return _
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable");
}
TEST_CASE_FIXTURE(Fixture, "PlaceholderWrite")
{
LintResult result = lint(R"(
local _ = 5
_ = 6
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "BuiltinGlobalWrite")
{
LintResult result = lint(R"(
math = {}
function assert(x)
end
assert(5)
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Built-in global 'math' is overwritten here; consider using a local or changing the name");
CHECK_EQ(result.warnings[1].text, "Built-in global 'assert' is overwritten here; consider using a local or changing the name");
}
TEST_CASE_FIXTURE(Fixture, "MultilineBlock")
{
LintResult result = lint(R"(
if true then print(1) print(2) print(3) end
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence");
}
TEST_CASE_FIXTURE(Fixture, "MultilineBlockSemicolonsWhitelisted")
{
LintResult result = lint(R"(
print(1); print(2); print(3)
)");
CHECK(result.warnings.empty());
}
TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
{
LintResult result = lint(R"(
print(1); print(2) print(3)
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence");
}
TEST_CASE_FIXTURE(Fixture, "MultilineBlockLocalDo")
{
LintResult result = lint(R"(
local _x do
_x = 5
end
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "ConfusingIndentation")
{
LintResult result = lint(R"(
print(math.max(1,
2))
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Statement spans multiple lines; use indentation to silence");
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal")
{
LintResult result = lint(R"(
function bar()
foo = 6
return foo
end
return bar()
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Global 'foo' is only used in the enclosing function 'bar'; consider changing it to local");
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMulti")
{
LintResult result = lint(R"(
local createFunction = function(configValue)
-- Create an internal convenience function
local function internalLogic()
print(configValue) -- prints passed-in value
end
-- Here, we thought we were creating another internal convenience function
-- that closed over the passed-in configValue, but this is actually being
-- declared at module scope!
function moreInternalLogic()
print(configValue) -- nil!!!
end
return function()
internalLogic()
moreInternalLogic()
return nil
end
end
fnA = createFunction(true)
fnB = createFunction(false)
fnA() -- prints "true", "nil"
fnB() -- prints "false", "nil"
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text,
"Global 'moreInternalLogic' is only used in the enclosing function defined at line 2; consider changing it to local");
}
TEST_CASE_FIXTURE(Fixture, "LocalShadowLocal")
{
LintResult result = lint(R"(
local arg = 6
print(arg)
local arg = 5
print(arg)
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Variable 'arg' shadows previous declaration at line 2");
}
TEST_CASE_FIXTURE(Fixture, "LocalShadowGlobal")
{
LintResult result = lint(R"(
local math = math
global = math
function bar()
local global = math.max(5, 1)
return global
end
return bar()
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Variable 'global' shadows a global variable used at line 3");
}
TEST_CASE_FIXTURE(Fixture, "LocalShadowArgument")
{
LintResult result = lint(R"(
function bar(a, b)
local a = b + 1
return a
end
return bar()
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Variable 'a' shadows previous declaration at line 2");
}
TEST_CASE_FIXTURE(Fixture, "LocalUnused")
{
LintResult result = lint(R"(
local arg = 6
local function bar()
local arg = 5
local blarg = 6
if arg then
blarg = 42
end
end
return bar()
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Variable 'arg' is never used; prefix with '_' to silence");
CHECK_EQ(result.warnings[1].text, "Variable 'blarg' is never used; prefix with '_' to silence");
}
TEST_CASE_FIXTURE(Fixture, "ImportUnused")
{
// Normally this would be defined externally, so hack it in for testing
addGlobalBinding(typeChecker, "game", typeChecker.anyType, "@test");
LintResult result = lint(R"(
local Roact = require(game.Packages.Roact)
local _Roact = require(game.Packages.Roact)
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Import 'Roact' is never used; prefix with '_' to silence");
}
TEST_CASE_FIXTURE(Fixture, "FunctionUnused")
{
LintResult result = lint(R"(
function bar()
end
local function qux()
end
function foo()
end
local function _unusedl()
end
function _unusedg()
end
return foo()
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Function 'bar' is never used; prefix with '_' to silence");
CHECK_EQ(result.warnings[1].text, "Function 'qux' is never used; prefix with '_' to silence");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeBasic")
{
LintResult result = lint(R"(
do
return 'ok'
end
print("hi!")
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 5);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopBreak")
{
LintResult result = lint(R"(
while true do
do break end
print("nope")
end
print("hi!")
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always breaks)");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopContinue")
{
LintResult result = lint(R"(
while true do
do continue end
print("nope")
end
print("hi!")
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always continues)");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeIfMerge")
{
LintResult result = lint(R"(
function foo1(a)
if a then
return 'x'
else
return 'y'
end
return 'z'
end
function foo2(a)
if a then
return 'x'
end
return 'z'
end
function foo3(a)
if a then
return 'x'
else
print('y')
end
return 'z'
end
return { foo1, foo2, foo3 }
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 7);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnSilent")
{
LintResult result = lint(R"(
function foo1(a)
if a then
error('x')
return 'z'
else
error('y')
end
end
return foo1
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeAssertFalseReturnSilent")
{
LintResult result = lint(R"(
function foo1(a)
if a then
return 'z'
end
assert(false)
end
return foo1
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnNonSilentBranchy")
{
LintResult result = lint(R"(
function foo1(a)
if a then
error('x')
else
error('y')
end
return 'z'
end
return foo1
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 7);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnPropagate")
{
LintResult result = lint(R"(
function foo1(a)
if a then
error('x')
return 'z'
else
error('y')
end
return 'x'
end
return foo1
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 8);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)");
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopWhile")
{
LintResult result = lint(R"(
function foo1(a)
while a do
return 'z'
end
return 'x'
end
return foo1
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopRepeat")
{
LintResult result = lint(R"(
function foo1(a)
repeat
return 'z'
until a
return 'x'
end
return foo1
)");
CHECK_EQ(result.warnings.size(),
0); // this is technically a bug, since the repeat body always returns; fixing this bug is a bit more involved than I'd like
}
TEST_CASE_FIXTURE(Fixture, "UnknownType")
{
unfreeze(typeChecker.globalTypes);
TableTypeVar::Props instanceProps{
{"ClassName", {typeChecker.anyType}},
};
TableTypeVar instanceTable{instanceProps, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed};
TypeId instanceType = typeChecker.globalTypes.addType(instanceTable);
TypeFun instanceTypeFun{{}, instanceType};
ClassTypeVar::Props enumItemProps{
{"EnumType", {typeChecker.anyType}},
};
ClassTypeVar enumItemClass{"EnumItem", enumItemProps, std::nullopt, std::nullopt, {}, {}};
TypeId enumItemType = typeChecker.globalTypes.addType(enumItemClass);
TypeFun enumItemTypeFun{{}, enumItemType};
ClassTypeVar normalIdClass{"NormalId", {}, enumItemType, std::nullopt, {}, {}};
TypeId normalIdType = typeChecker.globalTypes.addType(normalIdClass);
TypeFun normalIdTypeFun{{}, normalIdType};
// Normally this would be defined externally, so hack it in for testing
addGlobalBinding(typeChecker, "game", typeChecker.anyType, "@test");
addGlobalBinding(typeChecker, "typeof", typeChecker.anyType, "@test");
typeChecker.globalScope->exportedTypeBindings["Part"] = instanceTypeFun;
typeChecker.globalScope->exportedTypeBindings["Workspace"] = instanceTypeFun;
typeChecker.globalScope->exportedTypeBindings["RunService"] = instanceTypeFun;
typeChecker.globalScope->exportedTypeBindings["Instance"] = instanceTypeFun;
typeChecker.globalScope->exportedTypeBindings["ColorSequence"] = TypeFun{{}, typeChecker.anyType};
typeChecker.globalScope->exportedTypeBindings["EnumItem"] = enumItemTypeFun;
typeChecker.globalScope->importedTypeBindings["Enum"] = {{"NormalId", normalIdTypeFun}};
freeze(typeChecker.globalTypes);
LintResult result = lint(R"(
local _e01 = game:GetService("Foo")
local _e02 = game:GetService("NormalId")
local _e03 = game:FindService("table")
local _e04 = type(game) == "Part"
local _e05 = type(game) == "NormalId"
local _e06 = typeof(game) == "Bar"
local _e07 = typeof(game) == "Part"
local _e08 = typeof(game) == "vector"
local _e09 = typeof(game) == "NormalId"
local _e10 = game:IsA("ColorSequence")
local _e11 = game:IsA("Enum.NormalId")
local _e12 = game:FindFirstChildWhichIsA("function")
local _o01 = game:GetService("Workspace")
local _o02 = game:FindService("RunService")
local _o03 = type(game) == "number"
local _o04 = type(game) == "vector"
local _o05 = typeof(game) == "string"
local _o06 = typeof(game) == "Instance"
local _o07 = typeof(game) == "EnumItem"
local _o08 = game:IsA("Part")
local _o09 = game:IsA("NormalId")
local _o10 = game:FindFirstChildWhichIsA("Part")
)");
REQUIRE_EQ(result.warnings.size(), 12);
CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "Unknown type 'Foo'");
CHECK_EQ(result.warnings[1].location.begin.line, 2);
CHECK_EQ(result.warnings[1].text, "Unknown type 'NormalId' (expected class type)");
CHECK_EQ(result.warnings[2].location.begin.line, 3);
CHECK_EQ(result.warnings[2].text, "Unknown type 'table' (expected class type)");
CHECK_EQ(result.warnings[3].location.begin.line, 4);
CHECK_EQ(result.warnings[3].text, "Unknown type 'Part' (expected primitive type)");
CHECK_EQ(result.warnings[4].location.begin.line, 5);
CHECK_EQ(result.warnings[4].text, "Unknown type 'NormalId' (expected primitive type)");
CHECK_EQ(result.warnings[5].location.begin.line, 6);
CHECK_EQ(result.warnings[5].text, "Unknown type 'Bar'");
CHECK_EQ(result.warnings[6].location.begin.line, 7);
CHECK_EQ(result.warnings[6].text, "Unknown type 'Part' (expected primitive or userdata type)");
CHECK_EQ(result.warnings[7].location.begin.line, 8);
CHECK_EQ(result.warnings[7].text, "Unknown type 'vector' (expected primitive or userdata type)");
CHECK_EQ(result.warnings[8].location.begin.line, 9);
CHECK_EQ(result.warnings[8].text, "Unknown type 'NormalId' (expected primitive or userdata type)");
CHECK_EQ(result.warnings[9].location.begin.line, 10);
CHECK_EQ(result.warnings[9].text, "Unknown type 'ColorSequence' (expected class or enum type)");
CHECK_EQ(result.warnings[10].location.begin.line, 11);
CHECK_EQ(result.warnings[10].text, "Unknown type 'Enum.NormalId'");
CHECK_EQ(result.warnings[11].location.begin.line, 12);
CHECK_EQ(result.warnings[11].text, "Unknown type 'function' (expected class type)");
}
TEST_CASE_FIXTURE(Fixture, "ForRangeTable")
{
LintResult result = lint(R"(
local t = {}
for i=#t,1 do
end
for i=#t,1,-1 do
end
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?");
}
TEST_CASE_FIXTURE(Fixture, "ForRangeBackwards")
{
LintResult result = lint(R"(
for i=8,1 do
end
for i=8,1,-1 do
end
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?");
}
TEST_CASE_FIXTURE(Fixture, "ForRangeImprecise")
{
LintResult result = lint(R"(
for i=1.3,7.5 do
end
for i=1.3,7.5,1 do
end
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "For loop ends at 7.3 instead of 7.5; did you forget to specify step?");
}
TEST_CASE_FIXTURE(Fixture, "ForRangeZero")
{
LintResult result = lint(R"(
for i=0,#t do
end
for i=(0),#t do -- to silence
end
for i=#t,0 do
end
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "For loop starts at 0, but arrays start at 1");
CHECK_EQ(result.warnings[1].location.begin.line, 7);
CHECK_EQ(result.warnings[1].text,
"For loop should iterate backwards; did you forget to specify -1 as step? Also consider changing 0 to 1 since arrays start at 1");
}
TEST_CASE_FIXTURE(Fixture, "UnbalancedAssignment")
{
LintResult result = lint(R"(
do
local _a,_b,_c = pcall()
end
do
local _a,_b,_c = pcall(), 5
end
do
local _a,_b,_c = pcall(), 5, 6
end
do
local _a,_b,_c = pcall(), 5, 6, 7
end
do
local _a,_b,_c = pcall(), nil
end
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].location.begin.line, 5);
CHECK_EQ(result.warnings[0].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence");
CHECK_EQ(result.warnings[1].location.begin.line, 11);
CHECK_EQ(result.warnings[1].text, "Assigning 4 values to 3 variables leaves some values unused");
}
TEST_CASE_FIXTURE(Fixture, "ImplicitReturn")
{
LintResult result = lint(R"(
function f1(a)
if not a then
return 5
end
end
function f2(a)
if not a then
return
end
end
function f3(a)
if not a then
return 5
else
return
end
end
function f4(a)
for i in pairs(a) do
if i > 5 then
return i
end
end
print("element not found")
end
function f5(a)
for i in pairs(a) do
if i > 5 then
return i
end
end
error("element not found")
end
f6 = function(a)
if a == 0 then
return 42
end
end
function f7(a)
repeat
return 10
until a ~= nil
end
return f1,f2,f3,f4,f5,f6,f7
)");
CHECK_EQ(result.warnings.size(), 3);
CHECK_EQ(result.warnings[0].location.begin.line, 4);
CHECK_EQ(result.warnings[0].text,
"Function 'f1' can implicitly return no values even though there's an explicit return at line 4; add explicit return to silence");
CHECK_EQ(result.warnings[1].location.begin.line, 28);
CHECK_EQ(result.warnings[1].text,
"Function 'f4' can implicitly return no values even though there's an explicit return at line 25; add explicit return to silence");
CHECK_EQ(result.warnings[2].location.begin.line, 44);
CHECK_EQ(result.warnings[2].text,
"Function can implicitly return no values even though there's an explicit return at line 44; add explicit return to silence");
}
TEST_CASE_FIXTURE(Fixture, "ImplicitReturnInfiniteLoop")
{
LintResult result = lint(R"(
function f1(a)
while true do
if math.random() > 0.5 then
return 5
end
end
end
function f2(a)
repeat
if math.random() > 0.5 then
return 5
end
until false
end
function f3(a)
while true do
if math.random() > 0.5 then
return 5
end
if math.random() < 0.1 then
break
end
end
end
function f4(a)
repeat
if math.random() > 0.5 then
return 5
end
if math.random() < 0.1 then
break
end
until false
end
return f1,f2,f3,f4
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].location.begin.line, 25);
CHECK_EQ(result.warnings[0].text,
"Function 'f3' can implicitly return no values even though there's an explicit return at line 21; add explicit return to silence");
CHECK_EQ(result.warnings[1].location.begin.line, 36);
CHECK_EQ(result.warnings[1].text,
"Function 'f4' can implicitly return no values even though there's an explicit return at line 32; add explicit return to silence");
}
TEST_CASE_FIXTURE(Fixture, "TypeAnnotationsShouldNotProduceWarnings")
{
LintResult result = lint(R"(--!strict
type InputData = {
id: number,
inputType: EnumItem,
inputState: EnumItem,
updated: number,
position: Vector3,
keyCode: EnumItem,
name: string
}
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "BreakFromInfiniteLoopMakesStatementReachable")
{
LintResult result = lint(R"(
local bar = ...
repeat
if bar then
break
end
return 2
until true
return 1
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
{
LintResult result = lint(R"(
--!nolint
return foo
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "IgnoreLintSpecific")
{
LintResult result = lint(R"(
--!nolint UnknownGlobal
local x = 1
return foo
)");
CHECK_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringFormat")
{
LintResult result = lint(R"(
-- incorrect format strings
string.format("%")
string.format("%??d")
string.format("%Y")
-- incorrect format strings, self call
local _ = ("%"):format()
-- correct format strings, just to uh make sure
string.format("hello %d %f", 4, 5)
)");
CHECK_EQ(result.warnings.size(), 4);
CHECK_EQ(result.warnings[0].text, "Invalid format string: unfinished format specifier");
CHECK_EQ(result.warnings[1].text, "Invalid format string: invalid format specifier: must be a string format specifier or %");
CHECK_EQ(result.warnings[2].text, "Invalid format string: invalid format specifier: must be a string format specifier or %");
CHECK_EQ(result.warnings[3].text, "Invalid format string: unfinished format specifier");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringPack")
{
LintResult result = lint(R"(
-- incorrect pack specifiers
string.pack("?")
string.packsize("?")
string.unpack("?")
-- missing size
string.packsize("bc")
-- incorrect X alignment
string.packsize("X")
string.packsize("X i")
-- correct X alignment
string.packsize("Xi")
-- packsize can't be used with variable sized formats
string.packsize("s")
-- out of range size specifiers
string.packsize("i0")
string.packsize("i17")
-- a very very very out of range size specifier
string.packsize("i99999999999999999999")
string.packsize("c99999999999999999999")
-- correct format specifiers
string.packsize("=!1bbbI3c42")
)");
CHECK_EQ(result.warnings.size(), 11);
CHECK_EQ(result.warnings[0].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
CHECK_EQ(result.warnings[1].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
CHECK_EQ(result.warnings[2].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
CHECK_EQ(result.warnings[3].text, "Invalid pack format: fixed-sized string format must specify the size");
CHECK_EQ(result.warnings[4].text, "Invalid pack format: X must be followed by a size specifier");
CHECK_EQ(result.warnings[5].text, "Invalid pack format: X must be followed by a size specifier");
CHECK_EQ(result.warnings[6].text, "Invalid pack format: pack specifier must be fixed-size");
CHECK_EQ(result.warnings[7].text, "Invalid pack format: integer size must be in range [1,16]");
CHECK_EQ(result.warnings[8].text, "Invalid pack format: integer size must be in range [1,16]");
CHECK_EQ(result.warnings[9].text, "Invalid pack format: size specifier is too large");
CHECK_EQ(result.warnings[10].text, "Invalid pack format: size specifier is too large");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringMatch")
{
LintResult result = lint(R"(
local s = ...
-- incorrect character class specifiers
string.match(s, "%q")
string.gmatch(s, "%q")
string.find(s, "%q")
string.gsub(s, "%q", "")
-- various errors
string.match(s, "%")
string.match(s, "[%1]")
string.match(s, "%0")
string.match(s, "(%d)%2")
string.match(s, "%bx")
string.match(s, "%foo")
string.match(s, '(%d))')
string.match(s, '(%d')
string.match(s, '[%d')
string.match(s, '%,')
-- self call - not detected because we don't know the type!
local _ = s:match("%q")
-- correct patterns
string.match(s, "[A-Z]+(%d)%1")
)");
CHECK_EQ(result.warnings.size(), 14);
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[2].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[3].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[4].text, "Invalid match pattern: unfinished character class");
CHECK_EQ(result.warnings[5].text, "Invalid match pattern: sets can not contain capture references");
CHECK_EQ(result.warnings[6].text, "Invalid match pattern: invalid capture reference, must be 1-9");
CHECK_EQ(result.warnings[7].text, "Invalid match pattern: invalid capture reference, must refer to a valid capture");
CHECK_EQ(result.warnings[8].text, "Invalid match pattern: missing brace characters for balanced match");
CHECK_EQ(result.warnings[9].text, "Invalid match pattern: missing set after a frontier pattern");
CHECK_EQ(result.warnings[10].text, "Invalid match pattern: unexpected ) without a matching (");
CHECK_EQ(result.warnings[11].text, "Invalid match pattern: expected ) at the end of the string to close a capture");
CHECK_EQ(result.warnings[12].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[13].text, "Invalid match pattern: expected a magic character after %");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringMatchNested")
{
LintResult result = lint(R"~(
local s = ...
-- correct reference to nested pattern
string.match(s, "((a)%2)")
-- incorrect reference to nested pattern (not closed yet)
string.match(s, "((a)%1)")
-- incorrect reference to nested pattern (index out of range)
string.match(s, "((a)%3)")
)~");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid capture reference, must refer to a closed capture");
CHECK_EQ(result.warnings[0].location.begin.line, 7);
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid capture reference, must refer to a valid capture");
CHECK_EQ(result.warnings[1].location.begin.line, 10);
}
TEST_CASE_FIXTURE(Fixture, "FormatStringMatchSets")
{
LintResult result = lint(R"~(
local s = ...
-- fake empty sets (but actually sets that aren't closed)
string.match(s, "[]")
string.match(s, "[^]")
-- character ranges in sets
string.match(s, "[%a-b]")
string.match(s, "[a-%b]")
-- invalid escapes
string.match(s, "[%q]")
string.match(s, "[%;]")
-- capture refs in sets
string.match(s, "[%1]")
-- valid escapes and - at the end
string.match(s, "[%]x-]")
-- % escapes itself
string.match(s, "[%%]")
-- this abomination is a valid pattern due to rules wrt handling empty sets
string.match(s, "[]|'[]")
string.match(s, "[^]|'[]")
)~");
CHECK_EQ(result.warnings.size(), 7);
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[2].text, "Invalid match pattern: character range can't include character sets");
CHECK_EQ(result.warnings[3].text, "Invalid match pattern: character range can't include character sets");
CHECK_EQ(result.warnings[4].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[5].text, "Invalid match pattern: expected a magic character after %");
CHECK_EQ(result.warnings[6].text, "Invalid match pattern: sets can not contain capture references");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringFindArgs")
{
LintResult result = lint(R"(
local s = ...
-- incorrect character class specifier
string.find(s, "%q")
-- raw string find
string.find(s, "%q", 1, true)
string.find(s, "%q", 1, math.random() < 0.5)
-- incorrect character class specifier
string.find(s, "%q", 1, false)
-- missing arguments
string.find()
string.find("foo");
("foo"):find()
)");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[0].location.begin.line, 4);
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[1].location.begin.line, 11);
}
TEST_CASE_FIXTURE(Fixture, "FormatStringReplace")
{
LintResult result = lint(R"(
local s = ...
-- incorrect replacements
string.gsub(s, '(%d+)', "%")
string.gsub(s, '(%d+)', "%x")
string.gsub(s, '(%d+)', "%2")
string.gsub(s, '', "%1")
-- correct replacements
string.gsub(s, '[A-Z]+(%d)', "%0%1")
string.gsub(s, 'foo', "%0")
)");
CHECK_EQ(result.warnings.size(), 4);
CHECK_EQ(result.warnings[0].text, "Invalid match replacement: unfinished replacement");
CHECK_EQ(result.warnings[1].text, "Invalid match replacement: unexpected replacement character; must be a digit or %");
CHECK_EQ(result.warnings[2].text, "Invalid match replacement: invalid capture index, must refer to pattern capture");
CHECK_EQ(result.warnings[3].text, "Invalid match replacement: invalid capture index, must refer to pattern capture");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringDate")
{
LintResult result = lint(R"(
-- incorrect formats
os.date("%")
os.date("%L")
os.date("%?")
-- correct formats
os.date("it's %c now")
os.date("!*t")
)");
CHECK_EQ(result.warnings.size(), 3);
CHECK_EQ(result.warnings[0].text, "Invalid date format: unfinished replacement");
CHECK_EQ(result.warnings[1].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %");
CHECK_EQ(result.warnings[2].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %");
}
TEST_CASE_FIXTURE(Fixture, "FormatStringTyped")
{
LintResult result = lintTyped(R"~(
local s: string, nons = ...
string.match(s, "[]")
s:match("[]")
-- no warning here since we don't know that it's a string
nons:match("[]")
)~");
CHECK_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[1].location.begin.line, 4);
}
TEST_CASE_FIXTURE(Fixture, "TableLiteral")
{
LintResult result = lint(R"(-- line 1
_ = {
first = 1,
second = 2,
first = 3,
}
_ = {
first = 1,
["first"] = 2,
}
_ = {
1, 2, 3,
[1] = 42
}
_ = {
[3] = 42,
1, 2, 3,
}
local _: {
first: number,
second: string,
first: boolean
}
_ = {
1, 2, 3,
[0] = 42,
[4] = 42,
}
_ = {
[1] = 1,
[2] = 2,
[1] = 3,
}
)");
CHECK_EQ(result.warnings.size(), 6);
CHECK_EQ(result.warnings[0].text, "Table field 'first' is a duplicate; previously defined at line 3");
CHECK_EQ(result.warnings[1].text, "Table field 'first' is a duplicate; previously defined at line 9");
CHECK_EQ(result.warnings[2].text, "Table index 1 is a duplicate; previously defined as a list entry");
CHECK_EQ(result.warnings[3].text, "Table index 3 is a duplicate; previously defined as a list entry");
CHECK_EQ(result.warnings[4].text, "Table type field 'first' is a duplicate; previously defined at line 24");
CHECK_EQ(result.warnings[5].text, "Table index 1 is a duplicate; previously defined at line 36");
}
TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation")
{
LintResult result = lint(R"(
local Foo = require(script.Parent.Foo)
local x: Foo.Y = 1
)");
REQUIRE_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence");
}
TEST_CASE_FIXTURE(Fixture, "DisableUnknownGlobalWithTypeChecking")
{
LintResult result = lint(R"(
--!strict
unknownGlobal()
)");
REQUIRE_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "no_spurious_warning_after_a_function_type_alias")
{
LintResult result = lint(R"(
local exports = {}
export type PathFunction<P> = (P?) -> string
exports.tokensToFunction = function() end
return exports
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "use_all_parent_scopes_for_globals")
{
ScopePtr testScope = frontend.addEnvironment("Test");
unfreeze(typeChecker.globalTypes);
loadDefinitionFile(frontend.typeChecker, testScope, R"(
declare Foo: number
)",
"@test");
freeze(typeChecker.globalTypes);
fileResolver.environments["A"] = "Test";
fileResolver.source["A"] = R"(
local _foo: Foo = 123
-- os.clock comes from the global scope, the parent of this module's environment
local _bar: typeof(os.clock) = os.clock
)";
LintResult result = frontend.lint("A");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "DeadLocalsUsed")
{
LintResult result = lint(R"(
--!nolint LocalShadow
do
local x
for x in pairs({}) do
print(x)
end
print(x) -- x is not initialized
end
do
local a, b, c = 1, 2
print(a, b, c) -- c is not initialized
end
do
local a, b, c = table.unpack({})
print(a, b, c) -- no warning as we don't know anything about c
end
)");
CHECK_EQ(result.warnings.size(), 3);
CHECK_EQ(result.warnings[0].text, "Variable 'x' defined at line 4 is never initialized or assigned; initialize with 'nil' to silence");
CHECK_EQ(result.warnings[1].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence");
CHECK_EQ(result.warnings[2].text, "Variable 'c' defined at line 12 is never initialized or assigned; initialize with 'nil' to silence");
}
TEST_CASE_FIXTURE(Fixture, "LocalFunctionNotDead")
{
LintResult result = lint(R"(
local foo
function foo() end
)");
CHECK_EQ(result.warnings.size(), 0);
}
TEST_CASE_FIXTURE(Fixture, "DuplicateGlobalFunction")
{
LintResult result = lint(R"(
function x() end
function x() end
return x
)");
REQUIRE_EQ(1, result.warnings.size());
const auto& w = result.warnings[0];
CHECK_EQ(LintWarning::Code_DuplicateFunction, w.code);
CHECK_EQ("Duplicate function definition: 'x' also defined on line 2", w.text);
}
TEST_CASE_FIXTURE(Fixture, "DuplicateLocalFunction")
{
LintOptions options;
options.setDefaults();
options.enableWarning(LintWarning::Code_DuplicateFunction);
options.enableWarning(LintWarning::Code_LocalShadow);
LintResult result = lint(R"(
local function x() end
print(x)
local function x() end
return x
)",
options);
REQUIRE_EQ(1, result.warnings.size());
CHECK_EQ(LintWarning::Code_DuplicateFunction, result.warnings[0].code);
}
TEST_CASE_FIXTURE(Fixture, "DuplicateMethod")
{
LintResult result = lint(R"(
local T = {}
function T:x() end
function T:x() end
return x
)");
REQUIRE_EQ(1, result.warnings.size());
const auto& w = result.warnings[0];
CHECK_EQ(LintWarning::Code_DuplicateFunction, w.code);
CHECK_EQ("Duplicate function definition: 'T.x' also defined on line 3", w.text);
}
TEST_CASE_FIXTURE(Fixture, "DontTriggerTheWarningIfTheFunctionsAreInDifferentScopes")
{
LintResult result = lint(R"(
if true then
function c() end
else
function c() end
end
return c
)");
CHECK(result.warnings.empty());
}
TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
{
LintResult result = lint(R"(
local Hooty = require(workspace.A)
local HoHooty = require(workspace.A)
local h: Hooty.Pointy = ruire(workspace.A)
local h: H
local h: Hooty.Pointy = ruire(workspace.A)
local hh: Hooty.Pointy = ruire(workspace.A)
local h: Hooty.Pointy = ruire(workspace.A)
linooty.Pointy = ruire(workspace.A)
local hh: Hooty.Pointy = ruire(workspace.A)
local h: Hooty.Pointy = ruire(workspace.A)
linty = ruire(workspace.A)
local h: Hooty.Pointy = ruire(workspace.A)
local hh: Hooty.Pointy = ruire(workspace.A)
local h: Hooty.Pointy = ruire(workspace.A)
local h: Hooty.Pt
)");
CHECK_EQ(result.warnings.size(), 12);
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
{
unfreeze(typeChecker.globalTypes);
TypeId instanceType = typeChecker.globalTypes.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, {}});
typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};
getMutable<ClassTypeVar>(instanceType)->props = {
{"Name", {typeChecker.stringType}},
{"DataCost", {typeChecker.numberType, /* deprecated= */ true}},
{"Wait", {typeChecker.anyType, /* deprecated= */ true}},
};
freeze(typeChecker.globalTypes);
LintResult result = lintTyped(R"(
return function (i: Instance)
i:Wait(1.0)
print(i.Name)
return i.DataCost
end
)");
REQUIRE_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated");
CHECK_EQ(result.warnings[1].text, "Member 'Instance.DataCost' is deprecated");
}
TEST_CASE_FIXTURE(Fixture, "TableOperations")
{
LintResult result = lintTyped(R"(
local t = {}
local tt = {}
table.insert(t, #t, 42)
table.insert(t, (#t), 42) -- silenced
table.insert(t, #t + 1, 42)
table.insert(t, #tt + 1, 42) -- different table, ok
table.insert(t, 0, 42)
table.remove(t, 0)
table.remove(t, #t-1)
table.insert(t, string.find("hello", "h"))
table.move(t, 0, #t, 1, tt)
table.move(t, 1, #t, 0, tt)
)");
REQUIRE_EQ(result.warnings.size(), 8);
CHECK_EQ(result.warnings[0].text, "table.insert will insert the value before the last element, which is likely a bug; consider removing the "
"second argument or wrap it in parentheses to silence");
CHECK_EQ(result.warnings[1].text, "table.insert will append the value to the table; consider removing the second argument for efficiency");
CHECK_EQ(result.warnings[2].text, "table.insert uses index 0 but arrays are 1-based; did you mean 1 instead?");
CHECK_EQ(result.warnings[3].text, "table.remove uses index 0 but arrays are 1-based; did you mean 1 instead?");
CHECK_EQ(result.warnings[4].text, "table.remove will remove the value before the last element, which is likely a bug; consider removing the "
"second argument or wrap it in parentheses to silence");
CHECK_EQ(result.warnings[5].text,
"table.insert may change behavior if the call returns more than one result; consider adding parentheses around second argument");
CHECK_EQ(result.warnings[6].text, "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?");
CHECK_EQ(result.warnings[7].text, "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?");
}
TEST_CASE_FIXTURE(Fixture, "DuplicateConditions")
{
LintResult result = lint(R"(
if true then
elseif false then
elseif true then -- duplicate
end
if true then
elseif false then
else
if true then -- duplicate
end
end
_ = true and true
_ = true or true
_ = (true and false) and true
_ = (true and true) and true
_ = (true and true) or true
_ = (true and false) and (42 and false)
_ = true and true or false -- no warning since this is is a common pattern used as a ternary replacement
)");
REQUIRE_EQ(result.warnings.size(), 7);
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
CHECK_EQ(result.warnings[0].location.begin.line + 1, 4);
CHECK_EQ(result.warnings[1].text, "Condition has already been checked on column 5");
CHECK_EQ(result.warnings[2].text, "Condition has already been checked on column 5");
CHECK_EQ(result.warnings[3].text, "Condition has already been checked on column 6");
CHECK_EQ(result.warnings[4].text, "Condition has already been checked on column 6");
CHECK_EQ(result.warnings[5].text, "Condition has already been checked on column 6");
CHECK_EQ(result.warnings[6].text, "Condition has already been checked on column 15");
CHECK_EQ(result.warnings[6].location.begin.line + 1, 19);
}
TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsExpr")
{
LintResult result = lint(R"(
local correct, opaque = ...
if correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls")}) then
elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls")}) then
elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", false)}) then
end
)");
REQUIRE_EQ(result.warnings.size(), 1);
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 4");
CHECK_EQ(result.warnings[0].location.begin.line + 1, 5);
}
TEST_CASE_FIXTURE(Fixture, "DuplicateLocal")
{
LintResult result = lint(R"(
function foo(a1, a2, a3, a1)
end
local _, _, _ = ... -- ok!
local a1, a2, a1 = ... -- not ok
local moo = {}
function moo:bar(self)
end
return foo, moo, a1, a2
)");
REQUIRE_EQ(result.warnings.size(), 4);
CHECK_EQ(result.warnings[0].text, "Function parameter 'a1' already defined on column 14");
CHECK_EQ(result.warnings[1].text, "Variable 'a1' is never used; prefix with '_' to silence");
CHECK_EQ(result.warnings[2].text, "Variable 'a1' already defined on column 7");
CHECK_EQ(result.warnings[3].text, "Function parameter 'self' already defined implicitly");
}
TEST_SUITE_END();