// 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") { ScopedFastFlag sff{"LuauLintGlobalNeverReadBeforeWritten", true}; 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") { ScopedFastFlag sff{"LuauLintGlobalNeverReadBeforeWritten", true}; 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") { ScopedFastFlag sff{"LuauLintGlobalNeverReadBeforeWritten", true}; 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") { ScopedFastFlag sff{"LuauLintGlobalNeverReadBeforeWritten", true}; 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") { ScopedFastFlag sff{"LuauLintGlobalNeverReadBeforeWritten", true}; 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); 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}; 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?) -> 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(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, {}, "Test"});
persist(instanceType);
typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};
getMutable