luaut-parser 1.1.0 → 1.2.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
@@ -127,8 +127,7 @@ analyzeTypes(program, scopes, {
127
127
  (`export { a } from`) and `export * from` are.
128
128
  - `setmetatable` and metatables are not modelled.
129
129
  - Accessing a property a type does not have yields `unknown` rather than an
130
- error; assigning to a `readonly` property is not reported; generic
131
- constraints are not checked at call sites.
130
+ error; assigning to a `readonly` property is not reported.
132
131
 
133
132
  ## Development
134
133
 
package/dist/index.cjs CHANGED
@@ -3832,6 +3832,7 @@ var TypeAnalyzer = class {
3832
3832
  bindingType = /* @__PURE__ */ new Map();
3833
3833
  narrowedTypeOf = /* @__PURE__ */ new Map();
3834
3834
  typeOfTypeNode = /* @__PURE__ */ new Map();
3835
+ expectedTypeOf = /* @__PURE__ */ new Map();
3835
3836
  /** Public: each alias resolved once (generic aliases keep their params as
3836
3837
  * `typeParam` nodes in the body). */
3837
3838
  aliases = /* @__PURE__ */ new Map();
@@ -3897,6 +3898,7 @@ var TypeAnalyzer = class {
3897
3898
  bindingType: this.bindingType,
3898
3899
  narrowedTypeOf: this.narrowedTypeOf,
3899
3900
  typeOfTypeNode: this.typeOfTypeNode,
3901
+ expectedTypeOf: this.expectedTypeOf,
3900
3902
  aliases: this.resolveDeferredAliases(),
3901
3903
  diagnostics: this.diagnostics
3902
3904
  };
@@ -4878,16 +4880,76 @@ var TypeAnalyzer = class {
4878
4880
  return void 0;
4879
4881
  }
4880
4882
  /** Can this signature be called with these argument types? The signature's
4881
- * own generic parameters act as wildcards they are what the call would
4882
- * infer, so they must not make the match fail. */
4883
+ * own type parameters stand for what the call would infer, so each is
4884
+ * checked only against its constraint `<K extends keyof Services>`
4885
+ * accepts `"Players"` but not `""`. */
4883
4886
  overloadAccepts(f, argTypes) {
4884
4887
  if (!f.varargs && argTypes.length > f.params.length) return false;
4885
- const wildcards = new Map((f.typeParams ?? []).map((n) => [n, anyType]));
4888
+ const params = this.boundParams(f);
4886
4889
  return f.params.every((p, i) => {
4887
4890
  if (argTypes[i] === void 0) return p.optional === true;
4888
- return isAssignable(argTypes[i], substitute(p.type, wildcards));
4891
+ return isAssignable(argTypes[i], params[i]);
4889
4892
  });
4890
4893
  }
4894
+ /** A signature's parameter types as a call site sees them before inference:
4895
+ * each type parameter replaced by its constraint, or by `any` when it has
4896
+ * none — or when the constraint mentions another type parameter, which a
4897
+ * lone argument cannot be checked against without false errors. */
4898
+ boundParams(f) {
4899
+ if (!f.typeParams?.length) return f.params.map((p) => p.type);
4900
+ const bounds = new Map(f.typeParams.map((name) => [name, anyType]));
4901
+ const seen = /* @__PURE__ */ new WeakSet();
4902
+ const walk = (value) => {
4903
+ if (!value || typeof value !== "object" || seen.has(value)) return;
4904
+ seen.add(value);
4905
+ if (value instanceof Map) {
4906
+ value.forEach(walk);
4907
+ return;
4908
+ }
4909
+ const t = value;
4910
+ if (t.kind === "typeParam" && typeof t.name === "string" && bounds.has(t.name) && t.constraint && !containsTypeParam(t.constraint)) {
4911
+ bounds.set(t.name, this.reduceType(t.constraint));
4912
+ }
4913
+ for (const child of Object.values(value)) walk(child);
4914
+ };
4915
+ for (const p of f.params) walk(p.type);
4916
+ return f.params.map((p) => substitute(p.type, bounds));
4917
+ }
4918
+ /** Record what each written argument is expected to be — see
4919
+ * `TypeAnalysis.expectedTypeOf`. */
4920
+ recordExpected(written, fns, selfOf) {
4921
+ written.forEach((arg, j) => {
4922
+ const candidates = [];
4923
+ for (const f of fns) {
4924
+ const i = j + selfOf(f);
4925
+ const param = i < f.params.length ? this.boundParams(f)[i] : f.varargs;
4926
+ if (param) candidates.push(param);
4927
+ }
4928
+ if (candidates.length) this.expectedTypeOf.set(arg, union(candidates));
4929
+ });
4930
+ }
4931
+ /** No signature accepts the call, and the argument count is not the
4932
+ * problem: say which argument is wrong, the way TypeScript does. */
4933
+ reportArguments(call, written, fns, argsFor, selfOf) {
4934
+ if (!this.emitDiagnostics) return;
4935
+ if (fns.length > 1) {
4936
+ this.diagnostics.push({ node: call, message: "No overload matches this call" });
4937
+ return;
4938
+ }
4939
+ const f = fns[0];
4940
+ const args = argsFor(f);
4941
+ const params = this.boundParams(f);
4942
+ const self = selfOf(f);
4943
+ for (let i = 0; i < f.params.length; i++) {
4944
+ const arg = args[i];
4945
+ if (arg === void 0 || isAssignable(arg, params[i])) continue;
4946
+ this.diagnostics.push({
4947
+ node: written[i - self] ?? call,
4948
+ message: `Argument of type '${formatType(arg)}' is not assignable to parameter of type '${briefType(params[i])}'`
4949
+ });
4950
+ return;
4951
+ }
4952
+ }
4891
4953
  /** A required parameter may not follow an optional one — otherwise the
4892
4954
  * optional one could never actually be omitted. Same rule as TypeScript,
4893
4955
  * and it applies to a default (`a = 1`) as much as to a `?`. */
@@ -4917,22 +4979,25 @@ var TypeAnalyzer = class {
4917
4979
  return { min, max: f.varargs ? void 0 : f.params.length };
4918
4980
  }
4919
4981
  /** Report a call that passes too few or too many arguments. Only fires
4920
- * when *no* overload accepts the call, so an overload set still reports
4921
- * once, against its first signature. */
4982
+ * when *no* overload accepts the count, so an overload set still reports
4983
+ * once, against its first signature. Returns whether the count fits, so
4984
+ * an argument's type is only complained about when its count is right. */
4922
4985
  checkArity(node, fns, argCount, selfArgs) {
4923
- if (!this.emitDiagnostics || !fns.length) return;
4986
+ if (!fns.length) return true;
4924
4987
  const fits = fns.some((f) => {
4925
4988
  const { min: min2, max: max2 } = this.arityOf(f);
4926
4989
  const n = argCount + selfArgs;
4927
4990
  return n >= min2 && (max2 === void 0 || n <= max2);
4928
4991
  });
4929
- if (fits) return;
4992
+ if (fits) return true;
4993
+ if (!this.emitDiagnostics) return false;
4930
4994
  const { min, max } = this.arityOf(fns[0]);
4931
4995
  const need = max === void 0 ? `at least ${min - selfArgs}` : min === max ? `${min - selfArgs}` : `${min - selfArgs}-${max - selfArgs}`;
4932
4996
  this.diagnostics.push({
4933
4997
  node,
4934
4998
  message: `Expected ${need} argument${need === "1" ? "" : "s"}, got ${argCount}`
4935
4999
  });
5000
+ return false;
4936
5001
  }
4937
5002
  signatureToFnType(sig) {
4938
5003
  const names = sig.generics.map((g) => g.name);
@@ -5370,11 +5435,13 @@ var TypeAnalyzer = class {
5370
5435
  const argTypes = expr.arguments.map((a) => this.infer(a, env));
5371
5436
  const fns = this.overloadsOf(callee);
5372
5437
  if (fns.length) {
5373
- this.checkArity(expr, fns, argTypes.length, 0);
5438
+ this.recordExpected(expr.arguments, fns, () => 0);
5439
+ const arityFits = this.checkArity(expr, fns, argTypes.length, 0);
5374
5440
  const picked = this.pickOverload(fns, argTypes);
5375
5441
  if (picked) {
5376
5442
  return this.callReturn(picked, this.constArgs(picked, expr.arguments, argTypes, env));
5377
5443
  }
5444
+ if (arityFits) this.reportArguments(expr, expr.arguments, fns, () => argTypes, () => 0);
5378
5445
  return union(fns.map((f) => this.callReturn(f, argTypes)));
5379
5446
  }
5380
5447
  return callee.kind === "any" ? anyType : unknownType;
@@ -5385,13 +5452,16 @@ var TypeAnalyzer = class {
5385
5452
  const fns = this.overloadsOf(this.propertyType(objType, expr.method.name));
5386
5453
  if (fns.length) {
5387
5454
  const withSelf = (f) => this.takesSelf(f) ? [objType, ...argTypes] : argTypes;
5388
- this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
5455
+ const selfOf = (f) => this.takesSelf(f) ? 1 : 0;
5456
+ this.recordExpected(expr.arguments, fns, selfOf);
5457
+ const arityFits = this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
5389
5458
  const picked = this.pickOverload(fns, argTypes, withSelf);
5390
5459
  if (picked) {
5391
5460
  const self = this.takesSelf(picked) ? 1 : 0;
5392
5461
  const written = this.constArgs(picked, expr.arguments, argTypes, env, self);
5393
5462
  return this.callReturn(picked, this.takesSelf(picked) ? [objType, ...written] : written);
5394
5463
  }
5464
+ if (arityFits) this.reportArguments(expr, expr.arguments, fns, withSelf, selfOf);
5395
5465
  return union(fns.map((f) => this.callReturn(f, withSelf(f))));
5396
5466
  }
5397
5467
  return objType.kind === "any" ? anyType : unknownType;
@@ -5839,6 +5909,13 @@ function containsTypeQuery(node) {
5839
5909
  if (node.type === "TypeofTypeNode") return true;
5840
5910
  return Object.values(node).some(containsTypeQuery);
5841
5911
  }
5912
+ function briefType(t) {
5913
+ if (t.kind === "union" && t.types.length > 8) {
5914
+ const shown = t.types.slice(0, 6).map(formatType).join(" | ");
5915
+ return `${shown} | ... ${t.types.length - 6} more`;
5916
+ }
5917
+ return formatType(t);
5918
+ }
5842
5919
 
5843
5920
  // src/lib/luau.ts
5844
5921
  var import_node_fs = require("fs");
package/dist/index.d.cts CHANGED
@@ -1080,6 +1080,12 @@ interface TypeAnalysis {
1080
1080
  * `typeof x`, a property's type inside `{ ... }`. Inside a generic alias or
1081
1081
  * function its parameters stay unresolved (`T`). */
1082
1082
  readonly typeOfTypeNode: Map<TypeNode | TypePackNode, Type>;
1083
+ /** What each call argument is expected to be: the parameter it lands on,
1084
+ * with the signature's type parameters replaced by their constraints — a
1085
+ * union when an overload set disagrees. Recorded even for a call that does
1086
+ * not type-check, since that is exactly when an editor wants to offer the
1087
+ * values that would. */
1088
+ readonly expectedTypeOf: Map<Expression, Type>;
1083
1089
  /** Top-level type aliases, resolved — and the type names this module
1084
1090
  * imports, so tooling treats both alike. */
1085
1091
  readonly aliases: Map<string, Type>;
package/dist/index.d.ts CHANGED
@@ -1080,6 +1080,12 @@ interface TypeAnalysis {
1080
1080
  * `typeof x`, a property's type inside `{ ... }`. Inside a generic alias or
1081
1081
  * function its parameters stay unresolved (`T`). */
1082
1082
  readonly typeOfTypeNode: Map<TypeNode | TypePackNode, Type>;
1083
+ /** What each call argument is expected to be: the parameter it lands on,
1084
+ * with the signature's type parameters replaced by their constraints — a
1085
+ * union when an overload set disagrees. Recorded even for a call that does
1086
+ * not type-check, since that is exactly when an editor wants to offer the
1087
+ * values that would. */
1088
+ readonly expectedTypeOf: Map<Expression, Type>;
1083
1089
  /** Top-level type aliases, resolved — and the type names this module
1084
1090
  * imports, so tooling treats both alike. */
1085
1091
  readonly aliases: Map<string, Type>;
package/dist/index.js CHANGED
@@ -3738,6 +3738,7 @@ var TypeAnalyzer = class {
3738
3738
  bindingType = /* @__PURE__ */ new Map();
3739
3739
  narrowedTypeOf = /* @__PURE__ */ new Map();
3740
3740
  typeOfTypeNode = /* @__PURE__ */ new Map();
3741
+ expectedTypeOf = /* @__PURE__ */ new Map();
3741
3742
  /** Public: each alias resolved once (generic aliases keep their params as
3742
3743
  * `typeParam` nodes in the body). */
3743
3744
  aliases = /* @__PURE__ */ new Map();
@@ -3803,6 +3804,7 @@ var TypeAnalyzer = class {
3803
3804
  bindingType: this.bindingType,
3804
3805
  narrowedTypeOf: this.narrowedTypeOf,
3805
3806
  typeOfTypeNode: this.typeOfTypeNode,
3807
+ expectedTypeOf: this.expectedTypeOf,
3806
3808
  aliases: this.resolveDeferredAliases(),
3807
3809
  diagnostics: this.diagnostics
3808
3810
  };
@@ -4784,16 +4786,76 @@ var TypeAnalyzer = class {
4784
4786
  return void 0;
4785
4787
  }
4786
4788
  /** Can this signature be called with these argument types? The signature's
4787
- * own generic parameters act as wildcards they are what the call would
4788
- * infer, so they must not make the match fail. */
4789
+ * own type parameters stand for what the call would infer, so each is
4790
+ * checked only against its constraint `<K extends keyof Services>`
4791
+ * accepts `"Players"` but not `""`. */
4789
4792
  overloadAccepts(f, argTypes) {
4790
4793
  if (!f.varargs && argTypes.length > f.params.length) return false;
4791
- const wildcards = new Map((f.typeParams ?? []).map((n) => [n, anyType]));
4794
+ const params = this.boundParams(f);
4792
4795
  return f.params.every((p, i) => {
4793
4796
  if (argTypes[i] === void 0) return p.optional === true;
4794
- return isAssignable(argTypes[i], substitute(p.type, wildcards));
4797
+ return isAssignable(argTypes[i], params[i]);
4795
4798
  });
4796
4799
  }
4800
+ /** A signature's parameter types as a call site sees them before inference:
4801
+ * each type parameter replaced by its constraint, or by `any` when it has
4802
+ * none — or when the constraint mentions another type parameter, which a
4803
+ * lone argument cannot be checked against without false errors. */
4804
+ boundParams(f) {
4805
+ if (!f.typeParams?.length) return f.params.map((p) => p.type);
4806
+ const bounds = new Map(f.typeParams.map((name) => [name, anyType]));
4807
+ const seen = /* @__PURE__ */ new WeakSet();
4808
+ const walk = (value) => {
4809
+ if (!value || typeof value !== "object" || seen.has(value)) return;
4810
+ seen.add(value);
4811
+ if (value instanceof Map) {
4812
+ value.forEach(walk);
4813
+ return;
4814
+ }
4815
+ const t = value;
4816
+ if (t.kind === "typeParam" && typeof t.name === "string" && bounds.has(t.name) && t.constraint && !containsTypeParam(t.constraint)) {
4817
+ bounds.set(t.name, this.reduceType(t.constraint));
4818
+ }
4819
+ for (const child of Object.values(value)) walk(child);
4820
+ };
4821
+ for (const p of f.params) walk(p.type);
4822
+ return f.params.map((p) => substitute(p.type, bounds));
4823
+ }
4824
+ /** Record what each written argument is expected to be — see
4825
+ * `TypeAnalysis.expectedTypeOf`. */
4826
+ recordExpected(written, fns, selfOf) {
4827
+ written.forEach((arg, j) => {
4828
+ const candidates = [];
4829
+ for (const f of fns) {
4830
+ const i = j + selfOf(f);
4831
+ const param = i < f.params.length ? this.boundParams(f)[i] : f.varargs;
4832
+ if (param) candidates.push(param);
4833
+ }
4834
+ if (candidates.length) this.expectedTypeOf.set(arg, union(candidates));
4835
+ });
4836
+ }
4837
+ /** No signature accepts the call, and the argument count is not the
4838
+ * problem: say which argument is wrong, the way TypeScript does. */
4839
+ reportArguments(call, written, fns, argsFor, selfOf) {
4840
+ if (!this.emitDiagnostics) return;
4841
+ if (fns.length > 1) {
4842
+ this.diagnostics.push({ node: call, message: "No overload matches this call" });
4843
+ return;
4844
+ }
4845
+ const f = fns[0];
4846
+ const args = argsFor(f);
4847
+ const params = this.boundParams(f);
4848
+ const self = selfOf(f);
4849
+ for (let i = 0; i < f.params.length; i++) {
4850
+ const arg = args[i];
4851
+ if (arg === void 0 || isAssignable(arg, params[i])) continue;
4852
+ this.diagnostics.push({
4853
+ node: written[i - self] ?? call,
4854
+ message: `Argument of type '${formatType(arg)}' is not assignable to parameter of type '${briefType(params[i])}'`
4855
+ });
4856
+ return;
4857
+ }
4858
+ }
4797
4859
  /** A required parameter may not follow an optional one — otherwise the
4798
4860
  * optional one could never actually be omitted. Same rule as TypeScript,
4799
4861
  * and it applies to a default (`a = 1`) as much as to a `?`. */
@@ -4823,22 +4885,25 @@ var TypeAnalyzer = class {
4823
4885
  return { min, max: f.varargs ? void 0 : f.params.length };
4824
4886
  }
4825
4887
  /** Report a call that passes too few or too many arguments. Only fires
4826
- * when *no* overload accepts the call, so an overload set still reports
4827
- * once, against its first signature. */
4888
+ * when *no* overload accepts the count, so an overload set still reports
4889
+ * once, against its first signature. Returns whether the count fits, so
4890
+ * an argument's type is only complained about when its count is right. */
4828
4891
  checkArity(node, fns, argCount, selfArgs) {
4829
- if (!this.emitDiagnostics || !fns.length) return;
4892
+ if (!fns.length) return true;
4830
4893
  const fits = fns.some((f) => {
4831
4894
  const { min: min2, max: max2 } = this.arityOf(f);
4832
4895
  const n = argCount + selfArgs;
4833
4896
  return n >= min2 && (max2 === void 0 || n <= max2);
4834
4897
  });
4835
- if (fits) return;
4898
+ if (fits) return true;
4899
+ if (!this.emitDiagnostics) return false;
4836
4900
  const { min, max } = this.arityOf(fns[0]);
4837
4901
  const need = max === void 0 ? `at least ${min - selfArgs}` : min === max ? `${min - selfArgs}` : `${min - selfArgs}-${max - selfArgs}`;
4838
4902
  this.diagnostics.push({
4839
4903
  node,
4840
4904
  message: `Expected ${need} argument${need === "1" ? "" : "s"}, got ${argCount}`
4841
4905
  });
4906
+ return false;
4842
4907
  }
4843
4908
  signatureToFnType(sig) {
4844
4909
  const names = sig.generics.map((g) => g.name);
@@ -5276,11 +5341,13 @@ var TypeAnalyzer = class {
5276
5341
  const argTypes = expr.arguments.map((a) => this.infer(a, env));
5277
5342
  const fns = this.overloadsOf(callee);
5278
5343
  if (fns.length) {
5279
- this.checkArity(expr, fns, argTypes.length, 0);
5344
+ this.recordExpected(expr.arguments, fns, () => 0);
5345
+ const arityFits = this.checkArity(expr, fns, argTypes.length, 0);
5280
5346
  const picked = this.pickOverload(fns, argTypes);
5281
5347
  if (picked) {
5282
5348
  return this.callReturn(picked, this.constArgs(picked, expr.arguments, argTypes, env));
5283
5349
  }
5350
+ if (arityFits) this.reportArguments(expr, expr.arguments, fns, () => argTypes, () => 0);
5284
5351
  return union(fns.map((f) => this.callReturn(f, argTypes)));
5285
5352
  }
5286
5353
  return callee.kind === "any" ? anyType : unknownType;
@@ -5291,13 +5358,16 @@ var TypeAnalyzer = class {
5291
5358
  const fns = this.overloadsOf(this.propertyType(objType, expr.method.name));
5292
5359
  if (fns.length) {
5293
5360
  const withSelf = (f) => this.takesSelf(f) ? [objType, ...argTypes] : argTypes;
5294
- this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
5361
+ const selfOf = (f) => this.takesSelf(f) ? 1 : 0;
5362
+ this.recordExpected(expr.arguments, fns, selfOf);
5363
+ const arityFits = this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
5295
5364
  const picked = this.pickOverload(fns, argTypes, withSelf);
5296
5365
  if (picked) {
5297
5366
  const self = this.takesSelf(picked) ? 1 : 0;
5298
5367
  const written = this.constArgs(picked, expr.arguments, argTypes, env, self);
5299
5368
  return this.callReturn(picked, this.takesSelf(picked) ? [objType, ...written] : written);
5300
5369
  }
5370
+ if (arityFits) this.reportArguments(expr, expr.arguments, fns, withSelf, selfOf);
5301
5371
  return union(fns.map((f) => this.callReturn(f, withSelf(f))));
5302
5372
  }
5303
5373
  return objType.kind === "any" ? anyType : unknownType;
@@ -5745,6 +5815,13 @@ function containsTypeQuery(node) {
5745
5815
  if (node.type === "TypeofTypeNode") return true;
5746
5816
  return Object.values(node).some(containsTypeQuery);
5747
5817
  }
5818
+ function briefType(t) {
5819
+ if (t.kind === "union" && t.types.length > 8) {
5820
+ const shown = t.types.slice(0, 6).map(formatType).join(" | ");
5821
+ return `${shown} | ... ${t.types.length - 6} more`;
5822
+ }
5823
+ return formatType(t);
5824
+ }
5748
5825
 
5749
5826
  // src/lib/luau.ts
5750
5827
  import { readFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luaut-parser",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "luaut parser for roblox",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -36,8 +36,5 @@
36
36
  "tsup": "^8.5.1",
37
37
  "tsx": "^4.19.0",
38
38
  "typescript": "^5.0.4"
39
- },
40
- "dependencies": {
41
- "luaut-parser": "^1.0.0"
42
39
  }
43
40
  }