Repository navigation
feat: LuaCATS type checks, type guards and callback wrappers - #12
Merged
Merged
Conversation
Report calls that leave out an argument the function's LuaCATS annotations require, including functions defined in other files of the resource. The LuaCATS parser moves out of qbx-lua-ls into a new qbx_luacats crate so the linter and the language server share it. The server passes each file's source to summarize, so its missing-parameter follows annotations in other files as well.
A call like OnAction("keyPressed", function(key) end) fit both the declared
fun(action: string, ...) and the overload fun(action: "keyPressed", ...) by
kind alone, so the declared signature won and the callback parameters stayed
untyped. Literal arguments now count towards a signature's fit, a literal a
parameter does not list rules that signature out, and the best-fitting
signature wins instead of the first overload that beats the declared one.
A literal argument that the declared parameter lists among other values, as
an alias like `"jobUpdated"|"keyPressed"|string` does, would tie with the
overload that takes only that literal. That overload wins, so
`OnAction("jobUpdated", function(job) end)` types `job` from it.
Hovering the function name of a call shows the signature that call picks,
and signature help lists the declared signature with the overloads, the
picked one active.
Completing a function with a fun(...) or function parameter now also offers
a snippet that inserts the call with the callback written out, e.g.
TriggerCallback('event', function(...) end). Parameter names come from the
callback's type, a `...` or optional arguments after it get a stop after
`end`, and the snippet is left out when a `(` already follows the name, in
`function name`, for dot calls of `:` methods, and for names that already
have a hand-written snippet.
Handle textDocument/onTypeFormatting on newline. Enter at the end of a `---@tag ...` line starts the next line with `---@` at the same indentation, and Enter on a line that holds only `---@` removes it and keeps the cursor on that line, so the code typed next stays attached to its annotations. Enter in the middle of a line, plain comments and `---@` inside long strings are left alone, and a bare `---` added by the editor counts as an empty line.
@Class, @field, @alias, @enum and @overload accept a (server) or (client) attribute, alone or beside others such as (exact, server), and the parser records the side. `---@alias (x) Name` and `---@overload (x) fun(...)` were dropped before; a parenthesized overload type like (fun(): string) is still read as a type. applies_on() tells whether a scoped declaration applies to code on a side, with shared code seeing both. missing-parameter ignores overloads scoped to the other side of the call, using the same guard-aware side as the rest of the check. In qbx-lua-ls, overloads scoped with @overload (server) or (client) only compete for calls on that side, as decided by the file's manifest side and any IsDuplicityVersion() or lib.context guard around the call, and signature help lists only those. Classes, fields, aliases and enums scoped the same way follow the manifest side of the file, so a client script no longer sees a (server) class and each side gets its own type for a field declared once per side. Shared files and files of an unknown side see both. Hovers on type names prefer the declaration for the file's side.
`---@callback register|await|trigger [family]` marks a function that wraps a callback system, the way lib.callback.register and lib.callback.await do. Functions tagged with it form a callback system of their own. Their name, handler or response function and payload come from the wrapper's annotations: the first string parameter, the first function parameter and the `...`. Registrations are indexed like lib.callback.register, with the doc comment above the registering statement typing the handler, so AwaitServerCallback('name', ...) completes registered names, navigates and hovers them, shows the handler's payload without `source` in signature help and inlay hints, and returns the handler's @return types. A trigger wrapper passes those types to its response function. The function type built from the doc comment carries the role and family, so local aliases, table members and `:` methods of a wrapper are recognized as well. A function passed to a call also takes its parameter types from the @PARAM lines directly above that call's statement. Open documents that tag a wrapper are reindexed before the ones that call it, and a file the index did not know yet, such as a new one opened in the editor that the manifest does not list, gets its entry before its contents are indexed, so its calls to a wrapper register right away rather than on its first edit. After `---@callback ` completion offers the roles register, await and trigger with a short description of each, and after the role the family names that tagged wrappers in the index already use. Functions tagged await or trigger get a call snippet even when they take no function, e.g. AwaitServerCallback('$1'$2). The first stop is left empty between the quotes of the callback name, wherever the wrapper takes it. When the client lists editor.action.triggerSuggest under capabilities.experimental.commands.commands, the item carries that command, so the registered names are suggested right after insertion. Register wrappers keep their name placeholder, since the name they take is new.
Without @return, a function's return type came from the first `return` that had values, so a callback handler that returns early with a bare `return` before `return FixAppearanceKeys(...)` read as returning Appearance instead of Appearance?. Every `return` now counts: each position is the union of what the returns pass there, and a return with fewer values and running past the end of the body add nil. A body cannot run past its end when it ends in `return`, `error(...)`, an if/else whose branches all do, a `goto`, or a `while true do` or `repeat ... until false` without a `break`, so waiting loops like `while true do if ready then return value end Wait(0) end` keep the type of the value they return. A value that cannot be inferred leaves its position unknown rather than reading as nil, and `integer|number` reads as number. Hovers that list the fields of an optional class now keep its `?`, as in `Appearance? { ... }`.
Find References on the literal name in an event or callback registration or trigger now returns every registration and every trigger call of that name in the same family: native events, ox_lib, QB-Core and ESX callbacks, and each ---@callback family stay separate. Without declarations, only the triggers are listed. Go to Definition on a registration's name now returns that registration alone. VS Code answers a definition at the cursor with its references, so Ctrl+click on RegisterNetEvent('name', ...) lists the calls that trigger it. Definitions from a trigger still lead to the registrations.
Annotations written for LuaLS name the values type() returns `type`, as in ox_lib's `---@PARAM expected type`. Declare it as an alias of lua_type.
`---@enum WheelType` above `return { Sport = 0, ... }`, as qbx_customs declares its enums, was never indexed because enums were only read from tables assigned to globals and top-level locals.
qbx-lua-ls warns on each type name in a LuaCATS annotation that no indexed @Class, @alias or @enum declares, with "Undefined type or alias `Name`". Built-in types, native handle types such as Vehicle, self, generic parameters of the block, and those of generic classes declared in the file count as known; the names a line declares and @see targets are not checked. Type names are looked up for the file's side, so a client script that names a (server) class, alias or enum is told "Type `QBJob` only exists on the server, but this is a client script". Shared files and files of an unknown side see both sides and are not reported. Only the server indexes the declarations, so the linter registers the rule for levels, config and ---@diagnostic comments and leaves the check to it, like LuaLS's code of the same name that .luarc settings now map to. The LuaCATS parser gains referenced_type_names, the type names a doc line refers to without the name it declares or @see targets, and declared_generics, the parameters of @Generic and @Class Name<T>.
qbx-lua-ls warns on a table constructor whose type is a LuaCATS class when it leaves out fields the class requires: "Missing required fields in type `Test`: `test`". Tables are checked where their type is known: `---@type` locals and assignments, arguments to class-typed parameters, and the tables such fields hold. Fields of parent classes count, optional ones (`?`, nil, any) and fields scoped to the other side do not, and a class that several resources declare under one name without one being visible is not checked. Only the server knows the classes, so the linter registers the rule for levels, config and ---@diagnostic comments, and maps LuaLS's code of the same name.
Inside a table constructor whose type is a LuaCATS class, completion offers the @fields it does not set yet, with each field's type as the detail and its description as documentation, and inserts `name = `. Tables are typed by `---@type` on a local or assignment, a class-typed parameter, `return` in a function documented with @return, or a class-typed field of another such table. Before, only tables passed as call arguments were completed. The missing-fields module becomes class_tables, which finds the class-typed tables of a file once for both features.
`a and b` is `b` whenever `a` holds a value. When `b` could not be inferred, the union with `false` dropped it and typed the expression as boolean, so `local slot = inv and inv.items?[id]` hovered as boolean and a field check would read it as one.
qbx-lua-ls warns on a value stored in a field of a LuaCATS class that the field does not take, e.g. "Cannot assign `integer` to field `test` of type `string`". Table constructors typed as the class are checked, and so are assignments such as `abc.test = 5` or `self.name = nil`. Only clear cases count: a different kind of value, such as a number, string, boolean, table, function or nil, or a literal the field does not list, like 'other' for 'male'|'female'. nil clears a field only when its type allows it. A table for a class-typed field is checked as a table of that class. A field without its own @field is typed by the value of an index that takes its key, so `other = true` fails `---@field [string] number` and array entries are checked against `[integer]`. Tables for index-typed keys are checked as tables of their class too. Type::is_literal tells a literal-keyed index such as `---@field [1] number` apart, which is left unchecked since only the last such line is kept. A local that is assigned again after its declaration is skipped, since its declared type may not be what it holds; this avoids reports such as `evidenceId = currentCasing` for a casing id that starts out as nil. Type names are now looked up the way the declaring file sees them, since resources declare the same names differently: ox_fuel's @Class State is not qbx_vehicles' @enum State. A name that file cannot tell apart is not checked. missing-fields and field completion use the same lookups. Only the server knows the classes, so the linter registers the rule for levels, config and ---@diagnostic comments, and maps LuaLS's code of the same name.
A class marked `---@Class (strict)` or LuaLS's `(exact)` only takes the fields it declares. qbx-lua-ls reports a name or key such a class does not take, e.g. "Field `other` is not declared in strict class `Test`": set by a table constructor typed as the class, set through a value of it by an assignment or `function value:name()`, or read from one, like `abc[1]` when the class only has a `[string]` index. A field counts as declared by an @field of the class or a parent, or when it is set on the table the ---@Class annotation declares, such as `function Test:greet()`. The indexer now marks members set through values typed as the class instead, like `abc.other = true`, so they do not declare anything. A `[string]` index, or a parent that is no class, takes any name. `strict_classes = true` in qbxlint.toml makes every ---@Class strict unless it is marked `(loose)`, so projects can require declared fields everywhere without annotating each class. It only applies to classes declared in workspace files outside exclude and ignore_diagnostics; third-party resources keep their classes loose unless they say (strict) themselves. The check is skipped while no class is strict. The parser records the attribute as DocClass.strict: Some(true), Some(false), or None when the configured default decides. It combines with a side, as in `(exact, server)`. Only the server knows the classes, so the linter registers the rule for levels, config and ---@diagnostic comments, and the reference describes when a field counts as declared.
A function documented with @return has to return values of those types. return-type-mismatch reports a returned value its @return type does not take, e.g. "Cannot return `integer` as return value Qbox-project#1 of type `string`", with a trailing `...T` covering every further value. missing-return reports a `return` with fewer values than required and, at `end`, a body that can run past its end, e.g. an `if` without `else` or an empty body like `function Add(a, b) end`. A value is required unless its type allows nil. Only files marked `---@meta` keep their empty bodies unchecked. Return types are taken from the doc comment of every function the @PARAM lines apply to, including `M.f = function() end` and the functions passed to the call below the comment, so the handler of `RegisterServerCallback('add', function(a, b) end)` is checked as well. Returned tables typed as a class get missing-fields, assign-type-mismatch and undeclared-field like `---@type` tables. Only qbx-lua-ls infers the values, so the linter registers the rules for levels, config and ---@diagnostic comments, and maps LuaLS's codes of the same names. The reference lists every LuaLS code the .luarc fallback maps, including the type checks it already mapped.
`---@field [1] number` and `---@field [true] string` are now stored one by one in DocClass::literal_fields instead of as the class's index, where each such line replaced the one before and only the last survived, so every `employee[n]` read as the type of the last line. A tuple-like class with `[1] number`, `[2] string` and `[9] boolean` keeps all three; other keys such as `[integer]` still make the index. `employee[1]` is `number` and `employee[2]` is `string`. `employee[i]` with an integer `i` may be any of them, pairs and ipairs yield them with `integer` keys, and the class hover lists them. Table constructors key their array entries by position, so `{ 'a', 1 }` fails `[1] number`, `[2] string` with assign-type-mismatch, and a strict class takes exactly the literal keys it declares. Such keys are named like fields, e.g. "Field `[3]` is not declared in strict class `Test.Tuple`" and "Cannot assign `string` to field `[1]` of type `number`", which also replaces "has no `integer` keys" for `abc[1]`.
…overrides An override's `side = "client"`, `"server"` or `"shared"` sets the side of the Lua files it matches when the resource's fxmanifest.lua does not run them, such as the files a loader like loaf_wrapper's load.lua runs from `files`. They then see the globals, natives and (server) or (client) annotations of that side, and their own globals only reach scripts of that side. A manifest entry always wins, the last matching override with a side decides, and sides are never guessed from folder names. Such a file still counts as loaded on demand, since the manifest does not run it. qbx-lua-ls gives those files the same side, so the client files of such a resource no longer see (server) classes, aliases, enums and overloads. When qbxlint.toml changes, the files whose side moved are indexed again, closed ones included, since their globals reach only scripts of their side.
Right after the `(` or `,` in front of an argument whose parameter takes a
function, completion now offers the function literal with the parameters
of the signature the arguments before it pick, e.g. `function(source)`
after `OnAction("playerUnloaded", ` from the overload that takes
`action: "playerUnloaded"`. A call that is still being written fits a
signature although its later arguments are missing, overloads that fit
equally well each get an item, and `(server)`/`(client)` overloads count
on their side only.
`(` and `,` are completion trigger characters that only ever answer with
this item, so Enter after the `,` of a table still inserts a newline. A
`,` right before the cursor gets a space, and no item is offered inside
tables, strings and comments or in front of an argument already written.
An argument whose parameter lists string values, through its type, an alias such as `---@alias LoafWrapperActions "playerLoaded"|...|string` or the `@overload`s that apply on the side of the call, now completes them in the order they are declared: quoted as soon as the argument starts, on the typed `(` or `,` or when asked, and inside its string, where they replace the contents. Only the signatures the arguments before it fit count, and a value lists the overloads that take it alone. Call snippets leave the stop of such a parameter empty between the quotes, and functions with one get a snippet without taking a callback. When an overload takes one of the values alone, the snippet ends right after it, e.g. `OnAction('$1'$0)`, so that the handler written after the `,` matches the value picked. A snippet whose first stop is a list carries editor.action.triggerSuggest for clients that run it, titled "Suggest values". Strings use the formatter's quote_style, or else the quote most strings of the document use.
Built-in snippets such as RegisterNetEvent and lib.callback.await, the lib.onCache snippet, fxmanifest directive completions like `fx_version '...'` and the bodies qbx/snippets returns now quote their strings the way call snippets already did: with the formatter's quote_style, or else the quote most strings of the document use, and `'` when it has none. qbx/snippets without a document follows quote_style alone. The new qbx/quote request answers with that quote for an open document, or for `null`, so that editors can write their own snippets and templates in it too.
…s parameters A call to a ---@callback await or trigger wrapper passes its `...` to the handler registered under the name it gives, on the other side, so missing-parameter now checks those values against the parameters that handler requires, e.g. "Callback 'add' is called with 0 arguments, but needs 2; 'num1' (number) will be nil" for AwaitServerCallback('add'). The player a handler outside the client receives first is not counted, the handler that needs the fewest values decides when several are registered, and calls ending in a call or `...` are skipped. qbx-lint keeps checking the wrapper's own parameters.
`---@return boolean, string?` declared only the boolean, since the text after the first type was taken as its description, so the second value of `local found, name = GetName()` stayed unknown. Each comma separated entry is now a return value with its own optional name; a description after the last one belongs to it.
A local had one type wherever it was read, so `name` stayed `string?` after `if not name then return end`. A read now leaves out what the conditions around it rule out: `nil` and `false` where the local was found to hold a value, everything else where it was found not to, and `boolean` becomes `true` or `false`. Guards are `name`, `not name`, comparisons with `nil`, `true` and `false`, and those joined by `and` and `or`. They apply to the branches of an `if` or `elseif`, to the code after an `if` whose other branches all end in `return`, `error(...)`, `break` or `goto`, to the body of a `while`, to the right side of `and` and `or`, and to the code after `assert(name)`. A `goto` label ends the code a guard covers, since a jump may have skipped the guard. A local that is assigned again after its declaration is not narrowed, since a guard says nothing about the new value; globals and fields are left alone as well. Hovers and completion use the type at the position of the name, as inference does for every expression that reads it.
…ng)` A function often returns either one set of values or another, such as `false` when it fails and otherwise two names. `@return` and the `:` of a `fun(...)` type now take such sets separated by `|`, with the values of a set in parentheses. Before, the line read as `false|string, string` with `)` as its description. Each position holds what the sets have there, and `nil` for a set that ends before it, so `local firstname, lastname = GetName()` gives `false|string` and `string?`. The sets only hold on a function's one `@return` line; further lines add values as before. The locals that one call declares are narrowed together. A guard on one of them rules out the sets that do not fit it, and the others are read from the sets that are left: after `if not firstname then return end` both names are `string`, and inside the branch `lastname` is `nil`. An undocumented function gets its sets from its `return` statements when they differ and pass several values, so `local ok, err = f()` is linked without an annotation. A `false` or `true` a call returns beside values of another kind stays, as `false|string` rather than `boolean|string`. return-type-mismatch and missing-return compare each `return` with the set it comes closest to, so `return "Joe"` is missing a value and `return 1, "Doe"` returns the wrong type. Hovers and signature help show the sets, and the type names in them are found like those of any other annotation.
Off by default. Turned on, qbx-lua-ls reports each parameter, local and loop variable whose type is unknown: none is declared with `@param` or `@type`, and none can be inferred from its value, from the function a callback is passed to, or from what a loop goes through. A local declared without a value needs a `---@type`, since later assignments are not followed, and a parameter that only takes `any` from the `fun(...)` its function is passed as counts as untyped. `self` and names that start with ignore_unused_prefix are left out. Only the server infers types, so the linter registers the rule for levels, config and ---@diagnostic comments, and maps LuaLS's code of the same name.
… its own way VS Code writes the URI of a file as `file:///c%3A/...`, while the index spells it `file:///C:/...`. The pass that reports the files nobody has open looked open documents up by URI, did not find them, and published each open file a second time without its hints. VS Code takes both spellings for one file, so that list replaced the first: in an open file with a warning or an info, hints such as the fade of an unused argument were never shown. Open documents are now found by path. When a file is closed, what it showed is cleared under the client's spelling, and the file is reported under the one the index has.
…tions Annotations written for fivem-lls-addon name types the stubs did not have. `EventHandler` is now an alias of `EventHandlerData`, whose `key` and `name` had each other's type. `vector` is any of the three vector types, and `json_options`, `json_encode_state` and `msgpack_options` are declared, with `json.encode` taking the state. The vectors gain `r`, `g`, `b`, `a`, `xyz`, `xyzw` and `n`, `EntityInterface` its `__data`, and `promise` its `queue`, `success` and `failure`. The comparison also showed functions CfxLua registers that were reported as undefined: `dot`, `cross`, `inv`, `slerp`, `qua`, `mat` and the sized matrix constructors, `vec1`, `ivec*`, `bvec*`, `each` and `scrub`; the option, marker and extension functions of `json` and `msgpack`; `Citizen.InvokeNative2` and `Citizen.ResultAsObject2`; `utf8.strlenutf8` and `utf8.strcmputf8i`. Each is declared from the runtime's own registration tables, and `for k, v in each(t)` types its variables like `pairs`.
The functions CfxLua adds to the standard libraries, such as `os.nanotime`, `string.strtrim` and `table.clone`, are declared in the CfxLua stub, while `os`, `string` and `table` belong to the Lua stub. A stub had no index entry while it was indexed, so it saw no other file's globals and its `function os.nanotime()` went to a table of its own: the function had no hover and its result was unknown. Each stub now gets its entry first, as workspace files do.
… a dot
A function defined as `function Base.new(class, ...)` and called as
`Stall:new(name)` receives the stall as `class`. The arguments were lined
up from the first parameter unless it was named `self`, so generics bound
to the wrong argument and hints named the wrong parameter:
`lib.array:new(string.strsplit('', iter))` of ox_lib came out as a string.
Functions built from a definition now list their receiver, while
`fun(...)` fields still need a `self` parameter for that.
A rule level from the client's `diagnostics.rules` replaced the one `qbxlint.toml` sets, for every file, so a project could show other findings in the editor than `qbx-lint` reports in CI, and a level set for some files through `[[overrides]]` was lost. The levels of the config file, in `[rules]`, in `[[overrides]]` or taken from LuaLS or EmmyLua settings, now take precedence; `diagnostics.rules` sets the level of the rules the file leaves alone.
A doc comment above `RegisterNetEvent(name, function(id) end)` documents the handler, but it was not read once the statement kept what the call returns: with `handlers[name] = RegisterNetEvent(...)` or `local handler = RegisterNetEvent(...)`, `id` stayed `any` and `no-unknown` reported it. The comment above such a statement now reaches the functions passed to the call. Their parameters take its `@param` types, their returns are checked against its `@return` lines, and a callback registered this way is indexed with the documented signature. Hovering such a parameter shows the description of its `@param` line, above a call on its own too.
qbx-lua-ls reports an `==` or `~=` whose two sides can never be equal, so that the comparison always gives the same answer, e.g. "Comparing `"active"|"busy"|"ready"` with `"invalid_state"` is always false". The sides are different kinds of value, such as a string and a number, or they list literals and none is on both sides. That covers `GetState() == "invalid_state"` for a function whose `@return` lists its states, `not name == "admin"`, a model hash compared with a string, and `type(value) == "tabel"`. The rule is an `info` by default. A side only has a type that something declares: a literal, an operator, a `@param`, `@type` or `@return`, a `@field` of a class, an alias or enum, a stub or a native. A local that is never assigned again has the type of the value it is declared with, so `local state = GetState()` is checked like the call, and the guards around a read still narrow it. Only clear cases count. Types inferred from assigned values are left out, since `Config.Webhook = ''` tells what is stored today and not that `Config.Webhook == false` is never true; the same goes for what an undocumented function returns, for generics, and for `a or b` and `a and b`. Comparisons with `nil` are not reported, as annotations often leave out the `?` of a value that may be missing. A native documented as `boolean` can give scripts `1` instead of `true`, so `IsPedInAnyVehicle(ped) == 1` is left alone. The `lua_type` alias that `type` returns now lists the names CfxLua adds: `vector2`, `vector3`, `vector4`, `quat` and `matrix`. Only the server infers types, so the linter registers the rule for levels, config and ---@diagnostic comments.
A local that takes what a call returns was widened, so the `state` of `local state = GetState()` was a `string` for a function documented with `---@return "active"|"busy"|"ready"`. It now has the type the call returns, as hovers show, and so does each name of `local level, label = GetLevel()`. A local that is assigned again after its declaration stays widened, since it may hold other values of that kind, and a literal written out is widened as before: `local mode = 'dev'` is a `string`.
Escape literal completions for their Lua string delimiter, including completions inside an existing string. Check class table arguments against the overload selected by the call rather than the base signature. Retain side attributes on literal fields and general indices through the LuaCATS parser, index, inference, hovers and diagnostics. Recognize repeat bodies that always return while preserving fallthrough when they can break. Add LSP regression coverage for all four cases, including valid and invalid calls, both script sides, both quote styles and repeat loops with exits. Validation: cargo fmt, Clippy with warnings denied, cargo build, all 291 workspace tests, and the qbx-editor PR Qbox-project#5 integration suite on Windows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
This adds LuaCATS type checking to qbx-lua-ls, together with the annotation, inference and completion work it builds on. It is 35 commits, one per feature or fix, and each commit message describes its change in full; the summary below groups them.
New rules
missing-parameter---@callbackwrapper calls against the registered handler.undefined-doc-name@class,@aliasor@enumdeclares, or that only the other side declares.missing-fieldsassign-type-mismatchundeclared-fieldreturn-type-mismatch,missing-return@returndeclares.impossible-comparison==or~=between types that share no value, such asGetState() == "invalid"for a function that returns"active"|"busy".no-unknownmissing-parameteris reported by qbx-lint and the server. The others need the types only the server indexes, so qbx-lint registers them for levels,qbxlint.tomland---@diagnosticcomments and leaves the check to qbx-lua-ls. The.luarc.jsonfallback maps the LuaLS codes of the same names.The type checks only report clear cases: a different kind of value, or a literal the type does not list. Values of unknown type are skipped, and so is a local that is assigned again after its declaration.
Annotations
(server)and(client)attributes on@class,@field,@alias,@enumand@overload. Scoped declarations follow the manifest side of the file, and scoped overloads follow the side of the call.---@callback register|await|trigger [family]marks a function that wraps a callback system. Calls through it complete and navigate the registered names, show the handler's payload, and return the handler's@returntypes.---@class (strict), LuaLS's(exact), and(loose), withstrict_classes = trueinqbxlint.tomlto make every workspace class strict.@return false | (string, string)lists sets of returned values. This is a qbx extension of LuaCATS.@returnline now declares every comma-separated value; before, only the first was read.---@field [1] numberand other literal keys are kept one by one instead of replacing each other.Inference
nilandfalseafterif not name then return end, insideif name then, afterassert(name)and in similar guarded code. A guard on one value oflocal ok, err = f()narrows the others too.local state = GetState()is"active"|"busy"rather thanstring.return.local handler = RegisterNetEvent(name, function(id) end)now documents the handler.:call to a function defined with a dot, anda and bstaying unknown whenbis.Completion and navigation
quote_style; the newqbx/quoterequest lets editors do the same.---@annotation line, through on-type formatting.Configuration and stubs
[[overrides]]entry can setsidefor scripts the manifest does not list.typealias, and the vector namestype()returns.Things that change for existing projects
missing-parameter.qbxlint.tomlnow take precedence over the client'sdiagnostics.rules; the client setting applies to the rules the file leaves alone.(and,are completion trigger characters. They only ever answer with the function-literal and value items.Structure
The LuaCATS parser (
luacats.rs,types.rs) moves fromqbx_lua_lsinto a newqbx_luacatscrate, so the linter and the server share it. Documentation is updated indocs/rules.md,docs/reference.md, the server's README anddocs/protocol.md.The VS Code side is in a companion qbx-editor pull request.
Checks
cargo fmt --all -- --check,cargo clippy --workspace --all-targets --locked -- -D warningsandcargo test --workspace --lockedpass on Windows (287 tests). Not run on Linux or macOS.crates/qbx_lua_ls/tests/lsp.rs.impossible-comparisonadds 26 findings and changes nothing else; all 26 were read and are real, for example a model hash compared with a string in qbx_adminmenu andnot x == 'leo'in qb-policejob. Keeping the call's type in locals changes no diagnostics.🤖 Generated with Claude Code