The qbx-lint CLI and qbx-lua-ls language server share the parser, analysis rules and
qbxlint.toml formatting settings described here. Command-line options apply to the CLI;
for editor settings and LSP requests, see the
language-server protocol reference.
The parser handles Lua 5.4 and CfxLua extensions, including backtick hash literals, optional
chaining (value?.field), compound assignments (count += 1), local a, b in object, set
constructors ({ .police, .ambulance }), defer blocks, and /* ... */ comments.
qbx-lint loads one configuration per invocation. Without --config, it searches upward from
the first input file or directory for qbxlint.toml or .qbxlint.toml. An explicit configuration
path can be relative or absolute. Exclusion and override patterns use the configuration directory
as their base.
All settings are optional. Unknown keys and rule names are errors.
exclude = ["web/**", "**/vendor/**"]
ignore_diagnostics = ['\[standalone\]/', 'third_party/']
globals = ["SomeRuntimeGlobal"]
ignore_unused_prefix = "_"
strict_classes = false
strict = false
[rules]
"unused-argument" = "off"
"fivem/citizen-prefix" = "warning"
"qbox/prefer-cache" = "info"
[imports]
shared = ["@my_lib/shared/**.lua"]
[[overrides]]
files = ["tests/**", "**/*.spec.lua"]
globals = ["describe", "it"]
rules = { "undefined-global" = "off" }
[format]
indent_width = 4
use_tabs = false
line_width = 120
quote_style = "preserve"| Setting | Meaning |
|---|---|
exclude |
Additional glob patterns for paths to skip. Skipped files are not analyzed at all, so other files do not see their globals, exports or events. |
ignore_diagnostics |
Gitignore-style patterns for paths that are analyzed but never reported. |
globals |
Names supplied at runtime that the linter cannot discover. |
imports |
Files every resource runs without an fxmanifest.lua entry, grouped as shared, client and server. See Runtime imports. |
ignore_unused_prefix |
Locals and arguments with this prefix are exempt from unused checks; defaults to _. |
strict_classes |
Makes every ---@class without (loose) strict, as if it said (strict). Only classes declared in files whose diagnostics are reported follow it. See Strict classes. Defaults to false. |
strict |
Makes the type rules of qbx-lua-ls report what TypeScript's strict mode does beyond lua-language-server, such as n + 1 for a number?. See Strict mode. Defaults to the editor's setting, which is false unless it is turned on. |
rules |
Per-rule levels: off, hint, info, warning (or warn), and error. |
overrides |
Per-file globals, rule levels, strict and side, selected by the files patterns, and imports for the resources whose fxmanifest.lua the patterns match. Later matching overrides take precedence for rule levels, strict and sides. See Sides of unlisted scripts. |
format |
Formatting options, shown with their defaults above. |
The default exclusions include node_modules, .git, and [builders] directory contents.
Directory traversal also skips hidden directories. It follows directory symlinks, as FiveM does,
except links into the folder it traverses or to a folder above it, and reads a folder that several
links lead to once, also when one leads to a folder inside the one another leads to.
ignore_diagnostics is for code you do not maintain, such as third-party resources in a server
folder. Matching files are still parsed and indexed, so their globals, exports and events keep
resolving in your own files, and the language server still offers definitions and completion for
them. Nothing is reported for a matching file, not even syntax errors, and that includes matching
fxmanifest.lua and locale files. Formatting is not affected.
Patterns follow .gitignore rules, relative to the configuration directory:
| Pattern | Matches |
|---|---|
vendor/ |
Every directory named vendor, at any depth |
/vendor/ |
Only the vendor directory next to the configuration file |
resources/vendor |
A path relative to the configuration directory, because it contains a / |
*.min.lua |
Matching files at any depth |
!vendor/patched.lua |
Reports this file again although an earlier pattern ignores it |
FiveM category folders need escaped brackets: [standalone] is a character class that matches
one letter, while \[standalone\] matches the folder. Write such patterns as TOML literal strings
in single quotes, because \[ is not a valid escape in a double-quoted TOML string.
--rule CODE=LEVEL changes a rule's base level for the invocation. Matching file overrides are
applied afterward. --min-severity hint includes hints, which the default info threshold hides.
--max-warnings 0 makes reported warnings fail a lint run.
Configure qbx-lint with qbxlint.toml. Projects that are already set up for LuaLS or EmmyLua get
a fallback: when no qbxlint.toml or .qbxlint.toml exists in any parent directory, the nearest
directory with .luarc.json, .luarc.jsonc or .emmyrc.json supplies the few settings that
have an equivalent here, and the CLI names the files it used on stderr. Files found together in
that directory are merged. A qbxlint.toml replaces the fallback entirely, so add one as soon as
qbx-lint needs anything beyond it. --config also accepts these files.
| LuaLS / EmmyLua setting | Used as |
|---|---|
diagnostics.globals |
globals, without the names qbx-lint already knows: runtime globals, natives, and globals of imports such as @ox_lib/init.lua, so the manifest and client/server checks still apply to them |
diagnostics.disable |
off for undefined-global, lowercase-global, global-in-nil-env, unused-local, unused-function, unused-label, unused-vararg, redefined-local, unreachable-code, empty-block, unbalanced-assignments, duplicate-index, duplicate-set-field, count-down-loop, missing-parameter, redundant-parameter, undefined-doc-name, undefined-doc-param, duplicate-doc-alias, duplicate-doc-field, missing-global-doc, missing-local-export-doc, incomplete-signature-doc, missing-fields, assign-type-mismatch, invisible, param-type-mismatch, return-type-mismatch, missing-return, redundant-return-value, discard-returns, cast-type-mismatch, cast-local-type, no-unknown, need-check-nil, inject-field, newline-call, newfield-call, ambiguity-1, trailing-space, redundant-return, duplicate-doc-param, doc-field-no-class, unknown-operator, unknown-cast-variable, unknown-diag-code, close-non-object and circle-doc-class; EmmyLua's unused covers the unused-* rules |
diagnostics.severity |
Levels for the same codes (Error, Warning, Information, Hint, with or without a trailing !) |
diagnostics.neededFileStatus |
For the same codes, Any or Opened turns a rule that is off by default, such as no-unknown or missing-global-doc, on as a warning, unless diagnostics.severity gives its level. None turns a rule off, whatever its severity. With or without a trailing ! |
workspace.ignoreDir |
Exclusions. LuaLS entries are gitignore-style patterns; .emmyrc.json entries are directories from the root |
workspace.ignoreGlobs |
Exclusions, as glob patterns |
Codes that only share a name with a rule here, such as deprecated, are ignored, as are all other
settings. None of them stands for strict: LuaLS's type.weakNilCheck and
type.weakUnionCheck only make its checks laxer than its defaults. Keys may be dotted
("diagnostics.globals"), nested, or prefixed with Lua.. Comments and trailing commas are
accepted. Formatting keeps its defaults; in the language server, the
editor's indentation settings still apply. Discovery skips a file or
exclusion pattern it cannot read and says so on stderr; pass the file with --config to make that
an error.
Annotations written for fivem-lls-addon keep
their meaning: the type names of its runtime library, EventHandler, vector, json_options,
json_encode_state and msgpack_options, are declared beside the runtime's own, and the classes
the two share carry its fields.
Use a rule code to keep the suppression specific:
-- qbx-lint: disable-next-line unused-local
local keepMe = 1
print(externalValue) -- qbx-lint: disable-line undefined-global
-- qbx-lint: disable fivem/citizen-prefix
Citizen.Wait(0)
-- qbx-lint: enable fivem/citizen-prefixOmitting rule codes suppresses all findings in the selected line or range, except syntax errors.
Comma-separated and space-separated rule codes are accepted. LuaLS-style directives are also
recognized. As in LuaLS, their codes end at the first one that no comma follows, so the words after
it, as in ---@diagnostic disable-next-line: unused-local (set later), are a note:
---@diagnostic disable-next-line: undefined-global
print(fromSomewhereElse)-- luacheck: ignore is recognized as a broad suppression: on its own line it disables subsequent
findings; after code it suppresses that line. Luacheck's numeric codes and name filters are not
translated. Prefer explicit qbx-lint rule codes when narrowing a suppression.
A code that is neither a rule here nor one of lua-language-server's diagnostics or syntax errors
suppresses nothing, and unknown-diag-code reports it, with the rule it most likely misspells.
lua-language-server's codes are accepted even when no rule here checks the same, so comments
written for it stay quiet.
For a file inside a resource, the nearest fxmanifest.lua or __resource.lua supplies script
paths and sides. The analysis combines:
- Lua and CfxLua runtime definitions from the bundled stubs.
- Bundled FiveM native signatures and client/server metadata, including
N_0x...names. - Globals defined by scripts available on the file's side.
- Globals from manifest imports such as
@resource/file.lua, and from configuredimports. - Configured
globals.
An import is read from a sibling resource when available. Otherwise, known imports supply their
usual globals, such as lib and cache for @ox_lib/init.lua, MySQL for
@oxmysql/lib/MySQL.lua, and qbx for @qbx_core/modules/lib.lua.
Some resources run code from another resource without an fxmanifest.lua entry, for example a
loader that calls load(LoadResourceFile(...)) for each file of a shared library. imports lists
those files as @resource/path patterns, grouped by the side they run on, and each resource then
sees their globals as if its manifest imported them:
[imports]
shared = ["@my_lib/shared/**.lua"]
client = ["@my_lib/client/**.lua"]
server = ["@my_lib/server/**.lua", "@oxmysql/lib/MySQL.lua"]Paths may use manifest globs, where * stays within a folder and ** crosses folders. Top-level
imports apply to every resource. To limit them to some resources, put them in an override: its
imports apply to each resource whose fxmanifest.lua its files patterns match, since all
scripts of a resource share their globals. Keeping those resources in one category folder makes
the pattern short:
[[overrides]]
files = ["resources/[[]my_lib[]]/**"]
[overrides.imports]
shared = ["@my_lib/shared/**.lua"]In files patterns, [[] and []] match literal brackets; [my_lib] alone would be a character
class. Server scripts do not see the globals of client imports, and client scripts do not see
those of server imports. Excluded files add nothing.
The files a resource loads this way see its globals in turn. An import, in a manifest or in
imports, runs the Lua files of the other resource that its manifest does not list as scripts and
that sit in the folder of the imported file or below, since such an import is usually a loader;
for a glob, the folder is the part before the first *. A phone resource whose manifest has
shared_script '@bridge/phone/load.lua' thus runs the unlisted files under bridge/phone/, and
they see what the scripts and imports of phone declare, besides the globals of bridge. When
every resource that runs such a file is opaque, as an escrowed one is, its globals are not checked;
see Escrowed, obfuscated and mixed-language resources.
Files not listed as manifest scripts, including modules loaded through require or lib.load,
use globals from both sides unless an override gives them a side. Files outside a resource are
checked without a manifest environment.
A resource whose loader runs its own files at runtime, listing them only under files, leaves
their side unknown. An override's side (client, server or shared) supplies it:
[[overrides]]
files = ["client/**"]
side = "client"
[[overrides]]
files = ["server/**"]
side = "server"The side only applies to Lua files inside a resource that its manifest does not list as scripts;
a manifest entry always wins. Such a file is then checked like a script of that side: it sees the
globals, natives and (server) or (client) annotations of that side, and its own globals only
reach scripts of that side. The side is never guessed from folder names.
Recognized runtime guards narrow side checks within a file. Examples include
IsDuplicityVersion(), a local flag initialized from it, lib.context == 'server', a test for a
library only the server has, as if os then, and a local that holds the name of the side, as
ox_lib's local context = IsDuplicityVersion() and 'server' or 'client' does, compared with
'server' or 'client'.
An early return such as if not IsDuplicityVersion() then return end narrows the following code
to the server, isServer and os.time() narrows the right side of the and, and
isServer and 0 or PlayerPedId() the right side of the or to the client, unless the value between
them is a literal nil or false. Event registrations inside these regions use that effective side,
and so do natives with another signature on the server, such as GetAllVehicles, which returns a
Vehicle[] there.
FiveM only loads the io and os libraries on the server, so ox_lib tests for one, as in
os and os.time() or GetCloudTimeAsInt(), to tell the side. Scripts whose side is unknown are not
reported. io and os, natives and the other runtime globals of one side, such as
TriggerClientEvent, and the globals that the resource only defines in scripts of one side, such
as a function of client/utils.lua, are reported in scripts or guarded code of the other side, and
in code of a shared script that runs while it loads: outside functions, or in a thread that
CreateThread or SetTimeout starts then. A function of a shared script is left to the side that
calls it, as the callbacks of a shared config usually only run on the client. With
strict, as TypeScript knows no sides, they are reported anywhere in a shared
script that no guard keeps on their side. In client scripts, os and io are not completed and have no hover. Their functions are FiveM's:
there is no os.exit, io.input, io.output or io.read, and CfxLua adds os.createdir,
io.readdir and timers such as os.nanotime.
Where a global is defined in more than one place, it takes the type of one definition. One that is
or may be a class comes first, as lua-language-server lets a declared type win, then one that sets a
global table. Among those, the nearest wins: in the same file, then in another file of the same
resource, then in another resource, then in a definition file. At the same distance, a class
declared with ---@type comes before a value that may be of it, as Config = Config or {} is, and
the nearest folder before the others. Definitions that assign nil or a value of unknown type only
count when there is no other, or in the file that makes them. Hover names that definition, and go
to definition lists the nearest first.
Where client and server scripts define a global function differently, as two GetJobs, a call
from code that runs on both sides takes the nearest definition of each side, and neither when they
differ: what it returns is unknown and passes every type check. Inside one of these guards the call
takes the nearest definition of that side, as long as its arguments fit it.
qbx-lua-ls reads type libraries the way LuaLS reads its library: a file that belongs to no
resource is a definition file when it comes from a folder of the library setting, or when it is a
workspace file with a ---@meta line above its first statement. A ---@meta further down does
not count, here or for missing-return. A library folder inside the workspace is indexed as
workspace files first, so its files need ---@meta. Other workspace files outside resources keep
their globals to themselves.
The globals, classes and aliases of definition files reach the scripts of every resource, unless
the resource declares the same name itself, imports it, or gets it from the bundled stubs: a
resource's own ---@alias VehicleData wins over a library's ---@class VehicleData, and its own
vehicle = GetVehiclePedIsIn(ped, false) over a library's vehicle. Two things limit where a
definition file applies:
- Its side comes from an override's
side, or else from the words of its file name or nearest folder name:clientorclfor the client,serverorsvfor the server, as inserver_vehicle.lua,cl_main.luaor aclientfolder.shared,common, or both sides make it shared, and a name without any of these leaves the decision to the folder above it. - A file inside a folder named after a resource, one the workspace has or imports from, or one of
the well-known imports such as
ox_coreorqbx_core, declares what that resource's imports provide. Its globals only reach resources that load a file of that resource on the script's side, through the manifest, configuredimports,lib.loadorrequire. A library holdingox_core/server_vehicle.luawithvehicle = {}thus declaresvehiclefor the server scripts of resources that import@ox_core/lib/init.lua, andundefined-globalstill reports it anywhere else.
The globals of well-known imports, such as lib or MySQL, are reported by
fivem/import-not-declared without their import, whatever a definition file declares. Folders
above the workspace or library root that holds a file do not count for its side or resource. The
CLI does not read definition files outside resources.
By default the type rules of qbx-lua-ls report what lua-language-server reports on the same code.
strict = true in qbxlint.toml, or diagnostics.strict in the editor's settings
(qbxLua.diagnostics.strict in VS Code), makes them also report what TypeScript's strict mode
reports, for code that wants more checks. A strict in qbxlint.toml, also one that an
[[overrides]] entry sets for some files, takes precedence over the editor's. It changes how rules
check, not which ones run: rules that are off by default, such as no-unknown, are turned on in
[rules].
strict = true
[[overrides]]
files = ["resources/[standalone]/**"]
strict = falseWith strict:
need-check-nilalso reports arithmetic, concatenation,#and a<,<=,>or>=comparison with a value that may benil, and a numericforbound that may be. An item that a loop from 1 to the#of a table reads, asrows[i]infor i = 1, #rows do, has the type the table declares for it,nilincluded, and a function without@returnalso gives thenilof running past the end of its body, and that of areturn nilwhen it is called throughexports. Fields, keys and the values of calls are checked as locals are, asblip.label:upper()is for alabel? string, and so isnilwritten out. See Nil checks.- A loop over a table whose type declares its values, a class, an array, a map or a tuple, as
pairs(Config.Blips)is for aConfigtyped with---@type, gives its variables those types, as TypeScript does, for the checks that only count declared types. A table that a constructor builds and code fills, aslocal seen = {}, declares nothing. A global that one of its definitions declares as a class has that type for them too, asConfighas besideConfig = Config or {}. fivem/native-wrong-sidereports the names of one side anywhere in a shared script that no guard keeps on that side, not only in the code that runs while it loads. See Globals and client/server context.- A local without
---@typeor@parammay hold thenilthat code running at other times gives it, such ascoords = nilin another function, and a parameter of a callback marked optional, such as thebody?: stringofPerformHttpRequest, may holdnil. lua-language-server leaves both out, and so doneed-check-nilandparam-type-mismatchwithoutstrict. param-type-mismatchcounts thenilandfalseof a value read from a field or a key, asself.handlefor ahandle? number, which lua-language-server leaves out because it does not narrow fields, and so doassign-type-mismatchandreturn-type-mismatch. A native takes no number or boolean for a string parameter, also one written out or a player's server id, and numbers and booleans no longer pass for each other. See Function arguments.assign-type-mismatchreportsfield = nilfor a field whose type does not allownil. See Return values.unreachable-codereports the code after a call oferrororos.exitof its own, as TypeScript reports the code after athrow.cast-local-typereports clearing a local declared with a table, ascache = nilafterlocal cache = {}, thenilorfalseof a field, as intotal = counter.count, and anilthat no annotation declares, such as one a function without@returngives. It also checks the parameters of a function passed for a function type. See Local types.missing-fieldsrequires the fields of shapes, such as{ name: string }and the aliases of them, and of classes given type arguments, such asList<string>. See Typed variables.missing-fieldsandassign-type-mismatchcheck a table assigned to a global without a---@typeof its own, asPlayerData = {}for aPlayerDataan annotation types elsewhere, and a value stored through a global, asConfig.Debug = 1, which lua-language-server leaves out.inject-fieldandundefined-fieldcheck tables whose type no annotation declares. A table that a constructor with fields builds has those alone, at the top of a file as inside a function, so a field set through its local later, or through a loop variable over such tables, is aninject-field, and a field set through a local is still missing from the local's type where it is read. A local without an annotation that is given a value of unknown type keeps the type it is declared with, soPlayer.PlayerDataafterlocal Player = ''andPlayer = GetPlayer()reads astring. See Injected fields and Undefined fields.
missing-parameter compares calls with the LuaCATS annotations of the function they call. A
parameter is required when a @param documents it with a type that does not allow nil: no ?
after its name, and a type other than nil, any, unknown, a union with nil, or an @alias
that includes one of those. Undocumented parameters are optional, so a function without @param
lines is never reported. Every parameter before the last required one has to be passed as well.
@overload and ---@type fun(...) signatures count, and the one that needs the fewest arguments
decides. An @overload (server) fun(...) or @overload (client) fun(...) only counts for calls
on that side, as decided by the script's manifest side and any IsDuplicityVersion() or
lib.context guard around the call; shared code counts both.
redundant-parameter reports the arguments a call passes beyond the parameters of the function,
whose values are lost. Every parameter counts, documented or not, and a ... takes any number of
arguments. Here the signature that takes the most arguments decides. A call or ... among the
extra arguments counts as one:
local function noop() end
noop(function() end) -- 'noop' is called with 1 argument, but takes noneAs in LuaLS, every definition of a global counts, and a native or runtime function that a resource
redefines keeps its own signature beside the redefinition. A file of the resource that no manifest
runs, that no other resource loads and that the configuration gives no side, such as a test that
stubs HasModelLoaded, adds no signature unless it is a ---@meta definition file: the functions it
defines may be any.
A function @field that repeats the name of an unscoped one is another signature of it, as LuaLS
reads it, rather than a second field: a call picks the signature its arguments fit, and hover shows
the descriptions of both. Repeated with (server) or (client), the signature only applies on
that side. Fields that are themselves scoped to a side stay apart, and repeated fields that are not
functions are kept as they are.
An ---@extend line adds a signature to a function from another file, as
the language server's annotations
describe. Both rules count it beside the definitions of a global function or a field of a global
table, for calls on the side of its file and of its (server) or (client) attribute. It adds
nothing to a name the resource does not define, such as a native, which keeps the checks it has.
Calls are checked when the function is:
- a local function, or a local that is only ever assigned functions;
- a local, or a field of a local table, that a
---@type fun(...)above its declaration or an assignment types, also through an@aliasor with?, whatever value it is given; - a global function defined by a script on the caller's side, or by one of its imports;
- a field of a global table, or of a local table in the same file, that a
function Utils.round(x)orfunction Utils:round(x)statement or an assignment defines.
A method defined with : and called with . needs self as its first argument. When a name has
several definitions, for example a client and a server Notify, a call is compared with those its
side can reach, and is not checked when any of them is not a function. A : call passes its
receiver as the first argument whatever that parameter is named, so function Locale.new(_, opts)
called as Locale:new(opts) receives both. Calls whose last argument is ... pass an unknown
number of arguments, so missing-parameter skips them, and qbx-lint skips those that end with
another call too. qbx-lua-ls counts the values such a call returns when each signature its function
may use returns the same number of them and none ends with ...T, so setVolume(tonumber(x))
passes one argument, while a call of a function that returns a different number of values in an
@overload may pass any number. Neither rule checks
natives, runtime functions, exports, or methods of objects returned by calls, such as
GetPlayer(source):setJob(job).
qbx-lua-ls also checks the payload of calls to ---@callback await and trigger wrappers. The
values passed in the wrapper's ... go to the handler registered under the name the call passes,
on the other side, so they have to cover that handler's required parameters, and
redundant-parameter reports those it does not take. A handler registered outside the client
receives the calling player first, so that parameter is not counted:
---@param num1 number
---@param num2 number
RegisterServerCallback('add', function(source, num1, num2) ... end)
-- Callback 'add' is called with 0 arguments, but needs 2; 'num1' (number) will be nil
AwaitServerCallback('add')When several handlers are registered under the name, the one that needs the fewest values decides,
and for redundant-parameter the one that takes the most.
param-type-mismatch compares each argument with the type of its parameter, as
assign-type-mismatch compares a value with the type of its variable: a different kind of value,
or a literal the type does not list, is reported. Each type a union lists has to fit, so a
string|number is no string, as in lua-language-server.
---@param mode "fast" | "slow"
---@param count integer
local function run(mode, count) end
run("instant", 1) -- Cannot assign `"instant"` to parameter `mode` of type `"fast"|"slow"`
run("fast", "2") -- Cannot assign `string` to parameter `count` of type `integer`A parameter typed any or unknown takes any value, also beside other types, as in
unknown|string.
It checks every call to a function whose parameters have types, also runtime functions, exports,
methods of classes and fields typed fun(...). A call is compared with each definition the side
of the call reaches, with their @overloads and the signatures ---@extend lines add, and passes when a signature that takes as many
arguments as it passes takes each of them; when none takes that many, any of them may. A : call
passes its receiver as the first argument, as in Lua, except through exports, whose proxy drops
it. The payload of a ---@callback wrapper call is compared with the handlers registered under its
name.
An argument has a type when something declares it, as for impossible-comparison: a literal, an
operator, an annotation, a stub, or the values a local is declared with or assigned that reach the
call. A literal stored in a local counts by its kind only: local mode = 'dev' is a setting to
change, so it passes for "fast" | "slow", while local count = 5 is still no string. A
--[[@as T]] right after an argument casts it.
As in LuaLS, nil, written out or as the ? of a string?, needs a parameter that takes it: one
marked optional, or typed with nil, any or unknown. A guard such as if name then rules the
nil out first. false is a boolean like any other, also where code passes it to skip a
parameter, as in AddItem(source, item, 1, false, info). Also as in LuaLS, the nil and false
of an argument read from a field or a key, as self.handle or data['name'], and of self are
left out, as no guard narrows them for it, and so is the nil that the ? of an optional
parameter of a callback adds, as for body in
PerformHttpRequest(url, function(status, body) json.decode(body) end). With
strict, as in TypeScript, both count.
---@param name string
local function greet(name) end
---@type string?
local maybe
greet(maybe) -- Cannot assign `string?` to parameter `name` of type `string`
greet(nil) -- Cannot assign `nil` to parameter `name` of type `string`Natives are checked as the runtime passes their arguments on:
- Numbers and booleans pass for each other, as a
BOOLis an integer to natives:0and1pass for aboolean, andfalsefor an integer. - A string parameter takes
nilor astring?, and0andfalse, which the native wrappers turn into NULL, as inDrawMarker(..., nil, nil, false), and a number or boolean written out, which they turn into the text it is written as, as inSetConvarReplicated('volume', 30). A parameter for the server id of a player, such as theplayerSrcofDropPlayer, takes any number. Other numbers and booleans are reported:ReleaseNamedRendertarget(GetHashKey(name))passes a hash, not the name. - With
strict, as TypeScript reads the declared types, numbers and booleans no longer pass for each other, and a string parameter takes no number or boolean at all, whilenil, hashed strings and vectors still pass, and so does a number for the server id of a player, which only the native data calls a string. - A hash parameter takes a string, which the wrappers hash.
- A vector fills a number parameter for each of its parts, as in
SetEntityCoords(ped, coords, ...). The values after a value of unknown type that may be a vector are not checked when the call passes fewer values than the native takes, since they may go to later parameters.
The values a native writes through its pointers are returned after its result, as FiveM's Lua
wrappers return them: local _, size = GetGroupSize(group). The wrappers take a pointer as an
argument only when it is the native's one pointer and comes last, as an optional initial value,
like the entity of GetEntityPlayerIsFreeAimingAt(player, entity). Natives that release a handle,
such as DeleteEntity, still need theirs.
The functions natives call back take the parameters their documentation gives: the handler of
RegisterCommand takes source: integer, args: string[], rawCommand: string, and those of
AddStateBagChangeHandler, AddConvarChangeListener, RegisterConsoleListener and
SetHttpHandler take theirs, the last a request and response with their fields. The tables
that natives return hold what the runtime puts in them: GetActivePlayers() gives a Player[],
GetGamePool(name) an Entity[], GetPedDecorations(ped) [Hash, Hash] pairs and
GetRegisteredCommands() tables with a name, resource and arity, while
GetStateBagValue gives the stored value, of any type.
Other parameters of natives do not take nil: DoesEntityExist(entity) with an Entity? is
reported. Only clear cases count, and the rest is left alone:
- Types inferred from assigned values, such as that of
Config.Value = ''. - Parameters typed with a generic of the function called: the arguments of the call bind it, which
declares nothing, unless the parameter is typed with the generic alone and another argument
declares it.
table.insert(list, value)takes forvaluethe type of the entries of alistdeclared asnumber[], as TypeScript checkspush<T>(list: T[], value: T), whilelist: T[], which decides the generic, is left out.nildecides no generic, and such a parameter takes it, as TypeScript adds thenullof an argument to the type it infers. A function that a callee passes to a callback, such asresolveoffun(resolve: fun(value: T)), takes and returns what the other arguments of that call declare for the generic, as thebooleanofPromise:New('boolean', function(resolve) end)for a`T`, and any value for a generic they leave unbound. What a generic function returns is declared as far as the declared types of the arguments bind its generics, as thestring?offirst(names)forfun(list: V[]): V?and anamesdeclared asstring[]. - Parameters typed with the name of a native handle such as
Vehicle, which resources also declare as classes. - Calls through globals in an opaque resource, such as an escrowed one; see Escrowed, obfuscated and mixed-language resources.
A strict class takes only the fields it declares. Mark one with (strict), or LuaLS's (exact),
which means the same here:
---@class (strict) Test
---@field test string
local Test = {}
function Test:greet() end
---@type Test
local abc = {
test = "2543",
other = true, -- undeclared-field
}
abc.more = 1 -- undeclared-fieldqbx-lua-ls reports undeclared-field for a name that a table constructor typed as the class sets,
for fields that assignments and function value:name() statements set on values of the class,
such as a ---@type local, a parameter or self, and for fields that code reads from them, like
abc.nope or abc:nope(). A field counts as declared when an @field of the class or one of its
parents names it, or when it is set on the table the ---@class annotation declares, like greet
above; fields set through values of the class do not count. A [string] index, such as
---@field [string] any, takes any name, and an index of string literals, such as
---@field ['a'|'b'] integer or an alias of them, takes those names. A table type named as a
parent, like { id: integer } or table<string, any>, declares its fields and indices for the
class, so Name : table<string, any> takes any name too, while a parent that is neither a class
nor such a table type, like a plain table, takes any key.
A table that setmetatable gives a metatable whose __index is the class table, as
setmetatable(obj, Test) does after Test.__index = Test, is a value of the class from the call
on: obj.more = 1 after the call is reported, and so is self.more = 1 after
local self = setmetatable({}, Test). What is set on obj before the call is not. A table built
with fields of its own, like setmetatable({ id = 1 }, Test), keeps them, so its fields are not
checked against the class.
Other keys need an index of their type. With only ---@field [string] number, abc[1],
abc[1] = x and array entries such as { 'a' } are reported, since the class has no integer
keys. A key held in a string variable, as in abc[key], may name a field and is not reported.
A local without ---@type that is never assigned again reads the fields that the literals it holds
name, as lua-language-server reads them: with local gender = isMale and "male" or "female",
byGender[gender] has the type of the male and female fields, although gender is a string.
Fields keyed by an integer or boolean literal, such as the entries of a tuple, are declared one by one:
---@class (strict) Employee
---@field [1] number Source
---@field [2] string Character name
---@type Employee
local employee = { 1, "Jane" }
employee[3] = true -- undeclared-fieldArray entries are checked by their position, so { "Jane", 1 } is an assign-type-mismatch for
both entries. A key held in an integer variable, as in employee[i], may be either field, so it
is neither reported nor checked against their types.
Index types also decide what a field without its own @field holds, for every class and not only
strict ones: with ---@field [string] number, abc.other is a number, and { other = true } and
abc.other = true are an assign-type-mismatch.
A generic class gives its fields the type arguments a value is declared with. With
---@class List<T> and ---@field first T, first of a ---@type List<string> local is a
string, so { first = 1 } is an assign-type-mismatch. A parent passes its type arguments on,
as in ---@class Names : List<string>, and a generic ---@alias Box<T> { value: T } reads
Box<integer> the same way. A parameter left without an argument, like the R of Pair<string>
for Pair<L, R>, stays R, and so do all of them where the class is named without arguments,
as in ---@type List, just as the table the ---@class annotation declares and self in its
methods keep them, being List<T>. A field of such a type takes any value but must still be
given, and a method's own ---@generic T binds from the call. The annotations of its methods may
name its type parameters in any file, as in lua-language-server. An alias named without its type
arguments, as in ---@type Box, reads its parameters as unknown instead, as lua-language-server
does, and so does a table type that a class names as a parent, like the { [number]: T } of
---@class Array<T> : { [number]: T } for a ---@type Array.
With strict_classes = true, every class is strict unless it is marked (loose). That default
only covers classes declared in workspace files outside exclude and ignore_diagnostics, so the
classes of third-party resources stay as they are unless they say (strict) themselves. When one
class is declared in several places, one (strict) makes it strict, and otherwise one (loose)
keeps it loose.
A field belongs to a table when the code that owns the table sets it: through a global or a path
from one, like Config.debug = true, on the table a ---@class annotation declares, through
self in a method, through a local declared with a table, at the top of the file or inside a
function, and through the
instance a constructor makes in a local, as local self = setmetatable({}, Base) does for a table
without a ---@class. As in TypeScript, what is set through other values adds no field: a
parameter or local typed as a class, a loop variable over another file's tables, or another local
that holds the same table. Hover and completion leave such fields out, and lua-language-server
leaves them out of a class too.
inject-field reports the fields that assignments and function value:name() statements set
through a value whose type does not have them: a class, or a table type that an annotation
declares, such as ---@type { label: string }, as lua-language-server and TypeScript do. As in
lua-language-server, a table whose type no annotation declares takes any field: one a table
constructor builds, what a function without @return returns, what a generic gives for one, as
assert(Entities[id]), a loop variable over such tables, and a global or local table read through
another name, as local cfg = Config. With
strict, as in TypeScript, a table that a constructor with fields builds has those
alone, at the top of the file as inside a function, so what code sets through its local later is
reported, while an empty table such as local result = {} still takes any field. To turn it off:
[rules]
"inject-field" = "off"---@class Thing
---@field name string
local Thing = {}
function Thing:init()
self.ready = true -- declares `ready`
end
---@param thing Thing
local function tag(thing)
thing.ready = false
thing.extra = 5 -- inject-field: Field `extra` is not declared in `Thing`
end
local rows = { { label = 'a' }, { label = 'b' } }
for _, row in pairs(rows) do
row.count = 0 -- with `strict`: Field `count` is not declared in `{ label: string }`
endAn empty table, such as local result = {}, takes any field, and so do values of unknown type,
table and any. Keys held in variables are not checked, and strict classes are left to
undeclared-field. lua-language-server's ---@diagnostic disable: inject-field comments and
diagnostics.disable entries apply here too.
undefined-field reports a field read from a value whose type does not have it, as
lua-language-server and TypeScript report a property its type does not declare. Outside the
language server it covers the standard library tables, such as string.nope. In qbx-lua-ls it also
covers values whose type the language server knows, with the fields that inject-field lets code
set: those a class or table type declares and those set through the names that own a table, and,
as in lua-language-server, a field that the code sets through a local is one that local has, also
where inject-field reports setting it. With strict, as in TypeScript, a table
that a constructor with fields builds has those alone, such as the { label: string } of
{ label = 'a' }, and a field set through a local is still missing from its type. Reads with
., ['name'] and : count, also in conditions such as if point.z then. A union lacks a field
when none of its parts has it, and a local that is assigned again has the types of all the values
that may reach the read. A value that is surely nil has no fields at all: what a call of a
function that returns nothing gives, as a function whose body has no return with a value does
when no other definition of it with the same parameters returns a value, and a local declared with
such a call, nil or no value that is never assigned again. A read that a guard such as
if zone then keeps from running is not checked for a local that only ever holds false, as
local zone = false does when nothing assigns it another value.
---@class Point
---@field x number
---@field y number
---@param point Point
local function show(point)
print(point.x, point.z) -- undefined-field: Field `z` is not declared in `Point`
end
local rows = { { label = 'a' }, { label = 'b' } }
for _, row in pairs(rows) do
print(row.count) -- with `strict`: Field `count` is not declared in `{ label: string }`
endSome values may have any field, so reads from them are not checked:
- values of unknown type,
tableandany, empty tables such aslocal result = {}, and classes with an index that takes the name or a parent that is no class, such astable; - without
strict, tables whose type no annotation declares, as forinject-field, and locals without an annotation that may hold a value of unknown type, asPlayerdoes afterlocal Player = ''andPlayer = GetPlayer(); - global tables and the paths from them, such as
Config.debugorESX.PlayerData, whose fields other files and resources set, and the instances made from them, unless an annotation types them; the exports of a resource are checked only against a type declared for them, as---@type PhoneExportsaboveexports['phone'] = {}declares one, with what the resource registers; selfin a method of a table without a---@class, whose instances get their fields elsewhere, and tables whose metatable has an__indexfunction;- numbers, booleans and functions whose type is only inferred, not declared;
- strict classes, whose fields
undeclared-fieldchecks.
Like qbx-lua-ls without strict, lua-language-server's undefined-field does not check tables
built from table constructors. It counts a field set through any value of a class, which
qbx-lua-ls counts only for the local it is set through, and which inject-field reports. Its
---@diagnostic disable: undefined-field comments apply here too, also to the standard library
tables, but its .luarc.json settings do not, since projects often turn off its broad check, which
must not turn off the standard library one. To turn it off:
[rules]
"undefined-field" = "off"A class can keep fields and methods to itself, as in LuaLS:
---@class Account
---@field private balance number
---@field owner string
local Account = {}
---@private
function Account:audit() end
function Account:deposit(amount)
self.balance = self.balance + amount
end
---@type Account
local account = Account
print(account.balance) -- invisible
account:audit() -- invisible---@field private, protected and package mark a field. ---@private, ---@protected and
---@package above function Account:name(), Account.name = value or self.name = value mark
what the statement sets, and ---@public changes nothing. qbx-lua-ls reports invisible where
code reads, sets or calls such a member, or defines it with function value:name(), outside where
it may:
- A private member is used through the table the
---@classannotation declares, likeAccount.balanceor ox_lib'slib.array:new(), from anywhere, or inside a function defined on that table:function Account:name(),function Account.name(),Account.name = function() endor a function written in the table's own constructor, with the functions nested in it. - A protected member is also used through the table of a subclass, or inside its functions.
- A package member is used in the file that declares it.
The nearest class that declares a name decides, and one restrictive declaration there makes the
member restrictive, so a subclass that declares the name again without a keyword makes it public.
Setting a member on the table of a class declares it for that class, so a subclass may define a
member its parent keeps private, as ox_lib's function lib.ped:__index() does.
Keys of table constructors are not checked, and missing-fields asks for private and protected
fields wherever a table of the class is built, as LuaLS does. Completion leaves such members out
where the code may not use them.
Unlike LuaLS, functions nested in a method, such as a table.sort comparator or a CreateThread
body, and functions written in the class table's constructor count as inside the class. LuaLS's
doc.privateName, doc.protectedName and doc.packageName settings, which make fields private by
their names, are not read.
A function documented with @return has to return values of those types. That includes a
function passed to the call below the doc comment, such as the handler of
RegisterServerCallback('name', function(source) ... end), also when the statement assigns what
the call returns, as local handler = RegisterNetEvent('name', function() ... end) does, and a
function written in a table, whose doc comment goes above its field. qbx-lua-ls reports:
return-type-mismatchfor a returned value that clearly is not of its@returntype: a different kind of value, such asreturn 5for---@return string, or a literal the type does not list. Each type a union lists has to fit,nilincluded, soreturn maybe()is nonumberfor amaybethat returns anumber?; thenilthat the type of a field read allows does not count, as lua-language-server reads fields, unlessstrictasks for it. A trailing---@return ...string, or---@return string ..., covers every further value.missing-returnfor areturnwith fewer values than the function requires, and at theendof a function whose body can run past it without returning. A value is required unless its type allowsnil, asstring?,string|niloranydo.redundant-return-valuefor the values areturnpasses beyond those@returndeclares. A trailing...Ttakes any number of values, and an@overloadthat declares more values allows them. Only the values written out count: a call or...at the end may give none, soreturn 1, print()passes for---@return integer, andreturn text:gsub(...)for---@return stringalthoughgsubalso returns a count.
A function without @return gets the same checks from the function type it is written as, as in
lua-language-server: the fun(n: integer): string of the parameter it is passed for, of the
---@type above the statement it is the value of, or of the field of a typed table it is written
in. A fun() that lists no values takes none, so return true in a handler passed for
fun(...: any) is a redundant-return-value. A value whose type names a generic of the callee
or of the function type is not checked, since only the arguments of a call bind it, and nothing
is checked when the call fits several signatures, the type lists several function types, or it is
the @return of the function that returns it and names a generic of that function, as the
fun(): T, number of ox_lib's math.lerp does.
---@param cb fun(n: integer): string
local function withCallback(cb) end
withCallback(function(n) return n end) -- Cannot return `integer` as return value #1 of type `string`A body finishes without running past its end when it ends in return, error(...), an
if/else whose branches all finish, or a loop such as while true do that only a return
leaves. An empty body runs past its end too, except in a definition file marked ---@meta, whose
functions only declare their signatures. Returned tables typed as a class get the same checks as
---@type tables: missing-fields, assign-type-mismatch and undeclared-field.
A function that returns either one set of values or another lists them on its @return line,
separated by |, with the values of a set in parentheses:
---@return false | (string, string)
local function GetName() ... endEach return then has to be one of the sets: return false and return "Joe", "Doe" are, while
return "Joe" is a missing-return, return 1, "Doe" a return-type-mismatch and
return "Joe", "Doe", "Smith" a redundant-return-value. A return is compared with the set it
comes closest to. This notation is a qbx extension of LuaCATS, which qbx-lua-ls also uses to narrow
the locals a call declares; see its type guards.
Only clear cases count. Values whose type is not known are skipped, including a local that one of
the values that may reach the return leaves without a known type; see
locals that are assigned again. The
same applies to the values assign-type-mismatch checks. abc.field = nil clears any field, as in
lua-language-server; with strict, as in TypeScript, only one whose type allows
nil, such as string? or string|nil. An entry that only an index of the class takes, as
names[id] for ---@field [number] string, is removed with = nil either way, as one of an array
or a map is. With strict, such an entry also takes a value that may be nil, as
states[id] = isInside or nil sets or removes it.
discard-returns reports a call on a line of its own whose function is marked @nodiscard, as
its values are what it is called for. The runtime stubs mark the functions that only compute a
value, such as tostring, math.floor, json.encode and vector3:
---@nodiscard
---@return integer
local function count() ... end
count() -- The values that `count` returns cannot be discardedA call that may run one of several functions, such as a client and a server definition, is only
reported when each of them is marked. Of a function with @overloads, the signature the call picks
decides, as for LuaLS: an overload is not marked, so math.random(), which warms up the generator,
passes, while math.random(1, 10) is reported. string.gsub is not marked either, since with a
function as repl it loops over the matches. Calls through globals in an opaque resource, such as
an escrowed one, are not checked; see
Escrowed, obfuscated and mixed-language resources.
assign-type-mismatch also covers variables whose type is declared. A local or an assignment
with a ---@type above it has to store values of that type. A later assignment to a local
declared with ---@type, or to a parameter documented with @param, has to store them too, and
as lua-language-server reports it as cast-local-type, so does qbx-lua-ls, and
the suppressions written for lua-language-server silence it:
---@alias State 1 | 2 | 3 | false | true
---@type State
local state = "test" -- assign-type-mismatch: Cannot assign `"test"` to `state` of type `State`
state = "abc" -- cast-local-type: Cannot assign `"abc"` to `state`, defined as `State`
state = nil -- cast-local-type: Cannot assign `nil` to `state`, defined as `State`The check is that of return-type-mismatch: a different kind of value, or a literal the type does
not list. A local is compared with the type it is declared with, not with what the guards around
the assignment narrow it to, so name = "x" inside if not name then passes for a string?. A
local declared without a value is not reported, and neither is the nil that the statement under
a ---@type gives the names it declares, as lua-language-server allows ---@type number above
Hp = nil. A later = nil needs a type that allows it. A call at the end of the values gives each
name the value it returns at that position.
A ---@type line that lists several types gives each name of the statement its own, as
---@type boolean, string? does above local ok, err = pcall(...). A single type is the first
name's alone, as lua-language-server binds it, so ---@type boolean above
local onScreen, x, y = GetScreenCoordFromWorldCoord(...) types only onScreen. Names without a
type of their own, and other locals whose type is inferred from what they hold, have the type of
the value they are declared with, which cast-local-type checks. Globals in
assignments that have no ---@type above them take any value.
The value stored is what the code is known to give. A global's nil tells nothing: one declared as
CurrentZone = nil and set to a name by an event handler holds whatever the handler gives it, so
storing it anywhere passes. A --[[@as T]] or ---@as T right after a value, on its line, casts
it to T as in lua-language-server, which silences a check that the code knows better than:
local count = GetValue() --[[@as integer]] stores an integer. On a call, the cast types its
first value.
A table constructor typed as a table type rather than a class is checked like a class table, by
the type of each entry: a shape such as { value: string, count?: integer }, an alias of one,
generic ones such as Box<string> for ---@alias Box<T> { value: T } included, an array such as
string[], or a table<K, V>. That holds wherever a class table is checked: under a ---@type,
as an argument, as a returned value, and in the fields of another typed table, also for the class
tables a shape holds. An assignment without a ---@type checks a table against the type its
target is declared with, not one inferred from what the target held before. As in
lua-language-server, a global takes any table there, and the values stored through it, as
Config.Debug = 1, are not checked, unless strict is set. As in
lua-language-server, such a table may set fields its type does not name, and the entries of a table
typed as a union are not checked.
missing-fields reports a table typed as a shape that leaves out a field the shape requires, one
without ? whose type does not allow nil, only with strict, as TypeScript does
and lua-language-server does not. The same goes for a class given type arguments, as
---@type List<string>, while ---@type List requires the fields of List in both modes. The
tables that an array, a table<K, V>, a tuple or a shape holds need the fields of the type of
their entry, so the {} of { {} } for Dog[] and of { rex = {} } for table<string, Dog> are
reported, as in both. An entry whose key the type does not take, like the {} of { {} } for
table<string, Dog>, is not checked.
A table typed as a union of classes and shapes needs the required fields of one of them, and the
report has a line for each, as lua-language-server's does. Without strict, a shape requires no
field, so a union that lists one is not reported, as in lua-language-server. Types that hold no
table are left out, so a table for Dog|string needs the fields of Dog, and as in TypeScript, a
union that also lists an array, a table<K, V>, a tuple or a type that takes any table, such as
table or any, is not reported. A table that a table typed as a union holds, like the {} of
{ pet = {} }, needs the fields of one of the types that its members declare for its key, as
Dog|Cat for an entry of Dog[]|Cat[]. As in TypeScript and unlike in lua-language-server, the
tables held by one that may be a table or any, as for Dog[]|table, are not checked.
Unlike in lua-language-server, nothing is reported in a ---@meta file. It never runs, so a
---@type Config above Config = {} there only declares the type of Config.
cast-local-type reports an assignment that gives a local a value its type does not take. A local
with a ---@type or @param has that type, as described for
typed variables. As in lua-language-server and TypeScript, any other local has
the type of the value it is declared with:
---@return string?
local function find() end
local speed = 5
speed = "5" -- Cannot assign `string` to `speed`, defined as `integer`
speed = 7.5 -- passes: an `integer` local takes any number
speed = nil -- Cannot assign `nil` to `speed`, defined as `integer`
local mode = "dev"
mode = "live" -- passes: a literal written out stands for its kind
local name = find()
name = 5 -- Cannot assign `integer` to `name`, defined as `string?`The check is that of assign-type-mismatch, for each type the value may be: a different kind of
value, or a literal the type does not list. So a string? needs a local that may be nil, and so
does clearing a local with nil, but as in lua-language-server, a local declared with a table
constructor and no ---@type may be cleared. A value read from a field or a key has no nil or false for the
check, as lua-language-server does not narrow fields, and only a nil that an annotation or a stub
declares counts: lua-language-server leaves out the one a function without @return gives, as
nextFreePoint() does by running past its end when it finds none, also where a local passes it on.
With strict, as in TypeScript, all of those count. A local that starts as false
or true is a boolean, and one declared with a table constructor takes any table. A literal that
a function declares it returns, as the "a"|"b" of ---@return "a"|"b", is one of the only values
its local takes, while a literal written out, also through and and or, widens to its kind. A
local copied from a value that a type guard narrows
takes the literals that the value's type lists of the kinds the guard leaves, as lua-language-server
narrows a value by its kind alone: local mode = state after if state ~= "idle" then return end
takes "busy" too for an "idle"|"busy", and local enabled = flag after if not flag takes
false. With strict, as in TypeScript, it has the type the guard leaves. Each
name of an assignment is checked against its own local, also inside the functions that a local is
assigned in.
A local declared without a value, as nil, or with a value of unknown type or any, takes any
value, and so do _, self and parameters without @param, as in lua-language-server. A loop
variable has the type its loop gives it, such as the integer of for i, v in ipairs(list). With
strict, a parameter of a function passed for a function type, such as the handler of
fun(id: integer), has the type the function type declares, as in TypeScript. <const> and
<close> locals are left to const-reassign. A ---@cast changes what a local holds from its line on, but
not the type it is declared with, as in lua-language-server, so assigning a value of the cast type
is still reported; a ---@type above an assignment is the type of the value it stores. A BOOL
that a native returns may be a boolean or an integer, so either passes for the other where a native
gives it.
A ---@cast line changes the type of a local from its line on: ---@cast name T makes it a T,
+T and -T add and take out a type, and +? and -? add and take out nil. qbx-lua-ls
describes where a cast holds with its
type guards.
cast-type-mismatch reports a ---@cast name T whose T the declared type of the local does not
take. The check is that of assign-type-mismatch, for each type T lists: a different kind of
value, or a literal the declared type does not list. A cast to a type that takes every value of the
declared one widens it and passes, as TypeScript takes x as A | B for a B, although
lua-language-server reports it: integer? for an integer, a parent class, or a union that lists
the declared type, as ---@cast coords Cell | Coords for a Coords.
---@type integer
local count = 1
---@cast count string -- Cannot convert `integer` to `string`
---@cast count integer? -- passes
---@cast count number -- passesThe declared type is the ---@type or @param of the local, or else that of the value it is
declared with, as for impossible-comparison: a literal, or what a function declares it returns.
A local that is assigned again and has no annotation has the declared types of the values that
reach the cast, so local count = 5, count = 6 and then ---@cast count string is reported. A
local whose type is only inferred is not checked, and neither is one declared as nil, which the
cast gives its type as in lua-language-server, nor the +T and -T entries, which change the type
rather than replace it.
As in lua-language-server, a class has to be one that the declared type names or extends: a
Test.Animal can be cast to its subclass Test.Dog, and a Test.Dog back to Test.Animal, which
widens it, but neither to an unrelated class. Type arguments are not compared, a declared type that is no class,
like table or any, takes any class, and so does a local declared with a table constructor.
impossible-comparison reports an == or ~= whose two sides can never be equal, so that the
comparison always gives the same answer. Either the sides are different kinds of value, such as a
string and a number, or they list literals and none is on both sides:
---@return "active" | "busy" | "ready"
local function GetState() ... end
if GetState() == "invalid_state" then end -- Comparing `"active"|"busy"|"ready"` with `"invalid_state"` is always false
if GetState() == 5 then end -- Comparing `"active"|"busy"|"ready"` with `5` is always false
if not name == "admin" then end -- Comparing `boolean` with `"admin"` is always false
if type(value) == "tabel" then end -- Comparing `lua_type` with `"tabel"` is always falseA side has a type when something declares it: a literal, an operator such as not or .., a
@param, @type or @return, a @field of a class, an alias or enum, a stub or a native. A
parameter of a function passed to a call has the type the callee declares for it, with the generics
that the other arguments declare: value is a Player in
onValue('Player', function(value) end) for ---@param cb fun(value: T) and a `T`. A
local has the types of the values it is declared with or assigned that may reach the comparison,
so local state = GetState() is checked like the call, and inside a
type guard the type the guard narrows it to: the right
side of class ~= 13 or class ~= 14 only runs when class is 13, so it is always true. Guards
narrow the fields of locals the same way. Without
strict, a guard does not hold past a call that may run code of a script, which
may change a local that other functions assign, or a field: hits == 1 passes after hit() inside
if hits == 0 then for local function hit() hits = hits + 1 end, and so does
Machine.state == "busy" after TriggerEvent("machine:start"). Natives and the functions of the
Lua libraries and CfxLua that call none they are given, as print, run no such code. A
literal stored in a local counts by its kind only: local mode = 'dev' is a setting to change, so
mode == 'prod' passes, while mode == false is still reported as a string compared with
false. A value assigned to a local with a ---@type or @param keeps the parts of that type it
may be, as described for
locals that are assigned again.
Only clear cases count, and the rest is left alone:
- Types that are inferred from assigned values.
Config.Webhook = ''tells what the config holds today, not thatConfig.Webhook == falsecannot be true once someone edits it. The same goes for what an undocumented function returns, and for the value ofa or banda and b. - Comparisons with
nil. Annotations often leave out the?of a value that may be missing, and the check for it is deliberate. - A local that one of the values that may reach the comparison leaves without a declared type, as
name = untyped(name)does, unless a---@cast name Ttypes it.---@cast name +Tdoes not: it addsTto a type that is still not known, as it does for a local with no declared type. - Natives documented as
booleancompared with a number. Scripts can get1instead oftruefrom them, soIsPedInAnyVehicle(ped) == 1is valid.
no-unknown is off by default. Turned on, qbx-lua-ls reports each parameter, local and loop
variable whose type is unknown: none is declared, and none can be inferred from its value, from
the function a callback is passed to, from the declared type of the table field a function is
written in or defined for, or from what a loop goes through.
[rules]
"no-unknown" = "warning"function test(foo) end -- Parameter `foo` has no type; add `---@param foo <type>`
---@param data table
local function list(data)
for key, value in pairs(data) do end -- a plain `table` tells nothing about its keys and values
endA ---@param or ---@type gives the name a type, any and unknown included, as TypeScript takes
an unknown that is written out, and so does a loop over a table whose type declares its keys and
values, as { [unknown]: unknown } does. Without one, a parameter that
only takes any from the ... of the fun(...) its function is passed as, like the handler of
RegisterNetEvent, counts as untyped, and so does the data of a RegisterNUICallback handler,
which only the resource's own UI decides. Its ---@param goes above the statement that makes the
call, whether that is the call alone or handlers[name] = RegisterNetEvent(name, function(id) end).
A function written in a table takes the ---@param lines above its field, and the parameter
types of the field's fun(...) when the table has a declared type: the ---@type above its
local, the type of the parameter it is passed for, or metatable for the second argument of
setmetatable. A function defined for a ---@field name fun(...) of a class, with
function Class:name(), function Class.name() or Class.name = function(), takes the parameter
types of that fun(...) when it has as many parameters, not counting the self of :. A function
that a statement gives through or or and, as Prepare = Prepare or function(look) end, takes
what the statement declares as one written alone would, as TypeScript types the sides of ||;
a table given that way, as Config = Config or {}, is not checked against it. A function
that a return passes takes the ---@param lines above the return, and the parameter types of
the fun(...) that the @return of the function around it declares at that position, or the
function type declared for that function, as ---@type fun(): fun(ped: number). A local
declared without a value needs a ---@type, since
later assignments are not followed. Names that start with ignore_unused_prefix, and self, are
not reported. To check only your own resources, set the level in an [[overrides]] entry instead
of [rules].
It also reports a value of unknown type where a type is declared for what takes it: a local or
parameter with a ---@type or @param, a field of a class or of a table built for a declared
table type, an element of such a table, a parameter that each signature the call may use declares,
and a value that @return declares. The value of a statement that has its own ---@type is not
reported, as that annotation is how code gives a value of unknown type its type, as
---@type EsxSkinConfig above local config = ESX.GetConfig() does:
---@param entities number[]
local function target(entities)
if type(entities) ~= "table" then
entities = { entities } -- The type of the value assigned to field `[1]` of type `number` is unknown
end
endA target declared any takes any value, and the parameters of natives and those typed with a
generic of the function called are left out. A
value read from a local that is itself reported, as data.id for an untyped parameter data, is
not reported again: typing the local types the value.
need-check-nil reports a local that may hold nil where code indexes it, calls it or stores a
value under it as a key, all of which raise an error for a missing value, as lua-language-server
does. With strict, as in TypeScript, so does using it in arithmetic,
concatenation, # or a <, <=, > or >= comparison, or giving it as a bound of a numeric
for, which raise the error too. == and ~= compare any values and are not reported. To turn it
off:
[rules]
"need-check-nil" = "off"---@return string?
local function GetName() end
local name = GetName()
print(name:upper()) -- `name` may be nil: its type here is `string?`
print(name:lower()) -- `name` may be nil: its type here is `string?`As TypeScript and lua-language-server do for a value that may be missing, every such read is reported, also after one that would raise the error first, until a guard or cast rules the missing value out.
A local may hold nil when its declared type allows it: its ---@type or @param, the
@return or @field of the function or class its value comes from, the value type of a map or an
indexed table it is read from, as table<string, Row?> or { [number]: Row? }, or for a parameter
of a function passed to a call, what the callee declares for it, whoever declares them, stubs and
other resources included. false, as in false|string, counts as well. A function without
@return declares the nil or false that one of its returns writes out or leaves out, as
return, return nil or return print(...) do, as lua-language-server infers it; running past
the end of its body does not count, unless strict asks for what TypeScript
infers, nor does the nil that the type of another value it returns may hold. Nor do they count
for a function called through exports, as exports['qb-core']:GetPlayer(source), unless with
strict: lua-language-server does not see what resources export. In a loop from 1 to
the # of a table, or back, as for i = 1, #rows do, rows[i] is an item the table holds, which
is not nil, except with strict, as TypeScript reads the declared type of an item. The
type guards and casts around the read narrow the
type first, so if not name then return end, if name then, name and name:upper(),
assert(name) and ---@cast name -? all check it, as does a condition that reads from it, such as
if player?.job then for player. ?., ?[ and ?: give nil for a missing value and are not
reported. Where guards together rule out every value of the type, the read has the type that the
type guards give such code. Inside
if not count then, a number is nil, which holds no value that may be missing and is not
reported, as in lua-language-server. After round and (round == true or i < round), a boolean?
keeps its type, but the nil that round and rules out stays out.
A local that may hold nil, passed for a parameter that does not take nil, is a
param-type-mismatch, as in lua-language-server:
---@param target Player
local function greet(target) end
local player = GetPlayer() -- Player?
greet(player) -- Cannot assign `Player?` to parameter `target` of type `Player`need-check-nil still reports one passed for a parameter typed with the name of a native handle,
such as Vehicle, which param-type-mismatch leaves out, in every signature the call may use. A
---@diagnostic disable comment for param-type-mismatch silences it too.
Only a guard on the local itself checks it. After
local vehicle, coords = lib.getClosestVehicle(pos) and if not vehicle then return end, coords
is still reported where it is declared vector3?, unless the function declares the
sets of values it returns, as
@return nil | (integer, vector3) does. A lookup such as ESX.GetPlayerFromId(source) that is
declared to return xPlayer? counts like any other call.
Only clear cases count, and the rest is left alone:
- Without
strict, fields, which lua-language-server leaves alone too, and the values of calls. A local that takes the value of a field has the type the guards around it leave of the field, solocal job = data.jobinsideif data.job thenholds a value; see fields. Withstrict, they are checked as locals are, through the guards and assignments before them, also those read through a global such asConfig.Logs.Service. - A local that one of the values that may reach the read leaves without a declared type, as
name = name or 'none'does, unless a---@cast name Ttypes it. A local that is assigned again is checked with the values that reach the read, as described for locals that are assigned again, andlocal namedeclares no missing value: only the values given later count. - Without
strict, as in lua-language-server, thenilthat code running at other times gives a local without---@typeor@param:coords = nilin one function leavescoords.xin another, or after a call that may yield such asWait(0), unchecked, while anilassigned in the same function counts. So is thenilthat the?of an optional parameter of a callback adds, as for thebodyofPerformHttpRequest. A guard before a call that may yield holds after it too, also for a local with a---@typeor@param, sozone.nameafterif not zone then return endandWait(500)passes.
Guards only narrow the code after them, so a read inside a function defined before the guard is
reported, as return name:upper() is in a local function above if not name then return end:
the function may run before the guard does. lua-language-server reports it as well.
missing-global-doc, missing-local-export-doc and incomplete-signature-doc are off by default
and keep lua-language-server's names, so a .luarc.json that turns them on with
diagnostics.neededFileStatus turns them on here too. Turned on, each reports the parameters of a
function that no @param names, and each value of each return at a position that no @return
covers:
[rules]
"missing-global-doc" = "warning"
"incomplete-signature-doc" = "warning"function GetName(id) return tostring(id) end -- parameter 'id' of global function 'GetName' has no @param annotation
---@param id integer
local function getName(id) return tostring(id) end -- incomplete signature: return value #1 has no @return annotationmissing-global-docchecks the global functions defined withfunction Name()orName = function(), also inside other functions, but not fields of global tables or methods. One without parameters or returned values needs a comment instead: any comment directly above it, or after code on the line its parameters start on, other than---@diagnosticlines.missing-local-export-docchecks the local functions a file exports, and asks for a comment the same way. A module exports those it assigns by name to a field of a local table that areturngives, asM.name = namebeforereturn M; functions defined withfunction M.name()are not checked, as in lua-language-server.exports('Name', fn)exportsfn, and a function written in the call. A global function passed toexportsis left tomissing-global-doc.incomplete-signature-docchecks every function whose doc comment has@paramor@returnlines, handlers passed to calls and functions in table fields included. A global function can be reported by it andmissing-global-docalike.
A function's doc comment is the one above the line it starts on, or above a call that spans lines
and passes it; comment lines in between do not separate them, a blank line does. Only the first
function that starts on the line takes it. @param ... or @vararg documents ..., and self
and parameters that start with ignore_unused_prefix need no @param. A call or ... at the end
of a return counts as one value, and a last @return of ...T, or named ..., covers every
later value. A ---@type fun(...) in the doc comment documents the parameters and values it lists.
A parameter that the function type its function is passed as names needs no @param either, such
as source and phoneNumber of a handler passed to a wrapper documented with
---@param callback fun(source: number, phoneNumber: string, ...); the parameters that fall into
its ... still do. qbx-lint knows the function types of the global functions its resource
documents and of the local functions and local tables of the file. qbx-lua-ls knows every declared
type: those of stubs and other resources, class fields, and the ---@type of a variable or field.
Unlike lua-language-server, these rules count ---@type fun(...), the function type a function is
passed as, @vararg and a ...T @return as documentation, takes a comment above
Name = function() as that function's comment, asks for a comment on a function whose only
return gives no value, and marks the name of a function that needs a comment rather than the
whole function.
The runtime stubs declare sql as a string that holds an SQL query. Give it to the first
parameter of a function that runs queries:
---@param query sql
---@param params? table
function DB.fetch(query, params)
return MySQL.query.await(query, params)
endThe type checks read it as string. Editors use it to highlight the query in calls: the
qbx/sqlFunctions request lists the global, member and exported functions whose first parameter
takes sql, also as sql?, as calls write them, such as DB.fetch, DB:query for a function
defined with :, or exports.mydb:scalar for an export. Local functions are not listed.
lua-language-server does not know the type; a definition file with ---@alias sql string
declares it there.
The CLI first collects event registrations and exports from the resources being analyzed, then checks their callers. When an input belongs to a resource, the other Lua files in that resource contribute to analysis even if only one file was requested. Including related resources in the same run provides more context for calls between resources.
Static event names can be checked for the target side and missing or excess arguments. Export checks report excess arguments and unknown names. Unavailable resources and computed names limit these checks. A diagnostic about missing event arguments may describe an intentional optional parameter.
The events that FiveM itself triggers, such as onResourceStop, playerDropped and
onClientResourceStart, are known without any handler in the workspace. Completion in
AddEventHandler(' and RegisterNetEvent(' lists those of the code's side, hovering the name shows
the handler FiveM calls and what the event is for, and the parameters of the handler take the types
FiveM passes, as resourceName: string. A handler for an event that only the other side triggers,
such as playerDropped in client code, reports fivem/event-wrong-side.
qbx-lua-ls also takes the type of a resource's exports from LuaLS definitions:
---@type PhoneExports above exports.phone = {} or exports['phone'] = {}, or
---@class qbx_core above exports.qbx_core = {} with methods such as
function exports.qbx_core:GetCid(source) end. Such a declaration counts in a
definition file or any file with ---@meta, and elsewhere
when the annotation is there; a plain exports.Name = fn registers an export of its own resource,
and a table a test assigns to exports.phone declares nothing. exports.phone and
exports['phone'] then offer the members of the declared type first and the exports the resource
registers after them, the declared one winning when both name a member, also for resources the
workspace lacks. Completion of exports[' and of resource names, as in GetResourceState(',
lists those resources too. fivem/unknown-export only knows the exports resources register.
Locale checks compare static locale('key') calls with locales/en.json, falling back to the first
JSON file in locales/ when that file is absent. Unused keys are reported on the JSON file when
the run includes the resource manifest. Dynamic locale usage limits which keys can be considered
unused.
Encrypted FiveM files beginning with FXAP, Lua bytecode, and detected binary blobs are skipped.
Obfuscated or minified files are skipped too: a file counts as obfuscated when one of its lines is
at least 4096 bytes long and contains the function keyword. Long data lines, such as tables or
encoded strings, do not count. To skip other generated files, use exclude.
A .fxap marker or a skipped or unreadable script makes the resource opaque to checks that need
complete knowledge of its globals or locale usage, such as undefined-global, implicit-global and
unused-locale-key. Readable scripts are still analyzed. The same goes for the globals of a file
that only opaque resources load at runtime through their imports.
Unknown-export checks are suppressed for opaque resources and resources with non-Lua scripts.
An opaque resource may also handle events named with its resource: prefix, so a wrong-side
diagnostic is suppressed when the missing handler could be in that resource. Computed export
registrations similarly prevent a complete list of exports. missing-parameter,
redundant-parameter, param-type-mismatch and discard-returns do not check calls to globals in
an opaque resource, or to fields of global tables, such as Utils.round(), since an encrypted
script may define them differently. param-type-mismatch and discard-returns still check the
runtime's own functions, such as math.floor, and calls through exports.
Read-only linting uses replacement characters for invalid UTF-8 bytes. --fix, fmt, and
fmt --check report an encoding error for such source, and do not rewrite it. Convert its encoding
explicitly before using editing commands.
For a resource below the resources directory next to a server.cfg, the linter reads ensure
and start commands, included exec files, and ensure [category] groups. A resource that starts
earlier satisfies a dependency check. Resources started by the same category command have no
relative order in this model.
The resources directory also supplies installed resource names, including manifest provide
aliases. fivem/resource-not-found reports an export call to a missing resource only when that
call is outside an if. Its default level is info, because the enclosing function may never run.
Dependency checks make allowances for integrations selected at runtime:
if Config.Inventory == 'ox' then
exports.ox_inventory:AddItem(source, 'water', 1)
elseif Config.Inventory == 'qs' then
exports['qs-inventory']:AddItem(source, 'water', 1)
endRecognized string comparisons and early-return guards suppress missing-dependency and
resource-not-found findings for the selected code. The same allowances apply inside pcall or
xpcall, in files below bridge, bridges, framework, frameworks, compat, or integrations,
and in resource files not loaded as manifest scripts. Dependency checks also account for
GetResourceState usage. These are heuristics, not proof that a dependency is available at runtime.
qbx-lint fmt resources
qbx-lint fmt --check resources
qbx-lint fmt --config config/qbxlint.toml resources
qbx-lint --fix resourcesThe formatter adjusts spacing, indentation, and line wrapping while preserving constructs such
as one-line guards, expanded tables, trailing commas, inline casts, and blank-line grouping.
quote_style accepts preserve, single, and double; quote changes are limited to strings that
do not need new escapes.
Before writing, the formatter compares the input and output tokens and comments. String contents are compared after decoding escapes. A verification failure leaves that file unchanged. Statements with comments the printer cannot place are retained verbatim. Syntax errors also prevent formatting.
--fix applies available, non-overlapping fixes and then lints again, including those of rules
whose findings --min-severity leaves out, such as the removal of the whitespace trailing-space
reports at the end of lines. The candidate output must parse before it is written. A source file
that changed after analysis is not overwritten. Hash literal replacements are offered in value
positions; a standalone GetHashKey('adder') call cannot become a bare hash statement.
Editing commands can process several files before encountering an error; they are not a transaction over the entire input. Review the resulting diff.