luau/tests/Linter.test.cpp
vegorov-rbx 62483d40f0
Sync to upstream/release/562 (#828)
* Fixed rare use-after-free in analysis during table unification

A lot of work these past months went into two new Luau components:
* A near full rewrite of the typechecker using a new deferred constraint
resolution system
* Native code generation for AoT/JiT compilation of VM bytecode into x64
(avx)/arm64 instructions

Both of these components are far from finished and we don't provide
documentation on building and using them at this point.
However, curious community members expressed interest in learning about
changes that go into these components each week, so we are now listing
them here in the 'sync' pull request descriptions.

---
New typechecker can be enabled by setting
DebugLuauDeferredConstraintResolution flag to 'true'.
It is considered unstable right now, so try it at your own risk.
Even though it already provides better type inference than the current
one in some cases, our main goal right now is to reach feature parity
with current typechecker.
Features which improve over the capabilities of the current typechecker
are marked as '(NEW)'.

Changes to new typechecker:
* Regular for loop index and parameters are now typechecked
* Invalid type annotations on local variables are ignored to improve
autocomplete
* Fixed missing autocomplete type suggestions for function arguments
* Type reduction is now performed to produce simpler types to be
presented to the user (error messages, custom LSPs)
* Internally, complex types like '((number | string) & ~(false?)) |
string' can be produced, which is just 'string | number' when simplified
* Fixed spots where support for unknown and never types was missing
* (NEW) Length operator '#' is now valid to use on top table type, this
type comes up when doing typeof(x) == "table" guards and isn't available
in current typechecker

---
Changes to native code generation:
* Additional math library fast calls are now lowered to x64: math.ldexp,
math.round, math.frexp, math.modf, math.sign and math.clamp
2023-02-03 11:26:13 -08:00

1741 lines
46 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)
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnknownGlobal")
{
LintResult result = lint("--!nocheck\nreturn foo");
REQUIRE(1 == result.warnings.size());
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(frontend, "Wait", Binding{typeChecker.anyType, {}, true, "wait", "@test/global/Wait"});
LintResult result = lintTyped("Wait(5)");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'Wait' is deprecated, use 'wait' instead");
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobalNoReplacement")
{
// Normally this would be defined externally, so hack it in for testing
const char* deprecationReplacementString = "";
addGlobalBinding(frontend, "Version", Binding{typeChecker.anyType, {}, true, deprecationReplacementString});
LintResult result = lintTyped("Version()");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'Version' is deprecated");
}
TEST_CASE_FIXTURE(Fixture, "PlaceholderRead")
{
LintResult result = lint(R"(
local _ = 5
return _
)");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable");
}
TEST_CASE_FIXTURE(Fixture, "PlaceholderReadGlobal")
{
LintResult result = lint(R"(
_ = 5
print(_)
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(BuiltinsFixture, "BuiltinGlobalWrite")
{
LintResult result = lint(R"(
math = {}
function assert(x)
end
assert(5)
)");
REQUIRE(2 == result.warnings.size());
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
)");
REQUIRE(1 == result.warnings.size());
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)
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
{
LintResult result = lint(R"(
print(1); print(2) print(3)
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "ConfusingIndentation")
{
LintResult result = lint(R"(
print(math.max(1,
2))
)");
REQUIRE(1 == result.warnings.size());
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()
)");
REQUIRE(1 == result.warnings.size());
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, "GlobalAsLocalMultiFx")
{
LintResult result = lint(R"(
function bar()
foo = 6
return foo
end
function baz()
foo = 6
return foo
end
return bar() + baz()
)");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'foo' is never read before being written. Consider changing it to local");
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMultiFxWithRead")
{
LintResult result = lint(R"(
function bar()
foo = 6
return foo
end
function baz()
foo = 6
return foo
end
function read()
print(foo)
end
return bar() + baz() + read()
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalWithConditional")
{
LintResult result = lint(R"(
function bar()
if true then foo = 6 end
return foo
end
function baz()
foo = 6
return foo
end
return bar() + baz()
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal3WithConditionalRead")
{
LintResult result = lint(R"(
function bar()
foo = 6
return foo
end
function baz()
foo = 6
return foo
end
function read()
if false then print(foo) end
end
return bar() + baz() + read()
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalInnerRead")
{
LintResult result = lint(R"(
function foo()
local f = function() return bar end
f()
bar = 42
end
function baz() bar = 0 end
return foo() + baz()
)");
REQUIRE(0 == result.warnings.size());
}
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"
)");
REQUIRE(1 == result.warnings.size());
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)
)");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'arg' shadows previous declaration at line 2");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "LocalShadowGlobal")
{
LintResult result = lint(R"(
local math = math
global = math
function bar()
local global = math.max(5, 1)
return global
end
return bar()
)");
REQUIRE(1 == result.warnings.size());
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()
)");
REQUIRE(1 == result.warnings.size());
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()
)");
REQUIRE(2 == result.warnings.size());
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(frontend, "game", typeChecker.anyType, "@test");
LintResult result = lint(R"(
local Roact = require(game.Packages.Roact)
local _Roact = require(game.Packages.Roact)
)");
REQUIRE(1 == result.warnings.size());
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()
)");
REQUIRE(2 == result.warnings.size());
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!")
)");
REQUIRE(1 == result.warnings.size());
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!")
)");
REQUIRE(1 == result.warnings.size());
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!")
)");
REQUIRE(1 == result.warnings.size());
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 }
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeAssertFalseReturnSilent")
{
LintResult result = lint(R"(
function foo1(a)
if a then
return 'z'
end
assert(false)
end
return foo1
)");
REQUIRE(0 == result.warnings.size());
}
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
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopRepeat")
{
LintResult result = lint(R"(
function foo1(a)
repeat
return 'z'
until a
return 'x'
end
return foo1
)");
// this is technically a bug, since the repeat body always returns; fixing this bug is a bit more involved than I'd like
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnknownType")
{
unfreeze(typeChecker.globalTypes);
TableType::Props instanceProps{
{"ClassName", {typeChecker.anyType}},
};
TableType instanceTable{instanceProps, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed};
TypeId instanceType = typeChecker.globalTypes.addType(instanceTable);
TypeFun instanceTypeFun{{}, instanceType};
typeChecker.globalScope->exportedTypeBindings["Part"] = instanceTypeFun;
LintResult result = lint(R"(
local game = ...
local _e01 = type(game) == "Part"
local _e02 = typeof(game) == "Bar"
local _e03 = typeof(game) == "vector"
local _o01 = type(game) == "number"
local _o02 = type(game) == "vector"
local _o03 = typeof(game) == "Part"
)");
REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 2);
CHECK_EQ(result.warnings[0].text, "Unknown type 'Part' (expected primitive type)");
CHECK_EQ(result.warnings[1].location.begin.line, 3);
CHECK_EQ(result.warnings[1].text, "Unknown type 'Bar'");
CHECK_EQ(result.warnings[2].location.begin.line, 4);
CHECK_EQ(result.warnings[2].text, "Unknown type 'vector' (expected primitive or userdata 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
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(1 == result.warnings.size());
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
)");
REQUIRE(2 == result.warnings.size());
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
)");
REQUIRE(2 == result.warnings.size());
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
)");
REQUIRE(3 == result.warnings.size());
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
)");
REQUIRE(2 == result.warnings.size());
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
}
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "BreakFromInfiniteLoopMakesStatementReachable")
{
LintResult result = lint(R"(
local bar = ...
repeat
if bar then
break
end
return 2
until true
return 1
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
{
LintResult result = lint(R"(
--!nolint
return foo
)");
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "IgnoreLintSpecific")
{
LintResult result = lint(R"(
--!nolint UnknownGlobal
local x = 1
return foo
)");
REQUIRE(1 == result.warnings.size());
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 %+10d %.02f %%", 4, 5)
)");
REQUIRE(4 == result.warnings.size());
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")
)");
REQUIRE(11 == result.warnings.size());
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")
)");
REQUIRE(14 == result.warnings.size());
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)")
)~");
REQUIRE(2 == result.warnings.size());
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, "[^]|'[]")
)~");
REQUIRE(7 == result.warnings.size());
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()
)");
REQUIRE(2 == result.warnings.size());
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")
)");
REQUIRE(4 == result.warnings.size());
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("%?")
os.date("\0")
-- correct formats
os.date("it's %c now")
os.date("!*t")
)");
REQUIRE(4 == result.warnings.size());
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 %");
CHECK_EQ(result.warnings[3].text, "Invalid date format: date format can not contain null characters");
}
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("[]")
)~");
REQUIRE(2 == result.warnings.size());
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,
}
)");
REQUIRE(6 == result.warnings.size());
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(1 == result.warnings.size());
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(0 == result.warnings.size());
}
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
)");
REQUIRE(0 == result.warnings.size());
}
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");
REQUIRE(0 == result.warnings.size());
}
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
)");
REQUIRE(3 == result.warnings.size());
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
)");
REQUIRE(0 == result.warnings.size());
}
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
)");
REQUIRE(0 == result.warnings.size());
}
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
)");
REQUIRE(12 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
{
unfreeze(typeChecker.globalTypes);
TypeId instanceType = typeChecker.globalTypes.addType(ClassType{"Instance", {}, std::nullopt, std::nullopt, {}, {}, "Test"});
persist(instanceType);
typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};
getMutable<ClassType>(instanceType)->props = {
{"Name", {typeChecker.stringType}},
{"DataCost", {typeChecker.numberType, /* deprecated= */ true}},
{"Wait", {typeChecker.anyType, /* deprecated= */ true}},
};
TypeId colorType = typeChecker.globalTypes.addType(TableType{{}, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed});
getMutable<TableType>(colorType)->props = {{"toHSV", {typeChecker.anyType, /* deprecated= */ true, "Color3:ToHSV"}}};
addGlobalBinding(frontend, "Color3", Binding{colorType, {}});
freeze(typeChecker.globalTypes);
LintResult result = lintTyped(R"(
return function (i: Instance)
i:Wait(1.0)
print(i.Name)
print(Color3.toHSV())
print(Color3.doesntexist, i.doesntexist) -- type error, but this verifies we correctly handle non-existent members
return i.DataCost
end
)");
REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated");
CHECK_EQ(result.warnings[1].text, "Member 'toHSV' is deprecated, use 'Color3:ToHSV' instead");
CHECK_EQ(result.warnings[2].text, "Member 'Instance.DataCost' is deprecated");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "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)
table.create(42, {})
table.create(42, {} :: {})
)");
REQUIRE(10 == result.warnings.size());
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?");
CHECK_EQ(
result.warnings[8].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead");
CHECK_EQ(
result.warnings[9].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop 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
_ = if true then 1 elseif true then 2 else 3
)");
REQUIRE(8 == result.warnings.size());
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);
CHECK_EQ(result.warnings[7].text, "Condition has already been checked on column 8");
}
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(1 == result.warnings.size());
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(4 == result.warnings.size());
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_CASE_FIXTURE(Fixture, "MisleadingAndOr")
{
LintResult result = lint(R"(
_ = math.random() < 0.5 and true or 42
_ = math.random() < 0.5 and false or 42 -- misleading
_ = math.random() < 0.5 and nil or 42 -- misleading
_ = math.random() < 0.5 and 0 or 42
_ = (math.random() < 0.5 and false) or 42 -- currently ignored
)");
REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "The and-or expression always evaluates to the second alternative because the first alternative is false; "
"consider using if-then-else expression instead");
CHECK_EQ(result.warnings[1].text, "The and-or expression always evaluates to the second alternative because the first alternative is nil; "
"consider using if-then-else expression instead");
}
TEST_CASE_FIXTURE(Fixture, "WrongComment")
{
LintResult result = lint(R"(
--!strict
--!struct
--!nolintGlobal
--!nolint Global
--!nolint KnownGlobal
--!nolint UnknownGlobal
--! no more lint
--!strict here
do end
--!nolint
)");
REQUIRE(6 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?");
CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'");
CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'");
CHECK_EQ(result.warnings[3].text, "nolint directive refers to unknown lint rule 'KnownGlobal'; did you mean 'UnknownGlobal'?");
CHECK_EQ(result.warnings[4].text, "Comment directive with the type checking mode has extra symbols at the end of the line");
CHECK_EQ(result.warnings[5].text, "Comment directive is ignored because it is placed after the first non-comment token");
}
TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf")
{
LintResult result = lint(R"(
--!nolint
--!struct
)");
REQUIRE(0 == result.warnings.size()); // --!nolint disables WrongComment lint :)
}
TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsIfStatAndExpr")
{
LintResult result = lint(R"(
if if 1 then 2 else 3 then
elseif if 1 then 2 else 3 then
elseif if 0 then 5 else 4 then
end
)");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
}
TEST_CASE_FIXTURE(Fixture, "WrongCommentOptimize")
{
LintResult result = lint(R"(
--!optimize
--!optimize me
--!optimize 100500
--!optimize 2
)");
REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
CHECK_EQ(result.warnings[1].text, "optimize directive uses unknown optimization level 'me', 0..2 expected");
CHECK_EQ(result.warnings[2].text, "optimize directive uses unknown optimization level '100500', 0..2 expected");
result = lint("--!optimize ");
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
}
TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
{
LintResult result = lint(R"(
--!nocheck
local _ = `unknown {foo}`
)");
REQUIRE(1 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
{
LintResult result = lint(R"(
local _ = 0b10000000000000000000000000000000000000000000000000000000000000000
local _ = 0x10000000000000000
)");
REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and has been truncated to 2^64");
CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and has been truncated to 2^64");
}
TEST_CASE_FIXTURE(Fixture, "ComparisonPrecedence")
{
LintResult result = lint(R"(
local a, b = ...
local _ = not a == b
local _ = not a ~= b
local _ = not a <= b
local _ = a <= b == 0
local _ = a <= b <= 0
local _ = not a == not b -- weird but ok
-- silence tests for all of the above
local _ = not (a == b)
local _ = (not a) == b
local _ = not (a ~= b)
local _ = (not a) ~= b
local _ = not (a <= b)
local _ = (not a) <= b
local _ = (a <= b) == 0
local _ = a <= (b == 0)
)");
REQUIRE(5 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or add parentheses to silence");
CHECK_EQ(result.warnings[1].text, "not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or add parentheses to silence");
CHECK_EQ(result.warnings[2].text, "not X <= Y is equivalent to (not X) <= Y; add parentheses to silence");
CHECK_EQ(result.warnings[3].text, "X <= Y == Z is equivalent to (X <= Y) == Z; add parentheses to silence");
CHECK_EQ(result.warnings[4].text, "X <= Y <= Z is equivalent to (X <= Y) <= Z; did you mean X <= Y and Y <= Z?");
}
TEST_SUITE_END();