@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/dist/index.js CHANGED
@@ -596,6 +596,8 @@ var UNUSED_EXPECT_ERROR = "Unused '@tilua-expect-error' directive";
596
596
 
597
597
  // src/ast/builders.ts
598
598
  var NO_DOTS = "tilua has no bare '...': take a rest parameter, '...args: T[]', or 'scriptArgs' for the script's own";
599
+ var NO_MEMBER_NAME = "Expected a name after '.'";
600
+ var NO_TYPEOF_CALL = "'typeof' is an operator, not a function: write 'typeof v'";
599
601
  var ACCESSIBILITIES = /* @__PURE__ */ new Set(["public", "private", "protected"]);
600
602
  var CLASS_MODIFIERS = /* @__PURE__ */ new Set([...ACCESSIBILITIES, "static", "abstract", "override", "readonly"]);
601
603
  function bindThis(func, at) {
@@ -2133,6 +2135,7 @@ var Parser = class {
2133
2135
  * `:` Identifier followed by one of Lua's call forms. */
2134
2136
  startsMethodCall(offset = 0) {
2135
2137
  if (this.peek(offset + 1).type !== "Identifier") return false;
2138
+ if (this.startsOwnLine(this.peek(offset), this.peek(offset + 1))) return false;
2136
2139
  const after = this.peek(offset + 2);
2137
2140
  if (after.type === "Punctuator") {
2138
2141
  const v = String(after.value);
@@ -2158,6 +2161,23 @@ var Parser = class {
2158
2161
  const isString = t.type === "InterpolatedString" || t.type === "Literal" && t.kind === "string";
2159
2162
  return isString && this.punctuatorAt(1, ":") && this.startsMethodCall(1);
2160
2163
  }
2164
+ /** Is there no member name at `offset`, after the `.` or `:` before it? */
2165
+ namelessAfter(offset) {
2166
+ const name = this.peek(offset);
2167
+ if (name.type !== "Identifier") return true;
2168
+ const before = offset === 0 ? this.previous() : this.peek(offset - 1);
2169
+ return before !== void 0 && this.startsOwnLine(before, name);
2170
+ }
2171
+ /** Recovery only. `obj.` may go on to its name on a later line, as in
2172
+ * JavaScript — but while `obj.` is still being typed, the line below is
2173
+ * whatever statement was already there, and reading it as the name turns
2174
+ * one unfinished line into errors about the next. So, as a missing `}`
2175
+ * is placed, the indentation says which: a line that carries the access
2176
+ * on is indented deeper than the line it carries on from. */
2177
+ startsOwnLine(before, name) {
2178
+ if (!this.recover || name.line.start <= before.line.end) return false;
2179
+ return this.indentOf(name) <= this.indentOf(before);
2180
+ }
2161
2181
  /** Does the next token start right where the current one ends? */
2162
2182
  touchesNext() {
2163
2183
  const current = this.current();
@@ -2199,6 +2219,7 @@ var Parser = class {
2199
2219
  const t = this.current();
2200
2220
  if (t.type === "Keyword" && t.value === "not") return "not";
2201
2221
  if (t.type === "Operator" && (t.value === "-" || t.value === "#")) return t.value;
2222
+ if (t.type === "Identifier" && t.value === "typeof") return "typeof";
2202
2223
  return null;
2203
2224
  }
2204
2225
  /** Runs `fn` inside a bracket, where a `:` can only ever be a method
@@ -2298,6 +2319,10 @@ var Parser = class {
2298
2319
  const op = this.isUnaryOperator();
2299
2320
  if (op) {
2300
2321
  const opTok = this.advance();
2322
+ const next = this.current();
2323
+ if (op === "typeof" && this.checkPunctuator("(") && next.line.start === opTok.line.end && next.column.start === opTok.column.end) {
2324
+ this.problemAt(next, NO_TYPEOF_CALL);
2325
+ }
2301
2326
  const argument = this.parseExpression(UNARY_PRECEDENCE);
2302
2327
  return {
2303
2328
  type: "UnaryExpression",
@@ -2467,6 +2492,13 @@ var Parser = class {
2467
2492
  if (this.checkPunctuator("?") && this.touchesNext()) {
2468
2493
  const next = this.peek(1);
2469
2494
  const punct = next.type === "Punctuator" ? String(next.value) : void 0;
2495
+ if (punct === "." && this.recover && this.namelessAfter(2) && !this.punctuatorAt(2, "[") && !this.punctuatorAt(2, "(")) {
2496
+ this.advance();
2497
+ this.advance();
2498
+ this.problemAt(this.previous(), NO_MEMBER_NAME);
2499
+ base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
2500
+ break;
2501
+ }
2470
2502
  if (punct === "." && this.peek(2).type === "Identifier") {
2471
2503
  this.advance();
2472
2504
  this.advance();
@@ -2521,9 +2553,9 @@ var Parser = class {
2521
2553
  }
2522
2554
  }
2523
2555
  if (this.matchPunctuator(".")) {
2524
- if (this.recover && !this.checkType("Identifier")) {
2525
- this.softError("Expected identifier");
2526
- base = { type: "ErrorExpression", ...spanFrom(base, this.previous()) };
2556
+ if (this.namelessAfter(0)) {
2557
+ this.problemAt(this.previous(), NO_MEMBER_NAME);
2558
+ base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
2527
2559
  break;
2528
2560
  }
2529
2561
  const prop = this.parseIdentifier();
@@ -2536,6 +2568,12 @@ var Parser = class {
2536
2568
  base = { type: "IndexExpression", object: base, index, ...spanFrom(base, this.previous()) };
2537
2569
  continue;
2538
2570
  }
2571
+ if (this.recover && !this.ternaryColons && this.checkPunctuator(":") && this.peek(1).line.start > this.current().line.end && !this.startsMethodCall()) {
2572
+ this.advance();
2573
+ this.problemAt(this.previous(), "Expected a name after ':'");
2574
+ base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
2575
+ break;
2576
+ }
2539
2577
  if (this.checkPunctuator(":") && this.startsMethodCall() && this.takeMethodColon()) {
2540
2578
  this.advance();
2541
2579
  const method = this.parseIdentifier();
@@ -3379,6 +3417,7 @@ var Parser = class {
3379
3417
  return { type: "TupleTypeNode", elements, ...rest ? { rest } : {}, ...spanFrom(t, this.previous()) };
3380
3418
  }
3381
3419
  if (t.type === "Identifier" && t.value === "typeof" && this.peek(1).type === "Punctuator" && this.peek(1).value === "(") {
3420
+ this.problemAt(this.peek(1), NO_TYPEOF_CALL);
3382
3421
  this.advance();
3383
3422
  this.advance();
3384
3423
  const expression = this.parseExpression();
@@ -3759,7 +3798,7 @@ var BinaryOperators = [
3759
3798
  "and",
3760
3799
  "or"
3761
3800
  ];
3762
- var UnaryOperators = ["-", "not", "#"];
3801
+ var UnaryOperators = ["-", "not", "#", "typeof"];
3763
3802
 
3764
3803
  // src/ast/analyzeScopes.ts
3765
3804
  function getBinding(analysis, id) {
@@ -4386,7 +4425,9 @@ var Analyzer = class {
4386
4425
  case "BooleanLiteral":
4387
4426
  case "NumberLiteral":
4388
4427
  case "StringLiteral":
4428
+ return;
4389
4429
  case "ErrorExpression":
4430
+ if (expr.expression) this.visitExpression(expr.expression, scope);
4390
4431
  return;
4391
4432
  case "InterpolatedStringExpression":
4392
4433
  for (const part of expr.parts) {
@@ -5692,6 +5733,13 @@ function mergeEnv(a, b, base) {
5692
5733
  }
5693
5734
  return out;
5694
5735
  }
5736
+ function joinNarrowings(into, a, aFrom, b, bFrom) {
5737
+ for (const [key, left] of a) {
5738
+ const right = b.get(key);
5739
+ if (right === void 0 || left === aFrom.get(key) || right === bFrom.get(key)) continue;
5740
+ into.set(key, union([left, right]));
5741
+ }
5742
+ }
5695
5743
  function mergeIndexer(a, b) {
5696
5744
  return a ? { key: union([a.key, b.key]), value: union([a.value, b.value]) } : b;
5697
5745
  }
@@ -5971,6 +6019,10 @@ function unwrapParens(e) {
5971
6019
  while (e.type === "ParenthesizedExpression") e = e.expression;
5972
6020
  return e;
5973
6021
  }
6022
+ function memberLabel(label, key) {
6023
+ if (label === void 0 || key === void 0) return void 0;
6024
+ return typeof key === "number" ? `${label}[${key}]` : `${label}.${key}`;
6025
+ }
5974
6026
  function expressionLabel(e, depth = 0) {
5975
6027
  if (depth > 6) return void 0;
5976
6028
  const args = (list) => {
@@ -6042,6 +6094,7 @@ var TypeAnalyzer = class {
6042
6094
  metatables = [];
6043
6095
  metatableCache = /* @__PURE__ */ new WeakMap();
6044
6096
  methodSources = /* @__PURE__ */ new Map();
6097
+ typeofTables = /* @__PURE__ */ new Map();
6045
6098
  /** Public: each alias resolved once (generic aliases keep their params as
6046
6099
  * `typeParam` nodes in the body). */
6047
6100
  aliases = new LazyMap();
@@ -6120,7 +6173,7 @@ var TypeAnalyzer = class {
6120
6173
  const given = this.options.globalTypes?.[name];
6121
6174
  if (given) this.bindingType.set(id, given);
6122
6175
  else if (this.libGlobalTypes.has(name)) this.bindingType.defer(id, () => this.libGlobalTypes.get(name) ?? anyType);
6123
- else if (LANGUAGE_GLOBALS.includes(name)) this.bindingType.set(id, arrayOf(unknownType));
6176
+ else if (LANGUAGE_GLOBALS.includes(name)) this.bindingType.set(id, arrayOf(declaredUnknownType));
6124
6177
  else this.bindingType.set(id, anyType);
6125
6178
  }
6126
6179
  setAliasExpander((t) => this.expand(t));
@@ -6143,6 +6196,7 @@ var TypeAnalyzer = class {
6143
6196
  aliases: this.resolveDeferredAliases(),
6144
6197
  loops: this.loops,
6145
6198
  methodSources: this.methodSources,
6199
+ typeofTables: this.typeofTables,
6146
6200
  metatableMembers: (type) => this.metatableMembers(type),
6147
6201
  diagnostics: this.diagnostics
6148
6202
  };
@@ -6993,7 +7047,12 @@ var TypeAnalyzer = class {
6993
7047
  this.aliases.defer(name, () => this.classType(cls));
6994
7048
  continue;
6995
7049
  }
6996
- if (this.dependsOnTypeQuery(name)) continue;
7050
+ if (this.dependsOnTypeQuery(name)) {
7051
+ if (def.runtimeClass) {
7052
+ this.aliases.defer(name, () => this.withTypeParams(def.params, () => this.resolveDef(def)));
7053
+ }
7054
+ continue;
7055
+ }
6997
7056
  this.withTypeParams(def.params, () => {
6998
7057
  this.aliases.set(name, this.resolveDef(def));
6999
7058
  });
@@ -7001,14 +7060,16 @@ var TypeAnalyzer = class {
7001
7060
  }
7002
7061
  typeQueryDependents = /* @__PURE__ */ new Map();
7003
7062
  /** Does alias `name` contain a `typeof`, itself or through an alias it
7004
- * names? */
7063
+ * names? Its parameters count: `class C<K extends Config["key"]>`
7064
+ * resolves the bound along with the class. */
7005
7065
  dependsOnTypeQuery(name, visiting = /* @__PURE__ */ new Set()) {
7006
7066
  const known = this.typeQueryDependents.get(name);
7007
7067
  if (known !== void 0) return known;
7008
7068
  const def = this.aliasDefs.get(name);
7009
7069
  if (!def || def.class || visiting.has(name)) return false;
7010
7070
  visiting.add(name);
7011
- const result = containsTypeQuery(def.node) || referencedTypeNames(def.node).some((other) => other !== name && this.dependsOnTypeQuery(other, visiting));
7071
+ const nodes = [def.node, ...def.params.flatMap((p) => [p.constraint, p.default])];
7072
+ const result = nodes.some(containsTypeQuery) || referencedTypeNames(nodes).some((other) => other !== name && this.dependsOnTypeQuery(other, visiting));
7012
7073
  visiting.delete(name);
7013
7074
  this.typeQueryDependents.set(name, result);
7014
7075
  return result;
@@ -7733,7 +7794,7 @@ var TypeAnalyzer = class {
7733
7794
  }
7734
7795
  }
7735
7796
  const mode = this.initIsAsConst(source) ? "asconst" : !isFreshLiteralExpr(source) ? "keep" : stmt.kind === "const" ? "const" : "widen";
7736
- this.bindPattern(target, inferred, env, mode);
7797
+ this.bindPattern(target, inferred, env, mode, source && expressionLabel(source));
7737
7798
  if (stmt.kind === "const") {
7738
7799
  this.correlateDestructuring(target, inferred, env);
7739
7800
  this.correlateIndexed(target, source, env);
@@ -7826,9 +7887,10 @@ var TypeAnalyzer = class {
7826
7887
  } else if (target.type === "MemberExpression" || target.type === "IndexExpression") {
7827
7888
  this.infer(target, env);
7828
7889
  this.checkReadonlyAssign(target, env);
7890
+ this.checkMemberAssign(stmt, target, vt, env);
7829
7891
  this.assignToRef(target, isFreshLiteralExpr(source) ? widen(vt) : vt, env);
7830
7892
  } else if (target.type === "ObjectPattern" || target.type === "ArrayPattern") {
7831
- this.reassignPattern(target, vt, env);
7893
+ this.reassignPattern(target, vt, env, expressionLabel(source));
7832
7894
  }
7833
7895
  return;
7834
7896
  }
@@ -9194,7 +9256,8 @@ var TypeAnalyzer = class {
9194
9256
  }
9195
9257
  /** Like `bindPattern`, but the leaves are *existing* bindings (a `{a} = t`
9196
9258
  * assignment). Updates their flow type; keeps a declared annotation. */
9197
- reassignPattern(target, valueType, env) {
9259
+ reassignPattern(target, valueType, env, label) {
9260
+ if (target.type === "ObjectPattern" || target.type === "ArrayPattern") valueType = this.destructured(target, valueType, label);
9198
9261
  switch (target.type) {
9199
9262
  case "IdentifierPattern": {
9200
9263
  const id = this.scopes.bindingOf.get(target);
@@ -9219,21 +9282,43 @@ var TypeAnalyzer = class {
9219
9282
  for (const p of target.properties) {
9220
9283
  const key = !p.computed && p.key.type === "Identifier" ? p.key.name : !p.computed && p.key.type === "StringLiteral" ? p.key.value : void 0;
9221
9284
  const pt = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
9222
- this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env);
9285
+ this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env, memberLabel(label, key));
9223
9286
  }
9224
9287
  if (target.rest) this.reassignPattern(target.rest, this.withoutKeys(valueType, target.properties), env);
9225
9288
  return;
9226
9289
  }
9227
9290
  case "ArrayPattern": {
9228
9291
  target.elements.forEach((el, i) => {
9229
- if (el) this.reassignPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env);
9292
+ if (el) this.reassignPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, memberLabel(label, i + 1));
9230
9293
  });
9231
9294
  if (target.rest) this.reassignPattern(target.rest, this.restAfter(valueType, target.elements.length), env);
9232
9295
  return;
9233
9296
  }
9234
9297
  }
9235
9298
  }
9236
- bindPattern(target, valueType, env, mode) {
9299
+ destructuredReported = /* @__PURE__ */ new WeakSet();
9300
+ /** What a pattern reads from. `const { a } = v` is `v.a`, and fails where
9301
+ * that would: on a `v` that may be nil, and on one that is `unknown`.
9302
+ * `label` is the value as it would be written — `args[1]` for the inner
9303
+ * pattern of `const [{ a }] = args`. */
9304
+ destructured(pattern, raw, label) {
9305
+ let type = raw;
9306
+ const report = (message) => {
9307
+ if (!this.emitDiagnostics || this.destructuredReported.has(pattern)) return;
9308
+ this.destructuredReported.add(pattern);
9309
+ this.diagnostics.push({ node: pattern, message: `${label === void 0 ? "Object" : `'${label}'`} ${message}` });
9310
+ };
9311
+ if (this.includesNil(type)) {
9312
+ const t = this.expand(type);
9313
+ report(t.kind === "primitive" ? "is nil" : "is possibly nil. Check it first, or give it a default");
9314
+ type = withoutNil(t);
9315
+ }
9316
+ const read = this.expand(type);
9317
+ if (read.kind === "unknown" && read.declared) report("is of type 'unknown'");
9318
+ return type;
9319
+ }
9320
+ bindPattern(target, valueType, env, mode, label) {
9321
+ if (target.type === "ObjectPattern" || target.type === "ArrayPattern") valueType = this.destructured(target, valueType, label);
9237
9322
  switch (target.type) {
9238
9323
  case "IdentifierPattern": {
9239
9324
  const id = this.bindingIdByName(target.name, target);
@@ -9254,7 +9339,7 @@ var TypeAnalyzer = class {
9254
9339
  for (const p of target.properties) {
9255
9340
  const key = !p.computed && p.key.type === "Identifier" ? p.key.name : !p.computed && p.key.type === "StringLiteral" ? p.key.value : void 0;
9256
9341
  const propType = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
9257
- this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode);
9342
+ this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode, memberLabel(label, key));
9258
9343
  }
9259
9344
  if (target.rest) this.bindPattern(target.rest, this.withoutKeys(valueType, target.properties), env, mode);
9260
9345
  return;
@@ -9262,7 +9347,7 @@ var TypeAnalyzer = class {
9262
9347
  case "ArrayPattern": {
9263
9348
  target.elements.forEach((el, i) => {
9264
9349
  if (!el) return;
9265
- this.bindPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, mode);
9350
+ this.bindPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, mode, memberLabel(label, i + 1));
9266
9351
  });
9267
9352
  if (target.rest) this.bindPattern(target.rest, this.restAfter(valueType, target.elements.length), env, mode);
9268
9353
  return;
@@ -9827,6 +9912,7 @@ var TypeAnalyzer = class {
9827
9912
  }
9828
9913
  // Broken syntax is reported by the parser; nothing more to say.
9829
9914
  case "ErrorExpression":
9915
+ if (expr.expression) this.infer(expr.expression, env);
9830
9916
  return anyType;
9831
9917
  case "Identifier": {
9832
9918
  const id = this.bindingIdOf(expr);
@@ -9890,6 +9976,14 @@ var TypeAnalyzer = class {
9890
9976
  this.checkOperands(expr, "#", arg);
9891
9977
  return numberType;
9892
9978
  }
9979
+ case "typeof": {
9980
+ const names = this.typeofNamesOf(arg);
9981
+ const tables = names && [...names].filter((name) => name === "array" || name === "object");
9982
+ if (tables?.length === 1) this.typeofTables.set(expr, tables[0]);
9983
+ else this.typeofTables.delete(expr);
9984
+ const answers = names?.size ? [...names] : [...this.typeofTypes().keys()];
9985
+ return union(answers.map((name) => literal(name)));
9986
+ }
9893
9987
  }
9894
9988
  return arg;
9895
9989
  }
@@ -9932,7 +10026,9 @@ var TypeAnalyzer = class {
9932
10026
  return booleanType;
9933
10027
  case "==":
9934
10028
  case "~=":
9935
- this.checkOverlap(expr, op, l, r);
10029
+ if (!this.checkTypeofName(expr, expr.left, expr.right) && !this.checkTypeofName(expr, expr.right, expr.left)) {
10030
+ this.checkOverlap(expr, op, l, r);
10031
+ }
9936
10032
  return booleanType;
9937
10033
  case "+":
9938
10034
  case "-":
@@ -10133,6 +10229,7 @@ var TypeAnalyzer = class {
10133
10229
  const fromMetatable = this.receiverMetatableMember(objType, expr.method.name);
10134
10230
  if (fromMetatable) this.methodSources.set(expr, fromMetatable.source);
10135
10231
  const method = fromMetatable?.type ?? this.propertyType(objType, expr.method.name);
10232
+ this.typeOf.set(expr.method, method);
10136
10233
  const united = this.unionSignatures(method);
10137
10234
  const fns = united ?? this.overloadsOf(method);
10138
10235
  const explicit = this.explicitTypeArguments(expr, fns);
@@ -10273,6 +10370,43 @@ var TypeAnalyzer = class {
10273
10370
  message: `Cannot assign to '${name}' because it is a read-only property`
10274
10371
  });
10275
10372
  }
10373
+ /** `t.x = v` against what `x` was declared as. Only a property the
10374
+ * table's type names is checked: a key it does not name is the Lua
10375
+ * table growing, which `checkMissingMember` is not about. */
10376
+ checkMemberAssign(stmt, target, value, env) {
10377
+ if (!this.emitDiagnostics) return;
10378
+ let name;
10379
+ if (target.type === "MemberExpression") {
10380
+ name = target.property.name;
10381
+ } else {
10382
+ const index = this.expand(this.infer(target.index, env));
10383
+ if (index.kind !== "literal" || typeof index.value !== "string") return;
10384
+ name = index.value;
10385
+ }
10386
+ if (target.object.type === "Identifier") {
10387
+ const id = this.bindingIdOf(target.object);
10388
+ if (id !== void 0 && this.scopes.bindings.get(id)?.declaredBy === "namespace") return;
10389
+ }
10390
+ const declared = this.declaredProperty(this.expand(this.infer(target.object, env)), name);
10391
+ if (!declared || declared.kind === "any" || declared.kind === "unknown") return;
10392
+ if (this.fitsAnnotation(stmt.value, declared, value, env)) return;
10393
+ this.diagnostics.push({
10394
+ node: stmt,
10395
+ message: `Type '${briefType(value)}' is not assignable to '${briefType(declared)}'` + this.explainMismatch(value, declared)
10396
+ });
10397
+ }
10398
+ /** The type a property was declared with, where the type names it. */
10399
+ declaredProperty(t, name) {
10400
+ if (t.kind === "object") {
10401
+ const p = t.properties.get(name);
10402
+ return p && (p.optional ? optional(p.type) : p.type);
10403
+ }
10404
+ if (t.kind === "intersection") {
10405
+ const parts = t.types.flatMap((m) => this.declaredProperty(this.expand(m), name) ?? []);
10406
+ return parts.length ? intersection(parts) : void 0;
10407
+ }
10408
+ return void 0;
10409
+ }
10276
10410
  isReadonlyProperty(t, name) {
10277
10411
  if (t.kind === "object") return t.properties.get(name)?.readonly === true;
10278
10412
  if (t.kind === "intersection" || t.kind === "union") {
@@ -10668,13 +10802,23 @@ var TypeAnalyzer = class {
10668
10802
  if (cond.type === "BinaryExpression") {
10669
10803
  const { operator: op, left, right } = cond;
10670
10804
  if (op === "and") {
10671
- this.applyNarrowing(left, env, t, forkEnv(env));
10672
- this.applyNarrowing(right, t, t, forkEnv(t));
10805
+ const before = forkEnv(env);
10806
+ const leftFailed = forkEnv(env);
10807
+ this.applyNarrowing(left, env, t, leftFailed);
10808
+ const between = forkEnv(t);
10809
+ const rightFailed = forkEnv(t);
10810
+ this.applyNarrowing(right, t, t, rightFailed);
10811
+ joinNarrowings(f, leftFailed, before, rightFailed, between);
10673
10812
  return;
10674
10813
  }
10675
10814
  if (op === "or") {
10676
- this.applyNarrowing(left, env, forkEnv(env), f);
10677
- this.applyNarrowing(right, f, forkEnv(f), f);
10815
+ const before = forkEnv(env);
10816
+ const leftHeld = forkEnv(env);
10817
+ this.applyNarrowing(left, env, leftHeld, f);
10818
+ const between = forkEnv(f);
10819
+ const rightHeld = forkEnv(f);
10820
+ this.applyNarrowing(right, f, rightHeld, f);
10821
+ joinNarrowings(t, leftHeld, before, rightHeld, between);
10678
10822
  return;
10679
10823
  }
10680
10824
  if (op === "==" || op === "~=") {
@@ -10693,11 +10837,13 @@ var TypeAnalyzer = class {
10693
10837
  this.narrowOptionalLinks(cond, env, t);
10694
10838
  }
10695
10839
  /** `a == b` / `a ~= b`. Handles, in order: a declaration-driven
10696
- * `typeof(x) == "..."` test, a literal/`nil` comparison against a
10840
+ * `typeof x == "..."` test, a literal/`nil` comparison against a
10697
10841
  * reference, and a reference-to-reference comparison. */
10698
10842
  narrowByComparison(left, right, eq, env, t, f) {
10699
10843
  const yes = eq ? t : f;
10700
10844
  const no = eq ? f : t;
10845
+ if (this.narrowByTypeof(left, right, yes, no, env)) return;
10846
+ if (this.narrowByTypeof(right, left, yes, no, env)) return;
10701
10847
  if (this.narrowByCallResult(left, right, yes, no, env)) return;
10702
10848
  if (this.narrowByCallResult(right, left, yes, no, env)) return;
10703
10849
  const litOf = (e) => {
@@ -10724,18 +10870,149 @@ var TypeAnalyzer = class {
10724
10870
  this.narrowRef(right, env, yes, no, (cur) => ({ yes: narrowTo(cur, lt), no: cur }));
10725
10871
  }
10726
10872
  }
10727
- /** Declaration-driven `typeof(x) == "number"`.
10728
- *
10729
- * `typeof` is not special-cased: it is declared in the definitions as an
10730
- * overload set whose members return *string-literal* types
10731
- * (`(value: number) -> "number"`, ...). So for `f(x) == "lit"` we keep the
10732
- * parameter types of the overloads that can return `"lit"`, and that union
10733
- * is what `x` narrows to. Any user function declared the same way gets the
10734
- * same treatment for free.
10873
+ // --------------------------------------------------------
10874
+ // typeof
10875
+ // --------------------------------------------------------
10876
+ //
10877
+ // `typeof v` answers what Luau's `typeof` does, except for a table: that
10878
+ // is an `"array"` or an `"object"`. The names of a runtime's own values —
10879
+ // Roblox's `"Instance"`, `"Vector3"` — are a library's to give: it
10880
+ // declares `type TypeofNames = { Vector3: Vector3, Instance: Instance }`,
10881
+ // each property the name `typeof` answers for a value of that type. A
10882
+ // value several of them fit is the first one's.
10883
+ typeofTypesCache;
10884
+ /** Every name `typeof` answers, and what a value it answered that for
10885
+ * is — `unknown` where nothing more is known. */
10886
+ typeofTypes() {
10887
+ if (this.typeofTypesCache) return this.typeofTypesCache;
10888
+ const types = /* @__PURE__ */ new Map([
10889
+ ["nil", nilType],
10890
+ ["boolean", booleanType],
10891
+ ["number", numberType],
10892
+ ["string", stringType],
10893
+ ["function", fn([], declaredUnknownType, declaredUnknownType)],
10894
+ ["array", arrayOf(declaredUnknownType)],
10895
+ ["object", objectType([], { key: declaredUnknownType, value: declaredUnknownType })],
10896
+ ["thread", threadType],
10897
+ ["buffer", bufferType],
10898
+ ["userdata", unknownType]
10899
+ ]);
10900
+ for (const [name, type] of this.typeofLibraryNames()) if (!types.has(name)) types.set(name, type);
10901
+ return this.typeofTypesCache = types;
10902
+ }
10903
+ typeofLibraryCache;
10904
+ typeofLibraryNames() {
10905
+ if (this.typeofLibraryCache) return this.typeofLibraryCache;
10906
+ const declared = this.aliases.get("TypeofNames");
10907
+ const names = declared && this.expand(declared);
10908
+ return this.typeofLibraryCache = names?.kind === "object" ? [...names.properties].map(([name, property]) => [name, property.type]) : [];
10909
+ }
10910
+ /** The names `typeof` can answer for a value of type `raw`, or
10911
+ * `undefined` where the type does not say. */
10912
+ typeofNamesOf(raw, depth = 0) {
10913
+ if (depth > 8) return void 0;
10914
+ const t = this.expand(raw);
10915
+ switch (t.kind) {
10916
+ case "never":
10917
+ return /* @__PURE__ */ new Set();
10918
+ case "primitive":
10919
+ return /* @__PURE__ */ new Set([t.name]);
10920
+ case "literal":
10921
+ return /* @__PURE__ */ new Set([t.base]);
10922
+ case "templateLiteral":
10923
+ return /* @__PURE__ */ new Set(["string"]);
10924
+ case "array":
10925
+ case "tuple":
10926
+ return /* @__PURE__ */ new Set(["array"]);
10927
+ case "function":
10928
+ return /* @__PURE__ */ new Set(["function"]);
10929
+ case "object": {
10930
+ const named = t.class && this.typeofLibraryNames().find(([, type]) => isAssignable(t, type));
10931
+ return /* @__PURE__ */ new Set([named ? named[0] : "object"]);
10932
+ }
10933
+ case "union": {
10934
+ const all = /* @__PURE__ */ new Set();
10935
+ for (const member of t.types) {
10936
+ const names = this.typeofNamesOf(member, depth + 1);
10937
+ if (!names) return void 0;
10938
+ for (const name of names) all.add(name);
10939
+ }
10940
+ return all;
10941
+ }
10942
+ case "intersection": {
10943
+ const known = t.types.map((part) => this.typeofNamesOf(part, depth + 1)).filter((names) => names !== void 0);
10944
+ if (!known.length) return void 0;
10945
+ const common = [...known[0]].filter((name) => known.every((names) => names.has(name)));
10946
+ return new Set(common.length ? common : known[0]);
10947
+ }
10948
+ case "typeParam":
10949
+ return t.constraint ? this.typeofNamesOf(t.constraint, depth + 1) : void 0;
10950
+ case "difference":
10951
+ return this.typeofNamesOf(t.base, depth + 1);
10952
+ default:
10953
+ return void 0;
10954
+ }
10955
+ }
10956
+ /** `typeof v == "table"`: a name `typeof` never answers. Returns true if
10957
+ * the comparison needs no further checking. */
10958
+ checkTypeofName(node, typeofSide, nameSide) {
10959
+ const test = unwrapParens(typeofSide);
10960
+ if (test.type !== "UnaryExpression" || test.operator !== "typeof") return false;
10961
+ const name = this.asLiteral(unwrapParens(nameSide));
10962
+ if (typeof name !== "string") return false;
10963
+ if (name === "nil") return true;
10964
+ if (this.typeofTypes().has(name)) return false;
10965
+ if (this.emitDiagnostics) {
10966
+ this.diagnostics.push({
10967
+ node,
10968
+ message: name === "table" ? `'typeof' never answers "table": a table is an "array" or an "object"` : `'typeof' never answers "${name}"`
10969
+ });
10970
+ }
10971
+ return true;
10972
+ }
10973
+ /** `typeof x == "number"`: `x` is what answers that name where the test
10974
+ * holds, and what does not where it fails. Returns true if `testSide` is
10975
+ * a `typeof` compared with a name. */
10976
+ narrowByTypeof(testSide, nameSide, yes, no, env) {
10977
+ const test = unwrapParens(testSide);
10978
+ if (test.type !== "UnaryExpression" || test.operator !== "typeof") return false;
10979
+ const name = this.asLiteral(unwrapParens(nameSide));
10980
+ if (typeof name !== "string") return false;
10981
+ const answered = this.typeofTypes().get(name);
10982
+ if (!answered) return true;
10983
+ const opaque = answered.kind !== "unknown";
10984
+ const subtracts = opaque && name !== "array" && name !== "object";
10985
+ const members = (cur) => {
10986
+ const t = this.expand(cur);
10987
+ return t.kind === "union" ? t.types.flatMap(members) : [t];
10988
+ };
10989
+ if (this.refKeyOf(test.argument) !== void 0) {
10990
+ this.narrowRef(test.argument, env, yes, no, (cur) => {
10991
+ const kept = [];
10992
+ const dropped = [];
10993
+ for (const member of members(cur)) {
10994
+ const names = this.typeofNamesOf(member);
10995
+ if (!names) {
10996
+ kept.push(opaque ? narrowTo(member, answered) : member);
10997
+ dropped.push(subtracts ? narrowExclude(member, answered) : member);
10998
+ continue;
10999
+ }
11000
+ if (names.has(name)) kept.push(member);
11001
+ if (!(names.size === 1 && names.has(name))) dropped.push(member);
11002
+ }
11003
+ return { yes: union(kept), no: union(dropped) };
11004
+ });
11005
+ }
11006
+ this.narrowOptionalLinks(test.argument, env, name === "nil" ? no : yes);
11007
+ return true;
11008
+ }
11009
+ /** Declaration-driven `kind(x) == "number"`.
10735
11010
  *
10736
- * The analyzer knows nothing about the names `type` and `typeof`: with no
10737
- * definitions loaded there are no literal-returning overloads and this
10738
- * narrows nothing. Returns true if it handled the comparison. */
11011
+ * A function declared as an overload set whose members return
11012
+ * *string-literal* types (`(value: number) => "number"`, ...) says by its
11013
+ * answer what it was given. So for `f(x) == "lit"` we keep the parameter
11014
+ * types of the overloads that can return `"lit"`, and that union is what
11015
+ * `x` narrows to. Returns true if it handled the comparison. */
10739
11016
  narrowByCallResult(callSide, litSide, yes, no, env) {
10740
11017
  if (callSide.type !== "CallExpression" || callSide.arguments.length !== 1) return false;
10741
11018
  const name = this.asLiteral(litSide);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tilua/parser",
3
- "version": "3.1.0",
3
+ "version": "4.0.0",
4
4
  "description": "tilua parser for roblox",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",