Builtin Library
README.md
luau_introspect (module)
Parses Luau/asset source TEXT into structured declarations. Pure
string/pattern parsing (string.find / string.match / string.gmatch)
over the literal source — no compiling, no VM spawn, no runtime state, no
engine FFI. Given source text, it extracts what is written in the file.
This is the shared parser every per-assetType M.ref.inspect hook uses to
turn a component/module/tool's own source into the structured record
asset.inspect returns.
Exports
docstrings(src) -> DocMap— scans--!desc/--!arg/--!return/--!examplecomment runs and binds each run to the name of the declaration on the next non-comment line (the identifier afterfunction,public:,local function, orM.).publicFields(src) -> { FieldEntry }— parsespublic = { name = Field.<kind>(default, mode), ... }table-literal entries and module-scopepublic.<name> = Field.<kind>(...)assignments.events(src) -> { EventEntry }— parsesevents = { name = Event(payloadSchema?, syncMode?), ... }table-literal entries.methods(src) -> { MethodEntry }— parsesfunction public:<name>(...)/typed function public:<name>(...)declarations (params + optional return type).lifecycleHooks(src, catalog) -> { string }— top-levelfunction <name>(declarations whose name is in the caller-suppliedcatalogarray.moduleExports(src) -> { ExportEntry }— parsesM.<name> = functionassignments,function M.<name>(...)declarations, and a trailingreturn { foo = foo, ... }export table.forSource(src, computeFn) -> any— memoisescomputeFn(src)keyed onsrcitself in a module-local table. The same source text returns the cached result without re-runningcomputeFn; different source text recomputes. Keyed on the source content (not an asset checksum) because composite/container assets (component folders, scene folders, etc.) all hash to the empty-string checksum — a checksum-keyed cache collapses every such asset's detail onto whichever one was inspected first.
Usage
local Introspect = require("modules.luau_introspect")
local src = self:getInitScript()
local detail = Introspect.forSource(src, function(s)
return {
fields = Introspect.publicFields(s),
events = Introspect.events(s),
methods = Introspect.methods(s),
hooks = Introspect.lifecycleHooks(s, lifecycleCatalog),
}
end)
Notes
- Every function is a pure string→table transform: same source text in, same structured table out, every time. No reads of live component state, no entity/world queries.
- Structural scanning runs over a comment- and string-aware mask of
the source (a same-length copy where comment bodies and string-literal
contents are blanked to spaces, delimiters and newlines preserved).
Pattern matches and brace/paren balancing run on the mask — so a brace,
Field.,Event(, or a whole entry that appears inside a comment or a string literal never registers — while captured values are sliced from the original source at the same (aligned) indices. This makes the parser robust to apostrophes in comments, commented-out entries, string defaults containing}/Field./Event(, and[[ ]]long strings. default/categoryare raw trimmed source-text slices (with any trailing comment stripped), not evaluated values.- For a ref constructor (
Field.assetRef/dataRef/resource/componentRef) the FIRST positional argument is the category / contract / componentType selector, captured undercategory; the SECOND argument is captured underdefault. - For every other Field kind the first argument is the
defaultandcategoryis nil.
- For a ref constructor (
- A trailing
Sync/NoSyncidentifier is captured assync, tolerating a trailing comma or a trailing comment after the last argument. - Field/Event entries whose key is a computed or indirect expression
(
[expr] = Field.number(...)) are omitted — only literal identifier keys are recognised. methodscaptures a...variadic parameter (asname = "..."), with its: <type>annotation when present.- A
--!descwritten above a field/event declaration (<name> = Field.<kind>(...)/<name> = Event(...)) or apublic.<name> =assignment binds to that name indocstrings, and is folded into the correspondingpublicFields/eventsentry'sdesc. lifecycleHooksnever hardcodes the lifecycle-callback catalog; the caller passes the list of names to match against.forSource's memo is bounded (oldest-key eviction) so a long session touching many distinct asset versions can't grow it without limit.