@tilua/parser 3.1.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 CHANGED
@@ -124,14 +124,31 @@ Every problem comes back as `{ file, message, line, column }`, pointing into
124
124
  the config (or sourcemap) it is about. `host` reads files — pass your own to
125
125
  read unsaved editor buffers or to record what was read.
126
126
 
127
- `type` / `typeof` are **not** special-cased in the analyzer either: they are
128
- overload sets in `@tilua-types/lua` and `@tilua-types/roblox`, and narrowing is derived
129
- from them. Without a library that declares them, they narrow nothing.
127
+ `typeof v` is the language's own, an operator like `not` and `#`: it needs no
128
+ library, and Lua's `type()` and Luau's `typeof()` functions are declared by
129
+ none. It answers Luau's names — `"nil"`, `"boolean"`, `"number"`, `"string"`,
130
+ `"function"`, `"thread"`, `"buffer"`, `"userdata"` — except for a table, which
131
+ is an `"array"` or an `"object"`. `typeof v == "array"` narrows `v`, and
132
+ `== "table"` is an error: it is never the answer. There is no `typeof(v)`
133
+ function: written that way it is an error, and a parenthesised operand stands
134
+ apart from the operator — `typeof (a or b)`.
135
+
136
+ What a runtime's own values answer is a library's to say, in one alias:
137
+
138
+ ```ts
139
+ type TypeofNames = { Vector3: Vector3, Instance: Instance }
140
+ ```
141
+
142
+ Each property is the name `typeof` gives a value of the property's type; a
143
+ value several fit (a `Part` is an `Instance`) is the first one's.
144
+ `TypeAnalysis.typeofTables` lists each `typeof v` whose `v` is, by its type,
145
+ an array or an object wherever it is a table — what a compiler need not find
146
+ out as the program runs.
130
147
 
131
148
  Libraries stack: a name declared again *adds* to what an earlier library gave
132
149
  it — overloads of a function accumulate, and the members of a declared table
133
- merge. That is how `@tilua-types/roblox` extends Lua's `table` and `type` without
134
- restating them.
150
+ merge. That is how `@tilua-types/roblox` extends Lua's `table` without
151
+ restating it.
135
152
 
136
153
  ## The language, in brief
137
154
 
@@ -340,7 +357,8 @@ such as `print(x)`, and `globalValue` for a global read as a value, such as
340
357
  `local p = print`. A local with the same name belongs to the author, and no
341
358
  hook is asked about it. A runtime can read `__LINES__`, the bundle's line map
342
359
  (`nil` outside a bundle), to turn a line Luau reports back into a place in the
343
- project. `console:log`, `print` and `error` in `@tilua-types/lua` are built on
360
+ project. One that answers with a new array wraps it in `__ARRAY__(...)`, which
361
+ writes it down as an array for `typeof`. `console:log`, `print` and `error` in `@tilua-types/lua` are built on
344
362
  these hooks.
345
363
 
346
364
  **A `(` that starts a line** continues the statement above it, as in Lua and
@@ -383,8 +401,8 @@ declare class Part extends BasePart { Shape: EnumItem }
383
401
  A `Part` is a `BasePart` and an `Instance` because it extends them. A
384
402
  `ReplicatedStorage` is not a `Part`, and no table literal is an `Instance`,
385
403
  however alike their members. A class still fits a shape that names members it
386
- has (`{ Name: string }`). It is not a table, though, so `typeof(part)` picks
387
- the `"Instance"` overload, not `"table"`. Members are inherited, and a subclass
404
+ has (`{ Name: string }`). It is not a table, though, so `typeof part` is
405
+ `"Instance"`, as the library's `TypeofNames` says, not `"object"`. Members are inherited, and a subclass
388
406
  may narrow one (`Parent: SomeFolder`).
389
407
 
390
408
  **`class ... { }`** — a class written in code, rather than declared in a
@@ -804,8 +822,9 @@ three operators: only a call after the `>` makes it type arguments.
804
822
 
805
823
  `typeof x` in a type is TypeScript's type query — the type of a value
806
824
  (`typeof config`, `typeof config.volume`, `ReturnType<typeof f>`). Luau's
807
- `typeof(expr)` spelling works too. It is compile-time only, unrelated to the
808
- `typeof(v)` function that returns a string at runtime.
825
+ `typeof(expr)` spelling is an error here as well. It is compile-time only, unrelated to the
826
+ `typeof v` operator in an expression, which answers a string as the program
827
+ runs.
809
828
 
810
829
  **Modules** — `import` / `export`, export lists, re-exports and `export *`.
811
830
 
package/dist/index.cjs CHANGED
@@ -711,6 +711,8 @@ var UNUSED_EXPECT_ERROR = "Unused '@tilua-expect-error' directive";
711
711
 
712
712
  // src/ast/builders.ts
713
713
  var NO_DOTS = "tilua has no bare '...': take a rest parameter, '...args: T[]', or 'scriptArgs' for the script's own";
