Skip to content

feat: complete and check the values of declared types - #13

Merged
ChatDisabled merged 12 commits into
Qbox-project:mainfrom
loaf-scripts:main
Oct 1, 2026
Merged

ChatDisabled merged 12 commits into
Qbox-project:mainfrom
loaf-scripts:main

Conversation

@loaf-scripts

Copy link
Copy Markdown
Contributor

Changes

Three commits that make the server use the type a value is declared with: completion offers the values the type lists, and assign-type-mismatch reports values the type does not take.

Completion of the values a declared type lists

A value whose type lists what it can be, such as ---@alias State "busy"|"working"|"ready", got no suggestions outside a call. The values are now offered where such a value starts:

  • after the = of a local or an assignment, from the ---@type above it or from what the assigned local or class field is declared as,
  • after the = of a field in a table typed as a class, from its @field,
  • after return, from the @return of the function,
  • after == and ~=, from what the other side is declared as.

Details:

  • What is listed: strings in quotes, integers as they are, true and false for a boolean, and the nil a listing type allows, last. string? lists nothing.
  • Functions: where a function is stored or returned, snippet clients get the function literal with the parameters its fun(...) type names.
  • Inside a string: the string values replace the string's contents, including on either side of a comparison.
  • Triggers: = and space are trigger characters for these items only; a request triggered by one of them returns nothing else.
  • Call arguments: integers, booleans and nil are listed the same way. A typed ( or , still opens the list only for a parameter that lists a string or an integer.
  • Not offered: types inferred from assigned values list nothing, and nothing is offered in front of a value that is already written.

---@type lists

A ---@type line that lists several types, as ---@type boolean, string? does above local ok, err = pcall(...), was read up to its first comma, and that one type was given to every name: err was a boolean. The line now types the names one by one, and a name past the end of the list keeps its inferred type. One type still covers every name.

Hovers, the completion above, the class a table constructor is checked against and impossible-comparison follow the type of the name at hand.

assign-type-mismatch for typed variables

The rule only covered the fields of class tables, so a variable took any value whatever its annotation said. It now also checks variables whose type is declared:

---@alias State 1 | 2 | 3 | false | true

---@type State
local state = "test" -- Cannot assign `"test"` to `state` of type `State`

state = "abc"        -- Cannot assign `"abc"` to `state` of type `State`

The rule checks:

  • the values of a local or an assignment with a ---@type above it,
  • later assignments to a local declared with ---@type,
  • later assignments to a parameter documented with @param.

A mismatch is what it is for return-type-mismatch: a different kind of value, or a literal the type does not list. Values whose type is not known pass.

  • Declared type, not narrowed: name = "x" inside if not name then passes for a string?.
  • nil: = nil needs a type that allows it; a local declared without a value is not reported.
  • Calls: a call at the end of the values gives each name the value it returns at that position.
  • Not checked: locals without an annotation, and globals in assignments that have no ---@type above them.
  • Class fields: a value that a class field rejects is reported once, for the field.

Docs

docs/reference.md has a new "Typed variables" section. docs/rules.md, the rule description, the server README and crates/qbx_lua_ls/docs/protocol.md cover the new completion and the wider rule.

Checks

  • cargo fmt --all -- --check and cargo clippy --workspace --all-targets: clean.
  • cargo test --workspace: passes at the head of the branch. The ---@type list commit also passes the qbx_luacats and qbx_lua_ls tests on its own.
  • New LSP tests values_offer_what_their_declared_type_lists and values_assigned_to_typed_variables_are_checked, and a parser test for ---@type lists.
  • For the two diagnostics commits, diffed the published diagnostics before and after on four server trees (QBox, QBcore, ESX and a private one, about 3,800 diagnostics). The only change is two new findings, both a nil stored under a non-optional ---@type. One is in qbx_medical/client/main.lua:51 (---@type number above Hp = nil); the other is in a private resource.
  • Checked the State example above against a release build of the server over stdio.
  • Not run: a manual check in VS Code.

🤖 Generated with Claude Code

loaf-scripts and others added 12 commits September 30, 2026 18:26
A value whose type lists what it can be, such as `---@alias State
"busy"|"working"|"ready"`, got no suggestions outside a call: nothing
opened after `local state = ` under a `---@type State`, and Ctrl+Space
there returned nothing. The values are now offered where such a value
starts on its line:

- after the `=` of a `local` or an assignment, from the `---@type`
  above it or from what the assigned local or class field is declared
  as,
- after the `=` of a field in a table typed as a class, from its
  `@field`,
- after `return`, from the `@return` of the function,
- after `==` and `~=`, from what the other side is declared as.

Strings are written in quotes, and integers such as those of `1|2|3`
as they are. A `boolean` lists `true` and `false`, and a type that
lists values also lists the `nil` it allows, last, so `State?` ends
with `nil` while `string?` lists nothing. Where a function is stored or
returned, snippet clients get the function literal that arguments
already get, with the parameters the `fun(...)` type names. Inside a
string in one of those places, or on either side of the comparison, the
string values replace the string's contents. A typed word filters the
values, beside the names in scope.

A local typed by `---@type` or `@param` keeps that type where it is
assigned again. Types inferred from assigned values list nothing, since
`local mode = 'dev'` tells what is stored today, and nothing is offered
in front of a value that is already written.

`=` and space are completion trigger characters for these items only: a
request triggered by one of them returns nothing else, so no other
space opens a list.

Call arguments list integers, booleans and `nil` the same way. A typed
`(` or `,` still opens the list only for a parameter that lists a
string or an integer; the `true` and `false` of the many `boolean`
parameters are listed on request or once a word is typed. Call snippets
keep their quotes for parameters that list strings.
A `---@type` line that lists several types, as `---@type boolean,
string?` does above `local ok, err = pcall(...)`, was read up to its
first comma, and that one type was given to every name of the
statement: `err` was a `boolean`.

The line now types the names one by one, and a name past the end of the
list keeps the type inferred from its value. One type still covers
every name.

Hovers, the values completion offers, the class a table constructor is
checked against and `impossible-comparison` follow the type of the name
at hand.
`assign-type-mismatch` only covered the fields of class tables, so a
variable took any value whatever its annotation said:

    ---@alias State 1 | 2 | 3 | false | true

    ---@type State
    local state = "test"

    state = "abc"

Both values are now reported, as "Cannot assign `"test"` to `state` of
type `State`". The rule checks:

- the values of a `local` or an assignment with a `---@type` above it,
- later assignments to a local declared with `---@type`,
- later assignments to a parameter documented with `@param`.

A mismatch is what it is for `return-type-mismatch`: a different kind
of value, or a literal the type does not list. Values whose type is not
known pass. A call at the end of the values gives each name the value
it returns at that position.

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?`. `= nil` needs a type that
allows it, while a `local` declared without a value is not reported.

Locals without an annotation take any value, since their type is
inferred from what they hold, and so do globals in assignments that
have no `---@type` above them. A value that a class field rejects is
reported once, for the field.
@ChatDisabled
ChatDisabled merged commit fd87220 into Qbox-project:main Oct 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants