Skip to content

feat: read LuaLS type files for exports and library globals - #15

Merged
ChatDisabled merged 14 commits into
Qbox-project:mainfrom
loaf-scripts:feat/declared-exports
Oct 1, 2026
Merged

ChatDisabled merged 14 commits into
Qbox-project:mainfrom
loaf-scripts:feat/declared-exports

Conversation

@loaf-scripts

Copy link
Copy Markdown
Contributor

Changes

Three commits that let the server read type files written for LuaLS from the library setting. Such a folder only contributed its classes and aliases: the types it declares for exports, the extra signatures of repeated @fields and the globals it defines were ignored.

Types declared for exports

Type files describe the exports of a resource by typing its exports entry:

---@class PhoneExports
---@field GetConfig fun(self: PhoneExports): PhoneConfig

---@type PhoneExports
exports["phone"] = {}

exports["phone"] only listed the exports the resource registers with exports('Name', fn), so the exports of a resource that is escrowed or not in the workspace had no hover, completion or type. exports["res"] and exports.res now take the members of the declared type, followed by the registered exports.

  • Precedence: where both name an export, the declared type wins.
  • Where: declarations are found in any indexed file, across resources, as registered exports are. The file needs no manifest and no ---@meta.
  • Completion: exports[' and resource-name arguments such as GetResourceState(' also list the resources these declarations name.
  • fivem/unknown-export: unchanged. It still goes by what a readable resource registers, since a declaration doesn't make an export exist at runtime.

Repeated function @fields

Type files declare a function field once per way it can be called, such as an export that takes other parameters on the server:

---@field IsInCall fun(self: PhoneExports): boolean # client-side
---@field IsInCall fun(self: PhoneExports, source: number): boolean, number? # server-side

Every lookup used the first field, so server code got the client's parameters and return values. A function field that repeats the name of an unscoped one is now an overload of it, the way LuaLS reads it, and a call picks the signature its arguments fit: exports.phone:IsInCall(source) returns boolean, number?.

  • Hover: shows the descriptions of both lines.
  • Sides: a repeated field marked (server) or (client) gives its signature that side. Fields that are themselves scoped to a side stay separate, as before.
  • Other fields: repeated fields that aren't functions are unchanged.

Globals of definition files outside resources

A file that belongs to no resource only shared its globals with other such files. With a folder of LuaLS definitions in library, a resource didn't see the globals it declares: MySQL.update had no type, and undefined-global reported the names it defines. Such a file now declares its globals for every resource when:

  • it comes from a library folder, the way LuaLS reads its library, or
  • it's a workspace file with ---@meta above its first statement.

Other workspace files outside resources keep their globals to themselves. Opening a whole server folder indexes artifacts/citizen/scripting/lua/scheduler.lua, which defines CreateThread, Wait and exports without types.

  • fivem/import-not-declared: still reports a library global like MySQL when the manifest lacks the import that defines it, since a type file doesn't load it at runtime.
  • Library inside the workspace: a library folder that is also part of the workspace is indexed as workspace files first, so its files need ---@meta.

Checks

  • cargo fmt --all -- --check and cargo clippy --workspace --all-targets --locked: clean.
  • cargo test --workspace --locked: passes at the head of the branch, and each commit passes on its own.
  • New LSP tests declared_types_describe_the_exports_of_a_resource and definition_files_outside_resources_declare_globals_for_every_resource, on a declared_exports fixture with a workspace and a library folder, and a parser test repeated_function_fields_become_signatures.
  • Diffed the published diagnostics before and after on four server trees (QBox, QBcore, ESX and a private one, about 3,000 diagnostics): no change, also on QBox with a folder of LuaLS type files as library.
  • Checked hover, completion and types for exports["lb-phone"] and exports["lb-tablet"] against their LuaLS type files, with 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 14 commits October 1, 2026 04:42
Type files written for LuaLS declare a function field once per way it
can be called, such as an export that takes other parameters on the
server:

    ---@Class PhoneExports
    ---@field IsInCall fun(self: PhoneExports): boolean # client-side
    ---@field IsInCall fun(self: PhoneExports, source: number): boolean, number? # server-side

Both fields were kept and every lookup used the first, so server code
got the client's parameters and return values. A function field that
repeats the name of an unscoped one now becomes an overload of it, the
way LuaLS reads it, and a call picks the signature its arguments fit:
`phone:IsInCall(source)` returns `boolean, number?`. Hovers show the
descriptions of both lines.

A repeated field marked `(server)` or `(client)` gives its signature
that side. Fields scoped to a side themselves stay apart as before, and
so do fields that are not functions.
…r it

Type files written for LuaLS describe the exports of a resource by
typing its `exports` entry:

    ---@type PhoneExports
    exports["phone"] = {}

`exports["phone"]` only listed the exports the resource registers with
`exports('Name', fn)` in the workspace, so the exports of a resource
that is escrowed or not part of the workspace had no hover, completion
or type, even with such a file in the `library` setting.

`exports["res"]` and `exports.res` now take the members of a type
declared this way in any indexed file, followed by the exports the
resource registers. Where both name an export, the declared type wins.
Declarations are found across resources, as registered exports are,
so a library folder without an fxmanifest.lua works too. `exports['`
and resource-name arguments such as `GetResourceState('` also complete
the resources these declarations name.

`fivem/unknown-export` still goes by what a readable resource
registers, since a declaration does not make an export exist at
runtime.
Files that belong to no resource, such as a folder of type definitions
made for LuaLS in the `library` setting, only shared their globals
with each other. Their classes and aliases worked everywhere, but a
resource did not see the globals they declare: `MySQL.update` from
such a folder had no type, and `undefined-global` reported the names
they define.

A file outside any resource now declares its globals for every
resource when it comes from a `library` folder, the way LuaLS reads its
library, or when it is a workspace file with `---@meta` above its first
statement. Other workspace files outside resources, such as the runtime
scripts in a server's `artifacts` folder, keep their globals to
themselves.

`fivem/import-not-declared` still reports a library global like `MySQL`
in a resource whose manifest lacks the import that defines it, since a
type definition does not load it at runtime.
@ChatDisabled
ChatDisabled force-pushed the feat/declared-exports branch from 8e28ee5 to f98729a Compare October 1, 2026 02:43
@ChatDisabled
ChatDisabled merged commit c33a513 into Qbox-project:main Oct 1, 2026
2 checks passed
@loaf-scripts
loaf-scripts deleted the feat/declared-exports branch October 1, 2026 07:37
@loaf-scripts
loaf-scripts restored the feat/declared-exports branch October 1, 2026 07:37
@loaf-scripts
loaf-scripts deleted the feat/declared-exports branch October 1, 2026 07:38
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