714
+ var NO_MEMBER_NAME = "Expected a name after '.'";
715
+ var NO_TYPEOF_CALL = "'typeof' is an operator, not a function: write 'typeof v'";
714
716
  var ACCESSIBILITIES = /* @__PURE__ */ new Set(["public", "private", "protected"]);
715
717
  var CLASS_MODIFIERS = /* @__PURE__ */ new Set([...ACCESSIBILITIES, "static", "abstract", "override", "readonly"]);
716
718
  function bindThis(func, at) {
@@ -2248,6 +2250,7 @@ var Parser = class {
2248
2250
  * `:` Identifier followed by one of Lua's call forms. */
2249
2251
  startsMethodCall(offset = 0) {
2250
2252
  if (this.peek(offset + 1).type !== "Identifier") return false;
2253
+ if (this.startsOwnLine(this.peek(offset), this.peek(offset + 1))) return false;
2251
2254
  const after = this.peek(offset + 2);
2252
2255
  if (after.type === "Punctuator") {
2253
2256
  const v = String(after.value);
@@ -2273,6 +2276,23 @@ var Parser = class {
2273
2276
  const isString = t.type === "InterpolatedString" || t.type === "Literal" && t.kind === "string";
2274
2277
  return isString && this.punctuatorAt(1, ":") && this.startsMethodCall(1);
2275
2278
  }
2279
+ /** Is there no member name at `offset`, after the `.` or `:` before it? */
2280
+ namelessAfter(offset) {
2281
+ const name = this.peek(offset);
2282
+ if (name.type !== "Identifier") return true;
2283
+ const before = offset === 0 ? this.previous() : this.peek(offset - 1);
2284
+ return before !== void 0 && this.startsOwnLine(before, name);
2285
+ }
2286
+ /** Recovery only. `obj.` may go on to its name on a later line, as in
2287
+ * JavaScript — but while `obj.` is still being typed, the line below is
2288
+ * whatever statement was already there, and reading it as the name turns
2289
+ * one unfinished line into errors about the next. So, as a missing `}`
2290
+ * is placed, the indentation says which: a line that carries the access
2291
+ * on is indented deeper than the line it carries on from. */
2292
+ startsOwnLine(before, name) {
2293
+ if (!this.recover || name.line.start <= before.line.end) return false;
2294
+ return this.indentOf(name) <= this.indentOf(before);
2295
+ }
2276
2296
  /** Does the next token start right where the current one ends? */
2277
2297
  touchesNext() {
2278
2298
  const current = this.current();
@@ -2314,6 +2334,7 @@ var Parser = class {
2314
2334
  const t = this.current();
2315
2335
  if (t.type === "Keyword" && t.value === "not") return "not";
2316
2336
  if (t.type === "Operator" && (t.value === "-" || t.value === "#")) return t.value;
2337
+ if (t.type === "Identifier" && t.value === "typeof") return "typeof";
2317
2338
  return null;
2318
2339
  }
2319
2340
  /** Runs `fn` inside a bracket, where a `:` can only ever be a method
@@ -2413,6 +2434,10 @@ var Parser = class {
2413
2434
  const op = this.isUnaryOperator();
2414
2435
  if (op) {
2415
2436
  const opTok = this.advance();
2437
+ const next = this.current();
2438
+ if (op === "typeof" && this.checkPunctuator("(") && next.line.start === opTok.line.end && next.column.start === opTok.column.end) {
2439
+ this.problemAt(next, NO_TYPEOF_CALL);
2440
+ }
2416
2441
  const argument = this.parseExpression(UNARY_PRECEDENCE);
2417
2442
  return {
2418
2443
  type: "UnaryExpression",
@@ -2582,6 +2607,13 @@ var Parser = class {
2582
2607
  if (this.checkPunctuator("?") && this.touchesNext()) {
2583
2608
  const next = this.peek(1);
2584
2609
  const punct = next.type === "Punctuator" ? String(next.value) : void 0;
2610
+ if (punct === "." && this.recover && this.namelessAfter(2) && !this.punctuatorAt(2, "[") && !this.punctuatorAt(2, "(")) {
2611
+ this.advance();
2612
+ this.advance();
2613
+ this.problemAt(this.previous(), NO_MEMBER_NAME);
2614
+ base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
2615
+ break;
2616
+ }
2585
2617
  if (punct === "." && this.peek(2).type === "Identifier") {
2586
2618
  this.advance();
2587
2619
  this.advance();
@@ -2636,9 +2668,9 @@ var Parser = class {
2636
2668
  }
2637
2669
  }
2638
2670
  if (this.matchPunctuator(".")) {
2639
- if (this.recover && !this.checkType("Identifier")) {
2640
- this.softError("Expected identifier");
2641
- base = { type: "ErrorExpression", ...spanFrom(base, this.previous()) };
2671
+ if (this.namelessAfter(0)) {
2672
+ this.problemAt(this.previous(), NO_MEMBER_NAME);
2673
+ base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
2642
2674
  break;
2643
2675
  }
2644
2676
  const prop = this.parseIdentifier();
@@ -2651,6 +2683,12 @@ var Parser = class {
2651
2683
  base = { type: "IndexExpression", object: base, index, ...spanFrom(base, this.previous()) };
2652
2684
  continue;
2653
2685
  }
2686
+ if (this.recover && !this.ternaryColons && this.checkPunctuator(":") && this.peek(1).line.start > this.current().line.end && !this.startsMethodCall()) {
2687
+ this.advance();
2688
+ this.problemAt(this.previous(), "Expected a name after ':'");
2689
+ base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
2690
+ break;
2691
+ }
2654
2692
  if (this.checkPunctuator(":") && this.startsMethodCall() && this.takeMethodColon()) {
2655
2693
  this.advance();
2656
2694
  const method = this.parseIdentifier();
@@ -3494,6 +3532,7 @@ var Parser = class {
3494
3532
  return { type: "TupleTypeNode", elements, ...rest ? { rest } : {}, ...spanFrom(t, this.previous()) };
3495
3533
  }
3496
3534
  if (t.type === "Identifier" && t.value === "typeof" && this.peek(1).type === "Punctuator" && this.peek(1).value === "(") {
3535
+ this.problemAt(this.peek(1), NO_TYPEOF_CALL);
3497
3536
  this.advance();
3498
3537
  this.advance();
3499
3538
  const expression = this.parseExpression();
@@ -3874,7 +3913,7 @@ var BinaryOperators = [
3874
3913
  "and",
3875
3914
  "or"
3876
3915
  ];
3877
- var UnaryOperators = ["-", "not", "#"];
3916
+ var UnaryOperators = ["-", "not", "#", "typeof"];
3878
3917
 
3879
3918
  // src/ast/analyzeScopes.ts
3880
3919
  function getBinding(analysis, id) {
@@ -4501,7 +4540,9 @@ var Analyzer = class {
4501
4540
  case "BooleanLiteral":
4502
4541
  case "NumberLiteral":
4503
4542
  case "StringLiteral":
4543
+ return;
4504
4544
  case "ErrorExpression":
4545
+ if (expr.expression) this.visitExpression(expr.expression, scope);
4505
4546
  return;
4506
4547
  case "InterpolatedStringExpression":
4507
4548
  for (const part of expr.parts) {
@@ -5807,6 +5848,13 @@ function mergeEnv(a, b, base) {
5807
5848
  }
5808
5849
  return out;
5809
5850
  }
5851
+ function joinNarrowings(into, a, aFrom, b, bFrom) {
5852
+ for (const [key, left] of a) {
5853
+ const right = b.get(key);
5854
+ if (right === void 0 || left === aFrom.get(key) || right === bFrom.get(key)) continue;
5855
+ into.set(key, union([left, right]));
5856
+ }
5857
+ }
5810
5858
  function mergeIndexer(a, b) {
5811
5859
  return a ? { key: union([a.key, b.key]), value: union([a.value, b.value]) } : b;
5812
5860
  }
@@ -6086,6 +6134,10 @@ function unwrapParens(e) {
6086
6134
  while (e.type === "ParenthesizedExpression") e = e.expression;
6087
6135
  return e;
6088
6136
  }
6137
+ function memberLabel(label, key) {
6138
+ if (label === void 0 || key === void 0) return void 0;
6139
+ return typeof key === "number" ? `${label}[${key}]` : `${label}.${key}`;
6140
+ }
6089
6141
  function expressionLabel(e, depth = 0) {
6090
6142
  if (depth > 6) return void 0;
6091
6143
  const args = (list) => {
@@ -6157,6 +6209,7 @@ var TypeAnalyzer = class {
6157
6209
  metatables = [];
6158
6210
  metatableCache = /* @__PURE__ */ new WeakMap();
6159
6211
  methodSources = /* @__PURE__ */ new Map();
6212
+ typeofTables = /* @__PURE__ */ new Map();
6160
6213
  /** Public: each alias resolved once (generic aliases keep their params as
6161
6214
  * `typeParam` nodes in the body). */
6162
6215
  aliases = new LazyMap();
@@ -6235,7 +6288,7 @@ var TypeAnalyzer = class {
6235
6288
  const given = this.options.globalTypes?.[name];
6236
6289
  if (given) this.bindingType.set(id, given);
6237
6290
  else if (this.libGlobalTypes.has(name)) this.bindingType.defer(id, () => this.libGlobalTypes.get(name) ?? anyType);
6238
- else if (LANGUAGE_GLOBALS.includes(name)) this.bindingType.set(id, arrayOf(unknownType));
6291
+ else if (LANGUAGE_GLOBALS.includes(name)) this.bindingType.set(id, arrayOf(declaredUnknownType));
6239
6292
  else this.bindingType.set(id, anyType);
6240
6293
  }
6241
6294
  setAliasExpander((t) => this.expand(t));
@@ -6258,6 +6311,7 @@ var TypeAnalyzer = class {
6258
6311
  aliases: this.resolveDeferredAliases(),
6259
6312
  loops: this.loops,
6260
6313
  methodSources: this.methodSources,
6314
+ typeofTables: this.typeofTables,
6261
6315
  metatableMembers: (type) => this.metatableMembers(type),
6262
6316
  diagnostics: this.diagnostics
6263
6317
  };
@@ -7108,7 +7162,12 @@ var TypeAnalyzer = class {
7108
7162
  this.aliases.defer(name, () => this.classType(cls));
7109
7163
  continue;
7110
7164
  }
7111
- if (this.dependsOnTypeQuery(name)) continue;
7165
+ if (this.dependsOnTypeQuery(name)) {
7166
+ if (def.runtimeClass) {
7167
+ this.aliases.defer(name, () => this.withTypeParams(def.params, () => this.resolveDef(def)));
7168
+ }
7169
+ continue;
7170
+ }
7112
7171
  this.withTypeParams(def.params, () => {
7113
7172
  this.aliases.set(name, this.resolveDef(def));
7114
7173
  });
@@ -7116,14 +7175,16 @@ var TypeAnalyzer = class {
7116
7175
  }
7117
7176
  typeQueryDependents = /* @__PURE__ */ new Map();
7118
7177
  /** Does alias `name` contain a `typeof`, itself or through an alias it
7119
- * names? */
7178
+ * names? Its parameters count: `class C<K extends Config["key"]>`
7179
+ * resolves the bound along with the class. */
7120
7180
  dependsOnTypeQuery(name, visiting = /* @__PURE__ */ new Set()) {
7121
7181
  const known = this.typeQueryDependents.get(name);
7122
7182
  if (known !== void 0) return known;
7123
7183
  const def = this.aliasDefs.get(name);
7124
7184
  if (!def || def.class || visiting.has(name)) return false;
7125
7185
  visiting.add(name);
7126
- const result = containsTypeQuery(def.node) || referencedTypeNames(def.node).some((other) => other !== name && this.dependsOnTypeQuery(other, visiting));
7186
+ const nodes = [def.node, ...def.params.flatMap((p) => [p.constraint, p.default])];
7187
+ const result = nodes.some(containsTypeQuery) || referencedTypeNames(nodes).some((other) => other !== name && this.dependsOnTypeQuery(other, visiting));
7127
7188
  visiting.delete(name);
7128
7189
  this.typeQueryDependents.set(name, result);
7129
7190
  return result;
@@ -7848,7 +7909,7 @@ var TypeAnalyzer = class {
7848
7909
  }
7849
7910
  }
7850
7911
  const mode = this.initIsAsConst(source) ? "asconst" : !isFreshLiteralExpr(source) ? "keep" : stmt.kind === "const" ? "const" : "widen";
7851
- this.bindPattern(target, inferred, env, mode);
7912
+ this.bindPattern(target, inferred, env, mode, source && expressionLabel(source));
7852
7913
  if (stmt.kind === "const") {
7853
7914
  this.correlateDestructuring(target, inferred, env);
7854
7915
  this.correlateIndexed(target, source, env);
@@ -7941,9 +8002,10 @@ var TypeAnalyzer = class {
7941
8002
  } else if (target.type === "MemberExpression" || target.type === "IndexExpression") {
7942
8003
  this.infer(target, env);
7943
8004
  this.checkReadonlyAssign(target, env);
8005
+ this.checkMemberAssign(stmt, target, vt, env);
7944
8006
  this.assignToRef(target, isFreshLiteralExpr(source) ? widen(vt) : vt, env);
7945
8007
  } else if (target.type === "ObjectPattern" || target.type === "ArrayPattern") {
7946
- this.reassignPattern(target, vt, env);
8008
+ this.reassignPattern(target, vt, env, expressionLabel(source));
7947
8009
  }
7948
8010
  return;
7949
8011
  }
@@ -9309,7 +9371,8 @@ var TypeAnalyzer = class {
9309
9371
  }
9310
9372
  /** Like `bindPattern`, but the leaves are *existing* bindings (a `{a} = t`
9311
9373
  * assignment). Updates their flow type; keeps a declared annotation. */
9312
- reassignPattern(target, valueType, env) {
9374
+ reassignPattern(target, valueType, env, label) {
9375
+ if (target.type === "ObjectPattern" || target.type === "ArrayPattern") valueType = this.destructured(target, valueType, label);
9313
9376
  switch (target.type) {
9314
9377
  case "IdentifierPattern": {
9315
9378
  const id = this.scopes.bindingOf.get(target);
@@ -9334,21 +9397,43 @@ var TypeAnalyzer = class {
9334
9397
  for (const p of target.properties) {
9335
9398
  const key = !p.computed && p.key.type === "Identifier" ? p.key.name : !p.computed && p.key.type === "StringLiteral" ? p.key.value : void 0;
9336
9399
  const pt = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
9337
- this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env);
9400
+ this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env, memberLabel(label, key));
9338
9401
  }
9339
9402
  if (target.rest) this.reassignPattern(target.rest, this.withoutKeys(valueType, target.properties), env);
9340
9403
  return;
9341
9404
  }
9342
9405
  case "ArrayPattern": {
9343
9406
  target.elements.forEach((el, i) => {
9344
- if (el) this.reassignPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env);
9407
+ if (el) this.reassignPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, memberLabel(label, i + 1));
9345
9408
  });
9346
9409
  if (target.rest) this.reassignPattern(target.rest, this.restAfter(valueType, target.elements.length), env);
9347
9410
  return;
9348
9411
  }
9349
9412
  }
9350
9413
  }
9351
- bindPattern(target, valueType, env, mode) {
9414
+ destructuredReported = /* @__PURE__ */ new WeakSet();
9415
+ /** What a pattern reads from. `const { a } = v` is `v.a`, and fails where
9416
+ * that would: on a `v` that may be nil, and on one that is `unknown`.
9417
+ * `label` is the value as it would be written — `args[1]` for the inner
9418
+ * pattern of `const [{ a }] = args`. */
9419
+ destructured(pattern, raw, label) {
9420
+ let type = raw;
9421
+ const report = (message) => {
9422
+ if (!this.emitDiagnostics || this.destructuredReported.has(pattern)) return;
9423
+ this.destructuredReported.add(pattern);
9424
+ this.diagnostics.push({ node: pattern, message: `${label === void 0 ? "Object" : `'${label}'`} ${message}` });
9425
+ };
9426
+ if (this.includesNil(type)) {
9427
+ const t = this.expand(type);
9428
+ report(t.kind === "primitive" ? "is nil" : "is possibly nil. Check it first, or give it a default");
9429
+ type = withoutNil(t);
9430
+ }
9431
+ const read = this.expand(type);
9432
+ if (read.kind === "unknown" && read.declared) report("is of type 'unknown'");
9433
+ return type;
9434
+ }
9435
+ bindPattern(target, valueType, env, mode, label) {
9436
+ if (target.type === "ObjectPattern" || target.type === "ArrayPattern") valueType = this.destructured(target, valueType, label);
9352
9437
  switch (target.type) {
9353
9438
  case "IdentifierPattern": {
9354
9439
  const id = this.bindingIdByName(target.name, target);
@@ -9369,7 +9454,7 @@ var TypeAnalyzer = class {
9369
9454
  for (const p of target.properties) {
9370
9455
  const key = !p.computed && p.key.type === "Identifier" ? p.key.name : !p.computed && p.key.type === "StringLiteral" ? p.key.value : void 0;
9371
9456
  const propType = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
9372
- this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode);
9457
+ this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode, memberLabel(label, key));
9373
9458
  }
9374
9459
  if (target.rest) this.bindPattern(target.rest, this.withoutKeys(valueType, target.properties), env, mode);
9375
9460
  return;
@@ -9377,7 +9462,7 @@ var TypeAnalyzer = class {
9377
9462
  case "ArrayPattern": {
9378
9463
  target.elements.forEach((el, i) => {
9379
9464
  if (!el) return;
9380
- this.bindPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, mode);
9465
+ this.bindPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, mode, memberLabel(label, i + 1));
9381
9466
  });
9382
9467
  if (target.rest) this.bindPattern(target.rest, this.restAfter(valueType, target.elements.length), env, mode);
9383
9468
  return;
@@ -9942,6 +10027,7 @@ var TypeAnalyzer = class {
9942
10027
  }
9943
10028
  // Broken syntax is reported by the parser; nothing more to say.
9944
10029
  case "ErrorExpression":
10030
+ if (expr.expression) this.infer(expr.expression, env);
9945
10031
  return anyType;
9946
10032
  case "Identifier": {
9947
10033
  const id = this.bindingIdOf(expr);
@@ -10005,6 +10091,14 @@ var TypeAnalyzer = class {
10005
10091
  this.checkOperands(expr, "#", arg);
10006
10092
  return numberType;
10007
10093
  }
10094
+ case "typeof": {
10095
+ const names = this.typeofNamesOf(arg);
10096
+ const tables = names && [...names].filter((name) => name === "array" || name === "object");
10097
+ if (tables?.length === 1) this.typeofTables.set(expr, tables[0]);
10098
+ else this.typeofTables.delete(expr);
10099
+ const answers = names?.size ? [...names] : [...this.typeofTypes().keys()];
10100
+ return union(answers.map((name) => literal(name)));
10101
+ }
10008
10102
  }
10009
10103
  return arg;
10010
10104
  }
@@ -10047,7 +10141,9 @@ var TypeAnalyzer = class {
10047
10141
  return booleanType;
10048
10142
  case "==":
10049
10143
  case "~=":
10050
- this.checkOverlap(expr, op, l, r);
10144
+ if (!this.checkTypeofName(expr, expr.left, expr.right) && !this.checkTypeofName(expr, expr.right, expr.left)) {
10145
+ this.checkOverlap(expr, op, l, r);
10146
+ }
10051
10147
  return booleanType;
10052
10148
  case "+":
10053
10149
  case "-":
@@ -10248,6 +10344,7 @@ var TypeAnalyzer = class {
10248
10344
  const fromMetatable = this.receiverMetatableMember(objType, expr.method.name);
10249
10345
  if (fromMetatable) this.methodSources.set(expr, fromMetatable.source);
10250
10346
  const method = fromMetatable?.type ?? this.propertyType(objType, expr.method.name);
10347
+ this.typeOf.set(expr.method, method);
10251
10348
  const united = this.unionSignatures(method);
10252
10349
  const fns = united ?? this.overloadsOf(method);
10253
10350
  const explicit = this.explicitTypeArguments(expr, fns);
@@ -10388,6 +10485,43 @@ var TypeAnalyzer = class {
10388
10485
  message: `Cannot assign to '${name}' because it is a read-only property`
10389
10486
  });
10390
10487
  }
10488
+ /** `t.x = v` against what `x` was declared as. Only a property the
10489
+ * table's type names is checked: a key it does not name is the Lua
10490
+ * table growing, which `checkMissingMember` is not about. */
10491
+ checkMemberAssign(stmt, target, value, env) {
10492
+ if (!this.emitDiagnostics) return;
10493
+ let name;
10494
+ if (target.type === "MemberExpression") {
10495
+ name = target.property.name;
10496
+ } else {
10497
+ const index = this.expand(this.infer(target.index, env));
10498
+ if (index.kind !== "literal" || typeof index.value !== "string") return;
10499
+ name = index.value;
10500
+ }
10501
+ if (target.object.type === "Identifier") {
10502
+ const id = this.bindingIdOf(target.object);
10503
+ if (id !== void 0 && this.scopes.bindings.get(id)?.declaredBy === "namespace") return;
10504
+ }
10505
+ const declared = this.declaredProperty(this.expand(this.infer(target.object, env)), name);
10506
+ if (!declared || declared.kind === "any" || declared.kind === "unknown") return;
10507
+ if (this.fitsAnnotation(stmt.value, declared, value, env)) return;
10508
+ this.diagnostics.push({
10509
+ node: stmt,
10510
+ message: `Type '${briefType(value)}' is not assignable to '${briefType(declared)}'` + this.explainMismatch(value, declared)
10511
+ });
10512
+ }
10513
+ /** The type a property was declared with, where the type names it. */
10514
+ declaredProperty(t, name) {
10515
+ if (t.kind === "object") {
10516
+ const p = t.properties.get(name);
10517
+ return p && (p.optional ? optional(p.type) : p.type);
10518
+ }
10519
+ if (t.kind === "intersection") {
10520
+ const parts = t.types.flatMap((m) => this.declaredProperty(this.expand(m), name) ?? []);
10521
+ return parts.length ? intersection(parts) : void 0;
10522
+ }
10523
+ return void 0;
10524
+ }
10391
10525
  isReadonlyProperty(t, name) {
10392
10526
  if (t.kind === "object") return t.properties.get(name)?.readonly === true;
10393
10527
  if (t.kind === "intersection" || t.kind === "union") {
@@ -10783,13 +10917,23 @@ var TypeAnalyzer = class {
10783
10917
  if (cond.type === "BinaryExpression") {
10784
10918
  const { operator: op, left, right } = cond;
10785
10919
  if (op === "and") {
10786
- this.applyNarrowing(left, env, t, forkEnv(env));
10787
- this.applyNarrowing(right, t, t, forkEnv(t));
10920
+ const before = forkEnv(env);
10921
+ const leftFailed = forkEnv(env);
10922
+ this.applyNarrowing(left, env, t, leftFailed);
10923
+ const between = forkEnv(t);
10924
+ const rightFailed = forkEnv(t);
10925
+ this.applyNarrowing(right, t, t, rightFailed);
10926
+ joinNarrowings(f, leftFailed, before, rightFailed, between);
10788
10927
  return;
10789
10928
  }
10790
10929
  if (op === "or") {
10791
- this.applyNarrowing(left, env, forkEnv(env), f);
10792
- this.applyNarrowing(right, f, forkEnv(f), f);
10930
+ const before = forkEnv(env);
10931
+ const leftHeld = forkEnv(env);
10932
+ this.applyNarrowing(left, env, leftHeld, f);
10933
+ const between = forkEnv(f);
10934
+ const rightHeld = forkEnv(f);
10935
+ this.applyNarrowing(right, f, rightHeld, f);
10936
+ joinNarrowings(t, leftHeld, before, rightHeld, between);
10793
10937
  return;
10794
10938
  }
10795
10939
  if (op === "==" || op === "~=") {
@@ -10808,11 +10952,13 @@ var TypeAnalyzer = class {
10808
10952
  this.narrowOptionalLinks(cond, env, t);
10809
10953
  }
10810
10954
  /** `a == b` / `a ~= b`. Handles, in order: a declaration-driven
10811
- * `typeof(x) == "..."` test, a literal/`nil` comparison against a
10955
+ * `typeof x == "..."` test, a literal/`nil` comparison against a
10812
10956
  * reference, and a reference-to-reference comparison. */
10813
10957
  narrowByComparison(left, right, eq, env, t, f) {
10814
10958
  const yes = eq ? t : f;
10815
10959
  const no = eq ? f : t;
10960
+ if (this.narrowByTypeof(left, right, yes, no, env)) return;
10961
+ if (this.narrowByTypeof(right, left, yes, no, env)) return;
10816
10962
  if (this.narrowByCallResult(left, right, yes, no, env)) return;
10817
10963
  if (this.narrowByCallResult(right, left, yes, no, env)) return;
10818
10964
  const litOf = (e) => {
@@ -10839,18 +10985,149 @@ var TypeAnalyzer = class {
10839
10985
  this.narrowRef(right, env, yes, no, (cur) => ({ yes: narrowTo(cur, lt), no: cur }));
10840
10986
  }
10841
10987
  }
10842
- /** Declaration-driven `typeof(x) == "number"`.
10843
- *
10844
- * `typeof` is not special-cased: it is declared in the definitions as an
10845
- * overload set whose members return *string-literal* types
10846
- * (`(value: number) -> "number"`, ...). So for `f(x) == "lit"` we keep the
10847
- * parameter types of the overloads that can return `"lit"`, and that union
10848
- * is what `x` narrows to. Any user function declared the same way gets the
10849
- * same treatment for free.
10988
+ // --------------------------------------------------------
10989
+ // typeof
10990
+ // --------------------------------------------------------
10991
+ //
10992
+ // `typeof v` answers what Luau's `typeof` does, except for a table: that
10993
+ // is an `"array"` or an `"object"`. The names of a runtime's own values —
10994
+ // Roblox's `"Instance"`, `"Vector3"` — are a library's to give: it
10995
+ // declares `type TypeofNames = { Vector3: Vector3, Instance: Instance }`,
10996
+ // each property the name `typeof` answers for a value of that type. A
10997
+ // value several of them fit is the first one's.
10998
+ typeofTypesCache;
10999
+ /** Every name `typeof` answers, and what a value it answered that for
11000
+ * is — `unknown` where nothing more is known. */
11001
+ typeofTypes() {
11002
+ if (this.typeofTypesCache) return this.typeofTypesCache;
11003
+ const types = /* @__PURE__ */ new Map([
11004
+ ["nil", nilType],
11005
+ ["boolean", booleanType],
11006
+ ["number", numberType],
11007
+ ["string", stringType],
11008
+ ["function", fn([], declaredUnknownType, declaredUnknownType)],
11009
+ ["array", arrayOf(declaredUnknownType)],
11010
+ ["object", objectType([], { key: declaredUnknownType, value: declaredUnknownType })],
11011
+ ["thread", threadType],
11012
+ ["buffer", bufferType],
11013
+ ["userdata", unknownType]
11014
+ ]);
11015
+ for (const [name, type] of this.typeofLibraryNames()) if (!types.has(name)) types.set(name, type);
11016
+ return this.typeofTypesCache = types;
11017
+ }
11018
+ typeofLibraryCache;
11019
+ typeofLibraryNames() {
11020
+ if (this.typeofLibraryCache) return this.typeofLibraryCache;
11021
+ const declared = this.aliases.get("TypeofNames");
11022
+ const names = declared && this.expand(declared);
11023
+ return this.typeofLibraryCache = names?.kind === "object" ? [...names.properties].map(([name, property]) => [name, property.type]) : [];
11024
+ }
11025
+ /** The names `typeof` can answer for a value of type `raw`, or
11026
+ * `undefined` where the type does not say. */
11027
+ typeofNamesOf(raw, depth = 0) {
11028
+ if (depth > 8) return void 0;
11029
+ const t = this.expand(raw);
11030
+ switch (t.kind) {
11031
+ case "never":
11032
+ return /* @__PURE__ */ new Set();
11033
+ case "primitive":
11034
+ return /* @__PURE__ */ new Set([t.name]);
11035
+ case "literal":
11036
+ return /* @__PURE__ */ new Set([t.base]);
11037
+ case "templateLiteral":
11038
+ return /* @__PURE__ */ new Set(["string"]);
11039
+ case "array":
11040
+ case "tuple":
11041
+ return /* @__PURE__ */ new Set(["array"]);
11042
+ case "function":
11043
+ return /* @__PURE__ */ new Set(["function"]);
11044
+ case "object": {
11045
+ const named = t.class && this.typeofLibraryNames().find(([, type]) => isAssignable(t, type));
11046
+ return /* @__PURE__ */ new Set([named ? named[0] : "object"]);
11047
+ }
11048
+ case "union": {
11049
+ const all = /* @__PURE__ */ new Set();
11050
+ for (const member of t.types) {
11051
+ const names = this.typeofNamesOf(member, depth + 1);
11052
+ if (!names) return void 0;
11053
+ for (const name of names) all.add(name);
11054
+ }
11055
+ return all;
11056
+ }
11057
+ case "intersection": {
11058
+ const known = t.types.map((part) => this.typeofNamesOf(part, depth + 1)).filter((names) => names !== void 0);
11059
+ if (!known.length) return void 0;
11060
+ const common = [...known[0]].filter((name) => known.every((names) => names.has(name)));
11061
+ return new Set(common.length ? common : known[0]);
11062
+ }
11063
+ case "typeParam":
11064
+ return t.constraint ? this.typeofNamesOf(t.constraint, depth + 1) : void 0;
11065
+ case "difference":
11066
+ return this.typeofNamesOf(t.base, depth + 1);
11067
+ default:
11068
+ return void 0;
11069
+ }
11070
+ }
11071
+ /** `typeof v == "table"`: a name `typeof` never answers. Returns true if
11072
+ * the comparison needs no further checking. */
11073
+ checkTypeofName(node, typeofSide, nameSide) {
11074
+ const test = unwrapParens(typeofSide);
11075
+ if (test.type !== "UnaryExpression" || test.operator !== "typeof") return false;
11076
+ const name = this.asLiteral(unwrapParens(nameSide));
11077
+ if (typeof name !== "string") return false;
11078
+ if (name === "nil") return true;
11079
+ if (this.typeofTypes().has(name)) return false;
11080
+ if (this.emitDiagnostics) {
11081
+ this.diagnostics.push({
11082
+ node,
11083
+ message: name === "table" ? `'typeof' never answers "table": a table is an "array" or an "object"` : `'typeof' never answers "${name}"`
11084
+ });
11085
+ }
11086
+ return true;
11087
+ }
11088
+ /** `typeof x == "number"`: `x` is what answers that name where the test
11089
+ * holds, and what does not where it fails. Returns true if `testSide` is
11090
+ * a `typeof` compared with a name. */
11091
+ narrowByTypeof(testSide, nameSide, yes, no, env) {
11092
+ const test = unwrapParens(testSide);
11093
+ if (test.type !== "UnaryExpression" || test.operator !== "typeof") return false;
11094
+ const name = this.asLiteral(unwrapParens(nameSide));
11095
+ if (typeof name !== "string") return false;
11096
+ const answered = this.typeofTypes().get(name);
11097
+ if (!answered) return true;
11098
+ const opaque = answered.kind !== "unknown";
11099
+ const subtracts = opaque && name !== "array" && name !== "object";
11100
+ const members = (cur) => {
11101
+ const t = this.expand(cur);
11102
+ return t.kind === "union" ? t.types.flatMap(members) : [t];
11103
+ };
11104
+ if (this.refKeyOf(test.argument) !== void 0) {
11105
+ this.narrowRef(test.argument, env, yes, no, (cur) => {
11106
+ const kept = [];
11107
+ const dropped = [];
11108
+ for (const member of members(cur)) {
11109
+ const names = this.typeofNamesOf(member);
11110
+ if (!names) {
11111
+ kept.push(opaque ? narrowTo(member, answered) : member);
11112
+ dropped.push(subtracts ? narrowExclude(member, answered) : member);
11113
+ continue;
11114
+ }
11115
+ if (names.has(name)) kept.push(member);
11116
+ if (!(names.size === 1 && names.has(name))) dropped.push(member);
11117
+ }
11118
+ return { yes: union(kept), no: union(dropped) };
11119
+ });
11120
+ }
11121
+ this.narrowOptionalLinks(test.argument, env, name === "nil" ? no : yes);
11122
+ return true;
11123
+ }
11124
+ /** Declaration-driven `kind(x) == "number"`.
10850
11125
  *
10851
- * The analyzer knows nothing about the names `type` and `typeof`: with no
10852
- * definitions loaded there are no literal-returning overloads and this
10853
- * narrows nothing. Returns true if it handled the comparison. */
11126
+ * A function declared as an overload set whose members return
11127
+ * *string-literal* types (`(value: number) => "number"`, ...) says by its
11128
+ * answer what it was given. So for `f(x) == "lit"` we keep the parameter
11129
+ * types of the overloads that can return `"lit"`, and that union is what
11130
+ * `x` narrows to. Returns true if it handled the comparison. */
10854
11131
  narrowByCallResult(callSide, litSide, yes, no, env) {
10855
11132
  if (callSide.type !== "CallExpression" || callSide.arguments.length !== 1) return false;
10856
11133
  const name = this.asLiteral(litSide);