luau/docs/_pages/lint.md
Arseny Kapoulkine 2c40b7661c
Update lint.md (#634)
Add documentation for IntegerParsing and ComparisonPrecedence lints
2022-08-11 08:42:31 -07:00

17 KiB

permalink title toc
/lint Linting true

Luau comes with a set of linting passes, that help make sure that the code is correct and consistent. Unlike the type checker, that models the behavior of the code thoroughly and points toward type mismatches that are likely to result in runtime errors, the linter is more opinionated and produces warnings that can often be safely ignored, although it's recommended to keep the code clean of the warnings.

Linter produces many different types of warnings; many of these are enabled by default, and can be suppressed by declaring --!nolint NAME at the top of the file. In dire situations --!nolint at the top of the file can be used to completely disable all warnings (note that the type checker is still active, and requires a separate --!nocheck declaration).

The rest of this page documents all warnings produced by the linter; each warning has a name and a numeric code, the latter is used when displaying warnings.

UnknownGlobal (1)

By default, variables in Luau are global (this is inherited from Lua 5.x and can't be changed because of backwards compatibility). This means that typos in identifiers are invisible to the parser, and often break at runtime. For this reason, the linter considers all globals that aren't part of the builtin global table and aren't explicitly defined in the script "unknown":

local displayName = "Roblox"

-- Unknown global 'displaName'
print(displaName)

Note that injecting globals via setfenv can produce this warning in correct code; global injection is incompatible with type checking and has performance implications so we recommend against it and in favor of using require with correctly scoped identifiers.

DeprecatedGlobal (2)

Some global names exist for compatibility but their use is discouraged. This mostly affects globals introduced by Roblox, and since they can have problematic behavior or can break in the future, this warning highlights their uses:

-- Global 'ypcall' is deprecated, use 'pcall' instead
ypcall(function()
    print("hello")
end)

GlobalUsedAsLocal (3)

The UnknownGlobal lint can catch typos in globals that are read, but can't catch them in globals that are assigned to. Because of this, and to discourage the use of globals in general, linter detects cases when a global is only used in one function and can be safely converted to a local variable. Note that in some cases this requires declaring the local variable in the beginning of the function instead of where it's being assigned to.

local function testFunc(a)
    if a < 5 then
        -- Global 'b' is only used in the enclosing function; consider changing it to local
        b = 1
    else
        b = 2
    end
    print(b)
end

LocalShadow (4)

In Luau, it is valid to shadow locals and globals with a local variable, including doing it in the same function. This can result in subtle bugs, since the shadowing may not be obvious to the reader. This warning detects cases where local variables shadow other local variables in the same function, or global variables used in the script; for more cases of detected shadowing see LocalShadowPedantic.

local function foo()
    for i=1,10 do
        -- Variable 'i' shadows previous declaration at line 2
        for i=1,10 do
            print(i)
        end
    end
end

SameLineStatement (5)

Luau doesn't require the use of semicolons and doesn't automatically insert them at line breaks. When used wisely this results in code that is easy to read and understand, however it can cause subtle issues and hard to understand code when abused by using many different statements on the same line. This warning highlights cases where code should either be broken into multiple lines, or use ; as a visual guide.

-- A new statement is on the same line; add semi-colon on previous statement to silence
if b < 0 then local a = b + 1 print(a, b) end

MultiLineStatement (6)

An opposite problem is having statements that span multiple lines. This is good for readability when the code is indented properly, but when it's not it results in code that's hard to understand, as its easy to confuse the next line for a separate statement.

-- Statement spans multiple lines; use indentation to silence
print(math.max(1,
math.min(2, 3)))

LocalUnused (7)

This warning is one of the few warnings that highlight unused variables. Local variable declarations that aren't used may indicate a bug in the code (for example, there could be a typo in the code that uses the wrong variable) or redundant code that is no longer necessary (for example, calling a function to get its result and never using this result). This warning warns about locals that aren't used; if the locals are not used intentionally they can be prefixed with _ to silence the warning:

local x = 1
-- Variable 'y' is never used; prefix with '_' to silence
local y = 2
print(x, x)

FunctionUnused (8)

While unused local variables could be useful for debugging, unused functions usually contain dead code that was meant to be removed. Unused functions clutter code, can be a result of typos similar to local variables, and can mislead the reader into thinking that some functionality is supported.

-- Function 'bar' is never used; prefix with '_' to silence
local function bar()
end

local function foo()
end

return foo()

ImportUnused (9)

In Luau, there's no first-class module system that's part of the syntax, but require function acts as an import statement. When a local is initialized with a require result, and the local is unused, this is classified as "unused import". Removing unused imports improves code quality because it makes it obvious what the dependencies of the code are:

-- Import 'Roact' is never used; prefix with '_' to silence
local Roact = require(game.Packages.Roact)

BuiltinGlobalWrite (10)

While the sandboxing model of Luau prevents overwriting built-in globals such as table for the entire program, it's still valid to reassign these globals - this results in "global shadowing", where the script's global table contains a custom version of table after writing to it. This is problematic because it disables some optimizations, and can result in misleading code. When shadowing built-in globals, use locals instead.

-- Built-in global 'math' is overwritten here; consider using a local or changing the name
math = {}

PlaceholderRead (11)

_ variable name is commonly used as a placeholder to discard function results. The linter follows this convention and doesn't warn about the use of _ in various cases where a different name would cause a warning otherwise. To make sure the placeholder is only used to write values to it, this warning detects the cases where it's read instead:

local _ = 5
-- Placeholder value '_' is read here; consider using a named variable
return _

UnreachableCode (12)

In some cases the linter can detect code that is never executed, because all execution paths through the function exit the function or the loop before reaching it. Such code is misleading because it's not always obvious to the reader that it never runs, and as such it should be removed.

function cbrt(v)
    if v >= 0 then
        return v ^ (1/3)
    else
        error('cbrt expects a non-negative argument')
    end
    -- Unreachable code (previous statement always returns)
    return 0
end

UnknownType (13)

Luau provides several functions to get the value type as a string (type, typeof), and some Roblox APIs expose class names through string arguments (Instance.new). This warning detects incorrect use of the type names by checking the string literals used in type comparisons and function calls.

-- Unknown type 'String' (expected primitive type)
if type(v) == "String" then
    print("v is a string")
end

ForRange (14)

When using a numeric for, it's possible to make various mistakes when specifying the for bounds. For example, to iterate through the table backwards, it's important to specify the negative step size. This warning detects several cases where the numeric for only runs for 0 or 1 iterations, or when the step doesn't divide the size of the range evenly.

-- For loop should iterate backwards; did you forget to specify -1 as step?
for i=#t,1 do
end

UnbalancedAssignment (15)

Assignment statements and local variable declarations in Luau support multiple variables on the left side and multiple values on the right side. The number of values doesn't need to match; when the right side has more values, the extra values are discarded, and then the left side has more variables the extra variables are set to nil. However, this can result in subtle bugs where a value is omitted mistakenly. This warning warns about cases like this; if the last expression on the right hand side returns multiple values, the warning is not emitted.

-- Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence
local x, y, z = 1, 2

ImplicitReturn (16)

In Luau, there's a subtle difference between returning no values from a function and returning nil. In many contexts these are equivalent, but when the results are passed to a variadic function (perhaps implicitly), the difference can be observed - for example, print(foo()) prints nothing if foo returns no values, and nil if it returns nil.

To help write code that has consistent behavior, linter warns about cases when a function implicitly returns no values, if there are cases when it explicitly returns a result. For code like this it's recommended to use explicit return or return nil at the end of the function (these have different semantics, so the correct version to use depends on the function):

local function find(t, expected)
    for k,v in pairs(t) do
        if k == expected then
            return v
        end
    end
    -- Function 'find' can implicitly return no values even though there's an explicit return at line 4; add explicit return to silence
end

DuplicateLocal (17)

Luau syntax allows to use the same name for different parameters of a function as well as different local variables declared in the same statement. This is error prone, even if it's occasionally useful, so a warning is emitted in cases like this, unless the duplicate name is the placeholder _:

function foo(a, b, a) -- Function parameter 'a' already defined on column 14
end

function obj:method(self) -- Function parameter 'self' already defined implicitly
end

local x, y, x = v:GetComponents() -- Variable 'x' already defined on column 7

FormatString (18)

Luau has several library functions that expect a format string that specifies the behavior for the function. These format strings follow a specific syntax that depends on the question; mistakes in these strings can lead to runtime errors or unexpected behavior of the code.

To help make sure that the strings used for these functions are correct, linter checks calls to string.format, string.pack, string.packsize, string.unpack, string.match, string.gmatch, string.find, string.gsub and os.date and issues warnings when the call uses a literal string with an incorrect format:

-- Invalid match pattern: invalid capture reference, must refer to a closed capture
local cap = string.match(s, "(%d)%2")

-- Invalid format string: unfinished format specifier
local str = ("%d %"):format(1, 2)

Note that with the exception of string.format this only works when the function is called via the library, not via the method call (so prefer string.match(s, "pattern") to s:match("pattern")).

TableLiteral (19)

Table literals are often used in Luau to create objects or specify data. The syntax for table literals is rich but invites mistakes, for example it allows duplicate keys or redundant index specification for values already present in the list form. This warning is emitted for cases where some entries in the table literal are ignored at runtime as they're duplicate:

print({
    first = 1,
    second = 2,
    first = 3, -- Table field 'first' is a duplicate; previously defined at line 2
})

UninitializedLocal (20)

It's easy to forget to initialize a local variable and then proceed to use it regardless. This can happen due to a typo, or sometimes it can happen because the original variable definition is shadowed. When a local variable doesn't have an initial value and is used without ever being assigned to, this warning is emitted:

local foo

if foo then -- Variable 'foo' defined at line 1 is never initialized or assigned; initialize with 'nil' to silence
	print(foo)
end

DuplicateFunction (21)

This warning is emitted when a script defines two functions with the same name in the same scope.

The warning is not produced when the functions are defined in different scopes because this is much more likely to be intentional.

function foo() end
function foo() end -- Duplicate function definition: 'foo' also defined on line 1

-- OK: the functions are not defined in the same scope.
if x then
    function bar() end
else
    function bar() end
end

DeprecatedApi (22)

This warning is emitted when a script accesses a method or field that is marked as deprecated. Use of deprecated APIs is discouraged since they may have performance or correctness issues, may result in unexpected behavior, and could be removed in the future.

function getSize(i: Instance)
    return i.DataCost -- Member 'Instance.DataCost' is deprecated
end

TableOperations (23)

To manipulate array-like tables, Luau provides a set of functions as part of the standard table library. To help use these functions correctly, this warning is emitted when one of these functions is used incorrectly or can be adjusted to be more performant.

local t = {}

table.insert(t, 0, 42) -- table.insert uses index 0 but arrays are 1-based; did you mean 1 instead?

table.insert(t, #t+1, 42) -- table.insert will append the value to the table; consider removing the second argument for efficiency

DuplicateCondition (24)

When checking multiple conditions via and/or or if/elseif, a copy & paste error may result in checking the same condition redundantly. This almost always indicates a bug, so a warning is emitted when use of a duplicate condition is detected.

assert(self._adorns[normID1] and self._adorns[normID1]) -- Condition has already been checked on column 8

MisleadingAndOr (25)

In Lua, there is no first-class ternary operator but it can be emulated via a and b or c pattern. However, due to how boolean evaluation works, if b is false or nil, the resulting expression evaluates to c regardless of the value of a. Luau solves this problem with the if a then b else c expression; a warning is emitted for and-or expressions where the first alternative is false or nil because it's almost always a bug.

-- The and-or expression always evaluates to the second alternative because the first alternative is false; consider using if-then-else expression instead
local x = flag and false or true

The code above can be rewritten as follows to avoid the warning and the associated bug:

local x = if flag then false else true

CommentDirective (26)

Luau uses comments that start from ! to control certain aspects of analysis, for example setting type checking mode via --!strict or disabling individual lints with --!nolint. Unknown directives are ignored, for example --!nostrict doesn't have any effect on the type checking process as the correct spelling is --!nonstrict. This warning flags comment directives that are ignored during processing:

--!nostrict
-- Unknown comment directive 'nostrict'; did you mean 'nonstrict'?"

## IntegerParsing (27)

Luau parses hexadecimal and binary literals as 64-bit integers before converting them to Luau numbers. As a result, numbers that exceed 2^64 are silently truncated to 2^64, which can result in unexpected program behavior. This warning flags literals that are truncated:

-- Hexadecimal number literal exceeded available precision and has been truncated to 2^64 local x = 0x1111111111111111111111111111111111111


## ComparisonPrecedence (28)

Because of operator precedence rules, not X == Y parses as (not X) == Y; however, often the intent was to invert the result of the comparison. This warning flags erroneous conditions like that, as well as flagging cases where two comparisons happen in a row without any parentheses:

-- not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence if not x == 5 then end

-- X <= Y <= Z is equivalent to (X <= Y) <= Z; wrap one of the expressions in parentheses to silence if 1 <= x <= 3 then end