luaut-parser 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -11
- package/dist/index.cjs +2007 -293
- package/dist/index.d.cts +234 -12
- package/dist/index.d.ts +234 -12
- package/dist/index.js +2002 -293
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,24 +45,32 @@ annotation resolves to, and `expectedTypeOf` what each call argument should
|
|
|
45
45
|
be, so a tool never has to re-derive a type from text.
|
|
46
46
|
|
|
47
47
|
`parseWithRecovery(source)` returns `{ program, errors }` instead of throwing —
|
|
48
|
-
use it for editors, where the text is usually mid-edit.
|
|
48
|
+
use it for editors, where the text is usually mid-edit. An error costs as
|
|
49
|
+
little of the tree as it can: a broken value becomes an `ErrorExpression`
|
|
50
|
+
(typed `any`) in its place, a broken field or argument is skipped to the next
|
|
51
|
+
`,`, a missing comma between fields on separate lines, or a missing `)`, `}`,
|
|
52
|
+
`then`, `do` or `end`, is recorded and read past — a missing `end` is placed
|
|
53
|
+
by indentation — and an unclosed string ends at its line. Skipping never lets
|
|
54
|
+
an `end` or `}` inside a skipped function or object close the block around
|
|
55
|
+
it. Valid code parses to exactly the same tree as `parse`.
|
|
49
56
|
|
|
50
57
|
Neither analysis mutates the AST; both return side tables.
|
|
51
58
|
|
|
52
59
|
## Projects
|
|
53
60
|
|
|
54
|
-
**No
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
**No globals are built in** — not `print`, not `string`, not `game`. Only the
|
|
62
|
+
language's own utility types are (`Partial`, `Pick`, `Omit`, `Record`,
|
|
63
|
+
`ReturnType`, `Truthy`, ...; see `PRELUDE_SOURCE`). A project lists the type
|
|
64
|
+
libraries it uses in `luaut.config.json`, the way TypeScript uses `@types/*`:
|
|
57
65
|
|
|
58
66
|
```bash
|
|
59
|
-
npm i -D @luaut/roblox # or
|
|
67
|
+
npm i -D @luaut/roblox # Luau + Roblox; or @luaut/lua on its own
|
|
60
68
|
```
|
|
61
69
|
|
|
62
70
|
```jsonc
|
|
63
71
|
// luaut.config.json
|
|
64
72
|
{
|
|
65
|
-
"types": ["roblox"], //
|
|
73
|
+
"types": ["roblox"], // and what it depends on: @luaut/lua
|
|
66
74
|
"paths": { "@shared/*": ["src/shared/*"] }, // import aliases, as in tsconfig
|
|
67
75
|
"sourceMap": "sourcemap.json" // a Rojo sourcemap, or null
|
|
68
76
|
}
|
|
@@ -96,8 +104,13 @@ the config (or sourcemap) it is about. `host` reads files — pass your own to
|
|
|
96
104
|
read unsaved editor buffers or to record what was read.
|
|
97
105
|
|
|
98
106
|
`type` / `typeof` are **not** special-cased in the analyzer either: they are
|
|
99
|
-
overload sets in `@luaut/
|
|
100
|
-
library that declares them, they narrow nothing.
|
|
107
|
+
overload sets in `@luaut/lua` and `@luaut/roblox`, and narrowing is derived
|
|
108
|
+
from them. Without a library that declares them, they narrow nothing.
|
|
109
|
+
|
|
110
|
+
Libraries stack: a name declared again *adds* to what an earlier library gave
|
|
111
|
+
it — overloads of a function accumulate, and the members of a declared table
|
|
112
|
+
merge. That is how `@luaut/roblox` extends Lua's `table` and `type` without
|
|
113
|
+
restating them.
|
|
101
114
|
|
|
102
115
|
## The language, in brief
|
|
103
116
|
|
|
@@ -111,6 +124,31 @@ scope; like a TypeScript function declaration it cannot be reassigned.
|
|
|
111
124
|
`const` and `let` do not apply to functions. `function T.name()` and
|
|
112
125
|
`function T:name()` define a member.
|
|
113
126
|
|
|
127
|
+
**Hoisting** — a function declaration is visible to its whole block, above
|
|
128
|
+
itself too, so `let r: ReturnType<typeof load>` may come before `function
|
|
129
|
+
load()`. A closure reads the name its own value is bound to, as in JavaScript
|
|
130
|
+
(`let m = { clear: function() m.items = {} end }`), and a later name in the
|
|
131
|
+
same block; the compiler declares such a name before the statement that fills
|
|
132
|
+
it. A module's top-level names are visible to code that runs later —
|
|
133
|
+
function bodies and `typeof` — wherever that code is written, since a bundle
|
|
134
|
+
declares them all before the module runs. At the top level the whole function
|
|
135
|
+
is hoisted, and can be called above its declaration. Inside a function only
|
|
136
|
+
the name is: other functions can call it, but a call straight in the block
|
|
137
|
+
above the declaration is an error, because nothing is there yet.
|
|
138
|
+
|
|
139
|
+
**Returns** — a declared return type is checked: what a `return` gives must
|
|
140
|
+
fit it, and a function that declared one must return a value (a guard or an
|
|
141
|
+
`asserts` function needs none). The declared type also types what is written
|
|
142
|
+
there, so a returned callback takes its parameters from it.
|
|
143
|
+
|
|
144
|
+
**Overloads** — a `function name(...)` with no body is a signature for the
|
|
145
|
+
declaration that follows it, as in TypeScript: the signatures are what a call
|
|
146
|
+
sees, and the last one, with the body, is the implementation. `export` goes on
|
|
147
|
+
every line of the set or none of them. A parameter of the implementation that
|
|
148
|
+
carries no annotation holds what the signatures allow there — under
|
|
149
|
+
`get(stat: "hp")` and `get(stat: "name")`, the implementation's `stat` is
|
|
150
|
+
`"hp" | "name"` rather than `any`.
|
|
151
|
+
|
|
114
152
|
**Modules** — `import { a, b as c } from "./m"`, `import D from "./m"` and
|
|
115
153
|
`import * as M from "./m"`; `export const`, `export function`, `export default`,
|
|
116
154
|
`export { a as b }`, `export { a } from "./m"` and `export * from "./m"`.
|
|
@@ -122,8 +160,58 @@ brings in names that are types and nothing else: unlike TypeScript, using one
|
|
|
122
160
|
as a value is an error, and only type positions — `typeof A` included — may
|
|
123
161
|
name it. Compiled code keeps no trace of it.
|
|
124
162
|
|
|
163
|
+
**Array and string methods** — an array and a string answer to methods
|
|
164
|
+
written with `:`, the way JavaScript writes them:
|
|
165
|
+
|
|
166
|
+
```luau
|
|
167
|
+
const long = names:filter(function(n) return #n > 3 end):map(string.upper)
|
|
168
|
+
const first = names:find(function(n) return n:startsWith("A") end)
|
|
169
|
+
print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Which methods those are is not the language's business. The analyzer looks for
|
|
173
|
+
two types by name — `ArrayMethods<T>` and `StringMethods` — and reads an
|
|
174
|
+
array's or a string's members out of whichever type library declared them;
|
|
175
|
+
without such a library an array has no methods at all.
|
|
176
|
+
|
|
177
|
+
Running them is that library's business too. A library points at a JavaScript
|
|
178
|
+
module in its package.json, and the compiler asks it what a call becomes:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
"luaut": { "types": "index.d.luaut", "lowering": "lowering.mjs" }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import type { LoweringPlugin } from "luaut-parser" // the contract, declared here
|
|
186
|
+
|
|
187
|
+
const plugin: LoweringPlugin = {
|
|
188
|
+
runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
|
|
189
|
+
methodCall({ method, receiver, use }) {
|
|
190
|
+
if (receiver?.kind === "array" && method === "filter") {
|
|
191
|
+
return { callee: `${use("array")}.filter` }
|
|
192
|
+
}
|
|
193
|
+
return undefined
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
export default plugin
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`receiver` is the luaut type the analyzer worked out, `use(key)` gives the
|
|
200
|
+
local name that table got — emitted once, at the top of the output, only if a
|
|
201
|
+
call needed it — and the receiver is passed as the call's first argument. An
|
|
202
|
+
answer of `undefined` leaves an ordinary Luau method call, which is what
|
|
203
|
+
`text:upper()` wants, since a string already answers to it.
|
|
204
|
+
|
|
205
|
+
The compiler lowers the language and nothing else: `filter` appears nowhere in
|
|
206
|
+
it.
|
|
207
|
+
|
|
208
|
+
`@luaut/lua` ships the JavaScript-shaped set; there, indices are Luau's (the
|
|
209
|
+
first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
|
|
210
|
+
`shift`, `unshift`, `sort` and `reverse` change the array they are called on.
|
|
211
|
+
|
|
125
212
|
**Optionality** — there is no `T?` shorthand. `?` in type position always
|
|
126
|
-
belongs to a conditional type, and in expression position to a ternary
|
|
213
|
+
belongs to a conditional type, and in expression position to a ternary or an
|
|
214
|
+
optional chain.
|
|
127
215
|
|
|
128
216
|
```luau
|
|
129
217
|
name?: T -- may be absent; its type is `T | nil`
|
|
@@ -133,6 +221,13 @@ name: T | nil -- must be written, but may be nil
|
|
|
133
221
|
Omitting an argument requires `?` (or a default), as in TypeScript — a
|
|
134
222
|
parameter typed `T | nil` still has to be passed something.
|
|
135
223
|
|
|
224
|
+
**Optional chaining** — `a?.b` and `a?:m(x)` are nil when `a` is, and then
|
|
225
|
+
nothing further along the chain runs, arguments included: `folder?:FindFirstChild("A")?.Name`
|
|
226
|
+
is a `string | nil`. The `?` must touch the `.` or `:`; `c ? a : b` stays a
|
|
227
|
+
ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
|
|
228
|
+
an error). A chain that got through narrows what it tested: inside
|
|
229
|
+
`if part?.Parent then`, and `if part?.Name == "Door" then`, `part` is not nil.
|
|
230
|
+
|
|
136
231
|
**Classes** — types are structural, except for classes. A definitions file
|
|
137
232
|
declares one with `declare class`, and it is nominal, as Roblox's classes are:
|
|
138
233
|
|
|
@@ -172,13 +267,32 @@ is `[]` passed where one is expected, including inside an object literal.
|
|
|
172
267
|
|
|
173
268
|
**Calls** — every argument is checked against its parameter, and a generic
|
|
174
269
|
parameter against its constraint (`GetService<K extends keyof Services>`
|
|
175
|
-
rejects `""`).
|
|
270
|
+
rejects `""`). Arguments are checked again once the call's own type arguments
|
|
271
|
+
are known, so `pick("Bones", "Blast1")` is caught where `pick`'s second
|
|
272
|
+
parameter reads `Extract<Rows, { Page: P }>["Skills"][number]`. A type that
|
|
273
|
+
waits on a type parameter — a conditional, an index, `T[K]` — is worked out
|
|
274
|
+
where that parameter is.
|
|
275
|
+
|
|
276
|
+
**Trailing commas** are allowed wherever TypeScript allows them: parameter
|
|
277
|
+
lists, call arguments, generic parameters and type arguments, tables, arrays,
|
|
278
|
+
tuples, imports and exports.
|
|
279
|
+
|
|
280
|
+
A value read by a key narrows the key: after `const path = paths[stat]`, the
|
|
281
|
+
`else` of `if path then` leaves `stat` as exactly the keys `paths` does not
|
|
282
|
+
have — the same correlation `pairs` over a record and a destructured union
|
|
283
|
+
already get.
|
|
176
284
|
|
|
177
285
|
**Narrowing** follows TypeScript's model: references (`x`, `x.a.b`, `x["k"]`)
|
|
178
286
|
rather than just variables, discriminated unions at any depth, `and`/`or`,
|
|
179
287
|
early return, `break`/`continue`, `error()` (declared `-> never`), user type
|
|
180
288
|
guards (`v is T`), and assertion signatures (`asserts v`).
|
|
181
289
|
|
|
290
|
+
Reading a member of, indexing or calling a value that may be nil is an error
|
|
291
|
+
until a check narrows the nil away, as with TypeScript's `strictNullChecks`:
|
|
292
|
+
`FindFirstChild("A"):FindFirstChild("B")` reports that the first call is
|
|
293
|
+
possibly nil. Use `?.` / `?:`, or check first. The read is still typed from the
|
|
294
|
+
non-nil part.
|
|
295
|
+
|
|
182
296
|
Only `nil` and `false` are falsy — `0` and `""` are truthy, unlike JavaScript.
|
|
183
297
|
|
|
184
298
|
**Types** — unions, intersections, tuples `[A, B]`, type packs `(A, B)` (the
|
|
@@ -186,7 +300,14 @@ several values a function returns), `keyof`, `T[K]`, conditional types with
|
|
|
186
300
|
`infer`, mapped types with `as` remapping, template literal types
|
|
187
301
|
(`` `on${Event}` ``), and set difference `A - B`. The utility types
|
|
188
302
|
(`Partial`, `Pick`, `Omit`, `ReturnType`, `Parameters`, `Exclude`, …) are
|
|
189
|
-
written in luaut on top of those
|
|
303
|
+
built in, and written in luaut on top of those rather than special-cased in
|
|
304
|
+
the analyzer. A type library or a file may declare one again; the later
|
|
305
|
+
declaration wins.
|
|
306
|
+
|
|
307
|
+
A call may write its type arguments out — `find<Folder>("Remotes")`,
|
|
308
|
+
`inst:WaitForChild<Folder>("Remotes")` — and a type parameter may have a
|
|
309
|
+
default (`<T = Instance>`) for the calls that do not. `a < b > (c)` is still
|
|
310
|
+
three operators: only a call after the `>` makes it type arguments.
|
|
190
311
|
|
|
191
312
|
`<const T>` infers an argument at its narrowest, as in TypeScript 5.
|
|
192
313
|
|
|
@@ -197,6 +318,47 @@ written in luaut on top of those, not built in.
|
|
|
197
318
|
|
|
198
319
|
**Modules** — `import` / `export`, export lists, re-exports and `export *`.
|
|
199
320
|
|
|
321
|
+
**`satisfies`** — checks a value against a type without giving it that type,
|
|
322
|
+
as in TypeScript 4.9:
|
|
323
|
+
|
|
324
|
+
```luau
|
|
325
|
+
type Shape = { kind: "circle" | "rect", size: number }
|
|
326
|
+
const circle = { kind: "circle", size: 2 } satisfies Shape -- { kind: "circle", size: number }
|
|
327
|
+
const handlers = {
|
|
328
|
+
Click: function(x) return x + 1 end, -- x: number, from the contract
|
|
329
|
+
} satisfies { [string]: (x: number) -> number }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The contract types callbacks and empty arrays, and a literal stays a literal
|
|
333
|
+
where the contract asks for literals (`kind: "circle"`, not `string`). A value
|
|
334
|
+
that does not fit is an error. So is a property the contract does not know —
|
|
335
|
+
TypeScript's excess property check, which applies to an object literal written
|
|
336
|
+
straight into an annotation (`const s: Shape = { ..., typo: 1 }`) too. A value
|
|
337
|
+
that already has a type of its own keeps it exactly: `{ ... } as const
|
|
338
|
+
satisfies T` stays readonly and literal. `as` reinterprets instead of
|
|
339
|
+
checking, and compiled code keeps neither.
|
|
340
|
+
|
|
341
|
+
**Undeclared names** — `analyzeScopes(program, { builtinGlobals, reportUndeclared: true })`
|
|
342
|
+
reports each read of a name nothing declares: "Cannot find name 'x'". A global
|
|
343
|
+
assigned in the file (`x = 1`) and a `declare` count as declarations.
|
|
344
|
+
`analyzeTypes(program, scopes, { reportUnknownTypes: true })` does the same for
|
|
345
|
+
type names. Both are off by default, since they are only right when the type
|
|
346
|
+
libraries the file names are loaded.
|
|
347
|
+
|
|
348
|
+
**Directives** — comments that switch checking off, as TypeScript's
|
|
349
|
+
`// @ts-...` do. They silence scope and type errors, never syntax errors:
|
|
350
|
+
|
|
351
|
+
```luau
|
|
352
|
+
--@luaut-nocheck -- before the first line of code: the whole file
|
|
353
|
+
--@luaut-ignore -- the next line of code
|
|
354
|
+
--@luaut-expect-error -- the next line of code, which must have an error
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`parseWithRecovery` returns them as `directives`; `directivesOf(source)` reads
|
|
358
|
+
them for a caller that parsed some other way, and
|
|
359
|
+
`applyDirectives(directives, diagnostics, lineOf)` filters a list and names
|
|
360
|
+
each `expect-error` that had nothing to suppress.
|
|
361
|
+
|
|
200
362
|
## Options
|
|
201
363
|
|
|
202
364
|
```ts
|