// 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?) -> 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(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, "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();