Luau

A fast, small, safe, gradually typed embeddable scripting language derived from Lua


Why Luau?
Syntax
Sandboxing
Compatibility
Linting
Type checking
Performance


Linting

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
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
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

LocalShadowPedantic (17)

This warning extends LocalShadow by also warning about cases where a local variable shadows a local variable with the same name from a parent function, or when it shadows a builtin global. This warning tends to be noisy and as such is disabled by default.

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
})