luaut-parser 2.1.0 → 3.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
@@ -71,10 +71,10 @@ npm i -D @luaut/roblox # or just @luaut/luau
71
71
  - **Which config applies** — the nearest one in the file's folder or above.
72
72
  `luaut.config.json` and `luaut.config.jsonc` in the same folder is an error.
73
73
  Both forms accept comments and trailing commas.
74
- - **`types`** — `"luau"` is looked up as `@luaut/luau`, then as a package named
75
- `luau`, in `node_modules` from the config upward. A full package name or a
76
- relative path (`"./types"`, `"./defs.d.luaut"`) works too. A type library's
77
- own type-library dependencies load first.
74
+ - **`types`** — any name, looked up as the package `@luaut/<name>` in
75
+ `node_modules` from the config upward; one that is not installed is an
76
+ error. A relative path (`"./types"`, `"./defs.d.luaut"`) loads the project's
77
+ own definitions. A type library's own type-library dependencies load first.
78
78
  - **`paths`** — tsconfig rules: an exact pattern wins, then the `*` pattern
79
79
  with the longest prefix; targets resolve from `baseUrl` (default: the
80
80
  config's folder).
@@ -106,6 +106,22 @@ must.
106
106
 
107
107
  **Declarations** — `const` and `let` only; Lua's `local` is gone.
108
108
 
109
+ **Functions** — `function name() ... end` declares `name` in the enclosing
110
+ scope; like a TypeScript function declaration it cannot be reassigned.
111
+ `const` and `let` do not apply to functions. `function T.name()` and
112
+ `function T:name()` define a member.
113
+
114
+ **Modules** — `import { a, b as c } from "./m"`, `import D from "./m"` and
115
+ `import * as M from "./m"`; `export const`, `export function`, `export default`,
116
+ `export { a as b }`, `export { a } from "./m"` and `export * from "./m"`.
117
+ Imports are read-only: assigning to an imported name, or to a member of a
118
+ namespace (`M.x = 1`), is an error.
119
+
120
+ `import type { A } from "./m"` (also `import type D` and `import type * as M`)
121
+ brings in names that are types and nothing else: unlike TypeScript, using one
122
+ as a value is an error, and only type positions — `typeof A` included — may
123
+ name it. Compiled code keeps no trace of it.
124
+
109
125
  **Optionality** — there is no `T?` shorthand. `?` in type position always
110
126
  belongs to a conditional type, and in expression position to a ternary.
111
127
 
@@ -150,6 +166,10 @@ operand and then the right one, as Luau does. So `Vector3 + Vector3` and
150
166
  (`declare class Enum.Material extends EnumItem {}`), and code writes it the
151
167
  same way.
152
168
 
169
+ **Contextual typing** — an expression takes its type from where it is
170
+ written, as in TypeScript: `let queue: thread[] = []` is a `thread[]`, and so
171
+ is `[]` passed where one is expected, including inside an object literal.
172
+
153
173
  **Calls** — every argument is checked against its parameter, and a generic
154
174
  parameter against its constraint (`GetService<K extends keyof Services>`
155
175
  rejects `""`).
package/dist/index.cjs CHANGED
@@ -889,23 +889,14 @@ var Parser = class {
889
889
  if (t.type === "Punctuator" && t.value === "@") {
890
890
  const { attributes, start } = this.parseAttributes();
891
891
  const next = this.current();
892
- if (next.type === "Keyword" && (next.value === "const" || next.value === "let")) {
893
- const stmt = this.parseVariableDeclaration();
894
- if (stmt.type === "FunctionDeclaration") {
895
- stmt.attributes = attributes;
896
- stmt.line.start = start.line.start;
897
- stmt.column.start = start.column.start;
898
- }
899
- return stmt;
900
- }
901
892
  if (next.type === "Keyword" && next.value === "function") {
902
- const stmt = this.parseFunctionDeclarationStatement();
893
+ const stmt = this.parseFunctionStatement();
903
894
  stmt.attributes = attributes;
904
895
  stmt.line.start = start.line.start;
905
896
  stmt.column.start = start.column.start;
906
897
  return stmt;
907
898
  }
908
- throw new ParseError("Expected 'function', 'const', or 'let' after attribute", next.line.start, next.column.start);
899
+ throw new ParseError("Expected 'function' after an attribute", next.line.start, next.column.start);
909
900
  }
910
901
  if (t.type === "Keyword") {
911
902
  switch (t.value) {
@@ -923,7 +914,7 @@ var Parser = class {
923
914
  case "for":
924
915
  return this.parseForStatement();
925
916
  case "function":
926
- return this.parseFunctionDeclarationStatement();
917
+ return this.parseFunctionStatement();
927
918
  case "return":
928
919
  return this.parseReturnStatement();
929
920
  case "import":
@@ -1025,20 +1016,30 @@ var Parser = class {
1025
1016
  parseImportStatement() {
1026
1017
  const start = this.current();
1027
1018
  this.advance();
1019
+ const next = this.peek(1);
1020
+ const isTypeOnly = this.checkIdentifierValue("type") && (next.type === "Punctuator" && next.value === "{" || next.type === "Operator" && next.value === "*" || next.type === "Identifier");
1021
+ if (isTypeOnly) this.advance();
1028
1022
  let defaultImport;
1029
1023
  const specifiers = [];
1030
- if (this.checkType("Identifier")) {
1031
- const nameTok = this.expectIdentifier();
1032
- defaultImport = { type: "Identifier", name: nameTok.value, ...spanFrom(nameTok, nameTok) };
1033
- if (this.matchPunctuator(",")) {
1034
- this.expectPunctuator("{");
1035
- this.parseImportSpecifierList(specifiers);
1036
- this.expectPunctuator("}");
1024
+ let namespaceImport;
1025
+ const parseBindings = () => {
1026
+ if (this.checkOperator("*")) {
1027
+ this.advance();
1028
+ if (!this.checkKeyword("as")) this.error("Expected 'as' after 'import *'");
1029
+ this.advance();
1030
+ namespaceImport = this.parseIdentifier();
1031
+ return;
1037
1032
  }
1038
- } else {
1039
1033
  this.expectPunctuator("{");
1040
1034
  this.parseImportSpecifierList(specifiers);
1041
1035
  this.expectPunctuator("}");
1036
+ };
1037
+ if (this.checkType("Identifier")) {
1038
+ const nameTok = this.expectIdentifier();
1039
+ defaultImport = { type: "Identifier", name: nameTok.value, ...spanFrom(nameTok, nameTok) };
1040
+ if (this.matchPunctuator(",")) parseBindings();
1041
+ } else {
1042
+ parseBindings();
1042
1043
  }
1043
1044
  if (!this.checkKeyword("from")) {
1044
1045
  this.error("Expected 'from' in import statement");
@@ -1055,7 +1056,15 @@ var Parser = class {
1055
1056
  raw: sourceTok.raw,
1056
1057
  ...spanFrom(sourceTok, sourceTok)
1057
1058
  };
1058
- return { type: "ImportStatement", defaultImport, specifiers, source, ...spanFrom(start, this.previous()) };
1059
+ return {
1060
+ type: "ImportStatement",
1061
+ defaultImport,
1062
+ namespaceImport,
1063
+ specifiers,
1064
+ source,
1065
+ isTypeOnly: isTypeOnly || void 0,
1066
+ ...spanFrom(start, this.previous())
1067
+ };
1059
1068
  }
1060
1069
  parseImportSpecifierList(out) {
1061
1070
  if (this.checkPunctuator("}")) return;
@@ -1091,7 +1100,7 @@ var Parser = class {
1091
1100
  ...spanFrom(sourceTok, sourceTok)
1092
1101
  };
1093
1102
  }
1094
- // `export const ...` / `export let ...` / `export const function ...` /
1103
+ // `export const ...` / `export let ...` / `export function ...` /
1095
1104
  // `export type ...` / `export default <expr>`
1096
1105
  parseExportStatement() {
1097
1106
  const start = this.current();
@@ -1109,6 +1118,11 @@ var Parser = class {
1109
1118
  const declaration = this.parseVariableDeclaration();
1110
1119
  return { type: "ExportStatement", declaration, ...spanFrom(start, this.previous()) };
1111
1120
  }
1121
+ if (this.checkKeyword("function")) {
1122
+ const declaration = this.parseFunctionStatement();
1123
+ if (declaration.type !== "FunctionDeclaration") this.error("An exported function needs a plain name: 'export function name()'");
1124
+ return { type: "ExportStatement", declaration, ...spanFrom(start, this.previous()) };
1125
+ }
1112
1126
  if (this.checkPunctuator("{")) {
1113
1127
  this.advance();
1114
1128
  const specifiers = [];
@@ -1132,15 +1146,15 @@ var Parser = class {
1132
1146
  const source = this.parseModuleSource();
1133
1147
  return { type: "ExportAllStatement", source, ...spanFrom(start, this.previous()) };
1134
1148
  }
1135
- this.error("Expected 'const', 'let', 'type', 'default', '{' or '*' after 'export'");
1149
+ this.error("Expected 'const', 'let', 'function', 'type', 'default', '{' or '*' after 'export'");
1136
1150
  }
1137
- // `const x = ...` / `let x, y = ...` / `const function f() ... end`.
1151
+ // `const x = ...` / `let x, y = ...`.
1138
1152
  // luaut has no `local` — `const` bindings are immutable, `let` mutable.
1139
1153
  parseVariableDeclaration() {
1140
1154
  const start = this.current();
1141
1155
  const kind = this.advance().value;
1142
- if (this.matchKeyword("function")) {
1143
- return this.parseFunctionDeclarationRest(start, kind);
1156
+ if (this.checkKeyword("function")) {
1157
+ this.error(`A function is declared as 'function name()'; '${kind}' does not apply to functions`);
1144
1158
  }
1145
1159
  const names = [this.parseBindingTarget(true)];
1146
1160
  while (this.matchPunctuator(",")) {
@@ -1154,31 +1168,6 @@ var Parser = class {
1154
1168
  }
1155
1169
  return { type: "VariableDeclaration", kind, names, init, ...spanFrom(start, this.previous()) };
1156
1170
  }
1157
- /** `const/let function` — `function` already consumed. Collects TS-style
1158
- * overload signatures. */
1159
- parseFunctionDeclarationRest(start, kind) {
1160
- const name = this.parseIdentifier();
1161
- const signatures = [];
1162
- while (true) {
1163
- const head = this.parseFunctionHead();
1164
- if (this.isOverloadContinuation(name.name, kind)) {
1165
- signatures.push(this.headToSignature(head));
1166
- this.advance();
1167
- this.expectKeyword("function");
1168
- this.parseIdentifier();
1169
- continue;
1170
- }
1171
- const func = this.headToBody(head);
1172
- return {
1173
- type: "FunctionDeclaration",
1174
- kind,
1175
- name,
1176
- func,
1177
- signatures: signatures.length ? signatures : void 0,
1178
- ...spanFrom(start, this.previous())
1179
- };
1180
- }
1181
- }
1182
1171
  parseIfStatement() {
1183
1172
  const start = this.current();
1184
1173
  this.expectKeyword("if");
@@ -1268,7 +1257,9 @@ var Parser = class {
1268
1257
  ...spanFrom(start, this.previous())
1269
1258
  };
1270
1259
  }
1271
- parseFunctionDeclarationStatement() {
1260
+ /** `function name() end` declares `name`; `function a.b() end` and
1261
+ * `function T:m() end` define a member. */
1262
+ parseFunctionStatement() {
1272
1263
  const start = this.current();
1273
1264
  this.expectKeyword("function");
1274
1265
  const target = this.parseFunctionName();
@@ -1284,6 +1275,15 @@ var Parser = class {
1284
1275
  continue;
1285
1276
  }
1286
1277
  const func = this.headToBody(head);
1278
+ if (simpleName !== void 0) {
1279
+ return {
1280
+ type: "FunctionDeclaration",
1281
+ name: target.base,
1282
+ func,
1283
+ signatures: signatures.length ? signatures : void 0,
1284
+ ...spanFrom(start, this.previous())
1285
+ };
1286
+ }
1287
1287
  if (isMethod) {
1288
1288
  func.params.unshift({ type: "FunctionParameter", name: "self", ...spanFrom(target, target) });
1289
1289
  func.isMethod = true;
@@ -1300,12 +1300,8 @@ var Parser = class {
1300
1300
  }
1301
1301
  /** After a bodyless function head, is the next token the start of another
1302
1302
  * declaration for the same simple `name` (making the head an overload
1303
- * signature rather than an implementation)? `kind` is set for a
1304
- * `const/let function` group, undefined for a bare `function` group. */
1305
- isOverloadContinuation(name, kind) {
1306
- if (kind) {
1307
- return this.checkKeyword(kind) && this.peek(1).type === "Keyword" && this.peek(1).value === "function" && this.peek(2).type === "Identifier" && this.peek(2).value === name;
1308
- }
1303
+ * signature rather than an implementation)? */
1304
+ isOverloadContinuation(name) {
1309
1305
  return this.checkKeyword("function") && this.peek(1).type === "Identifier" && this.peek(1).value === name;
1310
1306
  }
1311
1307
  parseFunctionName() {
@@ -2612,7 +2608,7 @@ var Analyzer = class {
2612
2608
  };
2613
2609
  }
2614
2610
  // ---------------- declaration / resolution primitives ----------------
2615
- declare(scope, name, kind, node, isConst = false) {
2611
+ declare(scope, name, kind, node, isConst = false, declaredBy) {
2616
2612
  if (scope.declarations.has(name) && scope !== this.globalScope) {
2617
2613
  this.diagnostics.push({
2618
2614
  node,
@@ -2621,7 +2617,7 @@ var Analyzer = class {
2621
2617
  });
2622
2618
  }
2623
2619
  const id = this.nextId++;
2624
- this.bindings.set(id, { id, name, kind, declarationNode: node, references: [], isConst });
2620
+ this.bindings.set(id, { id, name, kind, declarationNode: node, references: [], isConst, declaredBy });
2625
2621
  scope.declarations.set(name, id);
2626
2622
  return id;
2627
2623
  }
@@ -2655,6 +2651,19 @@ var Analyzer = class {
2655
2651
  const id = this.resolve(scope, identifier.name);
2656
2652
  this.bindingOf.set(identifier, id);
2657
2653
  this.bindings.get(id).references.push(identifier);
2654
+ if (this.typeQueryDepth === 0) this.checkTypeOnly(id, identifier);
2655
+ }
2656
+ /** Inside `typeof x` in a type, where a type-only import may be named. */
2657
+ typeQueryDepth = 0;
2658
+ /** A name from `import type` used as a value. */
2659
+ checkTypeOnly(id, node) {
2660
+ const b = this.bindings.get(id);
2661
+ if (b.declaredBy !== "type") return;
2662
+ this.diagnostics.push({
2663
+ node,
2664
+ message: `'${b.name}' is imported with 'import type' and can only be used as a type`,
2665
+ kind: "type-only"
2666
+ });
2658
2667
  }
2659
2668
  /** For assignment-like targets (`x = ...`, `function foo() end`): if
2660
2669
  * this resolved to a global with no declaration site yet, treat this
@@ -2671,14 +2680,34 @@ var Analyzer = class {
2671
2680
  this.bindingOf.set(identifier, id);
2672
2681
  this.bindings.get(id).references.push(identifier);
2673
2682
  this.recordPossibleGlobalDefinition(id, identifier);
2683
+ this.checkTypeOnly(id, identifier);
2674
2684
  this.checkConstAssign(id, identifier);
2675
2685
  }
2686
+ /** `Module.x = 1` through `import * as Module`: a module's exports belong
2687
+ * to it and are read-only, as in ES modules. Deeper writes (`Module.x.y`)
2688
+ * change the value, not the module, and are fine. */
2689
+ checkModuleWrite(target) {
2690
+ if (target.type !== "MemberExpression" && target.type !== "IndexExpression") return;
2691
+ if (target.object.type !== "Identifier") return;
2692
+ const id = this.bindingOf.get(target.object);
2693
+ if (id !== void 0 && this.bindings.get(id).declaredBy === "namespace") {
2694
+ this.moduleWriteError(target.object.name, target);
2695
+ }
2696
+ }
2697
+ moduleWriteError(name, node) {
2698
+ this.diagnostics.push({
2699
+ node,
2700
+ message: `Cannot assign to a member of '${name}' \u2014 a module's exports are read-only`,
2701
+ kind: "const-assign"
2702
+ });
2703
+ }
2676
2704
  checkConstAssign(id, node) {
2677
2705
  const b = this.bindings.get(id);
2706
+ if (b.declaredBy === "type") return;
2678
2707
  if (b.isConst) {
2679
2708
  this.diagnostics.push({
2680
2709
  node,
2681
- message: `Cannot assign to '${b.name}' \u2014 it is a const`,
2710
+ message: `Cannot assign to '${b.name}' \u2014 it is ${b.declaredBy === "import" || b.declaredBy === "namespace" ? "an import" : b.declaredBy === "function" ? "a function" : "a const"}`,
2682
2711
  kind: "const-assign"
2683
2712
  });
2684
2713
  }
@@ -2719,6 +2748,7 @@ var Analyzer = class {
2719
2748
  const id = this.resolve(scope, t.name);
2720
2749
  this.bindingOf.set(t, id);
2721
2750
  this.recordPossibleGlobalDefinition(id, t);
2751
+ this.checkTypeOnly(id, t);
2722
2752
  this.checkConstAssign(id, t);
2723
2753
  return;
2724
2754
  }
@@ -2762,7 +2792,7 @@ var Analyzer = class {
2762
2792
  return;
2763
2793
  }
2764
2794
  case "FunctionDeclaration": {
2765
- this.declare(scope, stmt.name.name, "local", stmt.name, stmt.kind === "const");
2795
+ this.declare(scope, stmt.name.name, "local", stmt.name, true, "function");
2766
2796
  for (const signature of stmt.signatures ?? []) this.visitSignature(signature, scope);
2767
2797
  this.visitFunctionBody(stmt.func, scope);
2768
2798
  return;
@@ -2772,6 +2802,11 @@ var Analyzer = class {
2772
2802
  this.referenceAsAssignmentTarget(scope, stmt.target.base);
2773
2803
  } else {
2774
2804
  this.reference(scope, stmt.target.base);
2805
+ const id = this.bindingOf.get(stmt.target.base);
2806
+ const depth = stmt.target.path.length + (stmt.target.method ? 1 : 0);
2807
+ if (id !== void 0 && depth === 1 && this.bindings.get(id).declaredBy === "namespace") {
2808
+ this.moduleWriteError(stmt.target.base.name, stmt.target);
2809
+ }
2775
2810
  }
2776
2811
  for (const signature of stmt.signatures ?? []) this.visitSignature(signature, scope);
2777
2812
  this.visitFunctionBody(stmt.func, scope, stmt.isMethod);
@@ -2786,6 +2821,7 @@ var Analyzer = class {
2786
2821
  this.assignPattern(scope, target);
2787
2822
  } else {
2788
2823
  this.visitExpression(target, scope);
2824
+ this.checkModuleWrite(target);
2789
2825
  }
2790
2826
  }
2791
2827
  return;
@@ -2798,6 +2834,7 @@ var Analyzer = class {
2798
2834
  if (id !== void 0) this.checkConstAssign(id, stmt.target);
2799
2835
  } else {
2800
2836
  this.visitExpression(stmt.target, scope);
2837
+ this.checkModuleWrite(stmt.target);
2801
2838
  }
2802
2839
  return;
2803
2840
  }
@@ -2859,11 +2896,15 @@ var Analyzer = class {
2859
2896
  this.visitType(stmt.definition, scope);
2860
2897
  return;
2861
2898
  case "ImportStatement": {
2899
+ const typeOnly = stmt.isTypeOnly ? "type" : void 0;
2862
2900
  if (stmt.defaultImport) {
2863
- this.declare(scope, stmt.defaultImport.name, "local", stmt.defaultImport);
2901
+ this.declare(scope, stmt.defaultImport.name, "local", stmt.defaultImport, true, typeOnly ?? "import");
2902
+ }
2903
+ if (stmt.namespaceImport) {
2904
+ this.declare(scope, stmt.namespaceImport.name, "local", stmt.namespaceImport, true, typeOnly ?? "namespace");
2864
2905
  }
2865
2906
  for (const spec of stmt.specifiers) {
2866
- this.declare(scope, spec.local.name, "local", spec.local);
2907
+ this.declare(scope, spec.local.name, "local", spec.local, true, typeOnly ?? "import");
2867
2908
  }
2868
2909
  return;
2869
2910
  }
@@ -2922,7 +2963,12 @@ var Analyzer = class {
2922
2963
  return;
2923
2964
  }
2924
2965
  if (value.type === "TypeofTypeNode") {
2925
- this.visitExpression(value.expression, scope);
2966
+ this.typeQueryDepth++;
2967
+ try {
2968
+ this.visitExpression(value.expression, scope);
2969
+ } finally {
2970
+ this.typeQueryDepth--;
2971
+ }
2926
2972
  return;
2927
2973
  }
2928
2974
  for (const key of Object.keys(value)) {
@@ -4081,6 +4127,13 @@ var TypeAnalyzer = class {
4081
4127
  if (stmt.type !== "ImportStatement") continue;
4082
4128
  const exports2 = this.moduleFor(stmt.source.value);
4083
4129
  if (!exports2) continue;
4130
+ if (stmt.namespaceImport) {
4131
+ for (const [name, exported] of exports2.types) {
4132
+ const qualified = `${stmt.namespaceImport.name}.${name}`;
4133
+ this.importedTypes.set(qualified, exported);
4134
+ this.aliases.set(qualified, exported.type);
4135
+ }
4136
+ }
4084
4137
  for (const s of stmt.specifiers) {
4085
4138
  const exported = exports2.types.get(s.imported.name);
4086
4139
  if (exported) {
@@ -4275,6 +4328,16 @@ var TypeAnalyzer = class {
4275
4328
  });
4276
4329
  return subst;
4277
4330
  }
4331
+ /** An imported type, with its type arguments applied. */
4332
+ importedType(imported, typeArguments) {
4333
+ if (!imported.params.length) return imported.type;
4334
+ const subst = /* @__PURE__ */ new Map();
4335
+ imported.params.forEach((name, i) => {
4336
+ const arg = typeArguments[i];
4337
+ subst.set(name, arg ? this.resolveType(arg) : unknownType);
4338
+ });
4339
+ return this.reduceType(substitute(imported.type, subst));
4340
+ }
4278
4341
  // --------------------------------------------------------
4279
4342
  // TypeNode -> Type
4280
4343
  // --------------------------------------------------------
@@ -4325,17 +4388,11 @@ var TypeAnalyzer = class {
4325
4388
  });
4326
4389
  }
4327
4390
  const imported = this.importedTypes.get(node.base);
4328
- if (imported) {
4329
- if (!imported.params.length) return imported.type;
4330
- const subst = /* @__PURE__ */ new Map();
4331
- imported.params.forEach((name2, i) => {
4332
- const arg = node.typeArguments[i];
4333
- subst.set(name2, arg ? this.resolveType(arg) : unknownType);
4334
- });
4335
- return this.reduceType(substitute(imported.type, subst));
4336
- }
4391
+ if (imported) return this.importedType(imported, node.typeArguments);
4337
4392
  const lib = this.options.libTypes?.[node.base];
4338
4393
  if (lib) return lib;
4394
+ } else if (this.importedTypes.has(name)) {
4395
+ return this.importedType(this.importedTypes.get(name), node.typeArguments);
4339
4396
  } else if (this.aliasDefs.has(name)) {
4340
4397
  return this.expand({
4341
4398
  kind: "genericRef",
@@ -4939,6 +4996,14 @@ var TypeAnalyzer = class {
4939
4996
  };
4940
4997
  if (resolving && !exports2) report(stmt.source, `Cannot find module '${specifier}'`);
4941
4998
  const usable = exports2 && !exports2.partial ? exports2 : void 0;
4999
+ if (stmt.namespaceImport) {
5000
+ const id = this.bindingIdByName(stmt.namespaceImport.name, stmt.namespaceImport);
5001
+ if (id !== void 0) {
5002
+ const members = [...usable?.values ?? []].map(([name, type]) => [name, { type, optional: false, readonly: true }]);
5003
+ if (usable?.default) members.push(["default", { type: usable.default, optional: false, readonly: true }]);
5004
+ this.bindingType.set(id, usable ? objectType(members) : anyType);
5005
+ }
5006
+ }
4942
5007
  if (stmt.defaultImport) {
4943
5008
  if (usable && usable.default === void 0) {
4944
5009
  report(stmt.defaultImport, `Module '${specifier}' has no default export`);
@@ -5096,7 +5161,10 @@ var TypeAnalyzer = class {
5096
5161
  applyContext(expr, expected) {
5097
5162
  let e = expr;
5098
5163
  while (e.type === "ParenthesizedExpression") e = e.expression;
5099
- if (e.type !== "FunctionExpression" || !expected) return;
5164
+ if (!expected) return;
5165
+ if (e.type === "ArrayExpression") return this.applyArrayContext(e, expected);
5166
+ if (e.type === "TableExpression") return this.applyTableContext(e, expected);
5167
+ if (e.type !== "FunctionExpression") return;
5100
5168
  const members = expected.kind === "union" ? expected.types : [expected];
5101
5169
  const signatures = members.flatMap((m) => this.overloadsOf(this.expand(m)));
5102
5170
  if (!signatures.length) return;
@@ -5112,6 +5180,46 @@ var TypeAnalyzer = class {
5112
5180
  this.contextualParams.set(p, containsTypeParam(t) ? anyType : t);
5113
5181
  });
5114
5182
  }
5183
+ /** What an array literal is expected to be: an empty one takes that type
5184
+ * outright — `let queue: thread[] = []` is a `thread[]`, as in TypeScript
5185
+ * — and the elements of any other get the element type as their own
5186
+ * context. */
5187
+ contextualArrays = /* @__PURE__ */ new WeakMap();
5188
+ applyArrayContext(e, expected) {
5189
+ const target = this.expectedMembers(expected).find((m) => m.kind === "array" || m.kind === "tuple");
5190
+ if (!target) return;
5191
+ if (!e.elements.length) {
5192
+ if (!containsTypeParam(target)) this.contextualArrays.set(e, target);
5193
+ return;
5194
+ }
5195
+ e.elements.forEach((element, i) => {
5196
+ if (element.type === "SpreadElement") return;
5197
+ const elementType = target.kind === "array" ? target.element : target.elements[i];
5198
+ this.applyContext(element, elementType);
5199
+ });
5200
+ }
5201
+ /** `{ list: [] }` where `{ list: thread[] }` is expected: each field's
5202
+ * value gets its property's type as context. */
5203
+ applyTableContext(e, expected) {
5204
+ const objects = this.expectedMembers(expected).filter((m) => m.kind === "object");
5205
+ if (!objects.length) return;
5206
+ for (const field of e.fields) {
5207
+ if (field.type !== "TableFieldNamed") continue;
5208
+ const key = field.key.type === "Identifier" ? field.key.name : field.key.value;
5209
+ const types = objects.flatMap((o) => {
5210
+ const property = o.properties.get(key);
5211
+ return property ? [property.type] : o.indexer ? [o.indexer.value] : [];
5212
+ });
5213
+ if (types.length) this.applyContext(field.value, union(types));
5214
+ }
5215
+ }
5216
+ /** The members of an expected type worth matching a literal against:
5217
+ * aliases seen through, `nil` left out. */
5218
+ expectedMembers(expected) {
5219
+ const t = this.expand(expected);
5220
+ const members = t.kind === "union" ? t.types : [t];
5221
+ return members.map((m) => this.expand(m)).filter((m) => !(m.kind === "primitive" && m.name === "nil"));
5222
+ }
5115
5223
  /** The parameter type each written argument lands on, across `fns`. */
5116
5224
  expectedArguments(written, fns, selfOf) {
5117
5225
  return written.map((_, j) => {
@@ -5686,8 +5794,9 @@ var TypeAnalyzer = class {
5686
5794
  return this.resolveType(expr.typeAnnotation);
5687
5795
  }
5688
5796
  case "SatisfiesExpression": {
5689
- const actual = this.infer(expr.expression, env);
5690
5797
  const declared = this.resolveType(expr.typeAnnotation);
5798
+ this.applyContext(expr.expression, declared);
5799
+ const actual = this.infer(expr.expression, env);
5691
5800
  if (this.emitDiagnostics && declared.kind !== "any" && !this.fitsAnnotation(expr.expression, declared, actual, env)) {
5692
5801
  this.diagnostics.push({
5693
5802
  node: expr,
@@ -5815,6 +5924,8 @@ var TypeAnalyzer = class {
5815
5924
  }
5816
5925
  }
5817
5926
  inferArray(expr, env, asConst) {
5927
+ const contextual = this.contextualArrays.get(expr);
5928
+ if (contextual && !asConst) return contextual;
5818
5929
  const elems = [];
5819
5930
  let hadSpread = false;
5820
5931
  for (const el of expr.elements) {
@@ -6468,13 +6579,13 @@ function resolveTypeLibraries(config, host = nodeHost) {
6468
6579
  else problems.push({ file: config.path, message: `'${entry}' has no ${ENTRY_FILE} (or 'luaut.types' in its package.json)`, ...entryPosition(config, entry) });
6469
6580
  continue;
6470
6581
  }
6471
- const names = entry.startsWith("@") || entry.includes("/") ? [entry] : [`@luaut/${entry}`, entry];
6472
- const found = names.map((name) => findPackage(name, config.directory, host)).find(Boolean);
6582
+ const name = entry.startsWith("@luaut/") ? entry : `@luaut/${entry}`;
6583
+ const found = findPackage(name, config.directory, host);
6473
6584
  if (found) addPackage(found.directory, found.file, /* @__PURE__ */ new Set());
6474
6585
  else {
6475
6586
  problems.push({
6476
6587
  file: config.path,
6477
- message: `Cannot find type library '${entry}'. Install it with: npm i -D ${names[0]}`,
6588
+ message: `Cannot find type library '${name}'. Install it with: npm i -D ${name}`,
6478
6589
  ...entryPosition(config, entry)
6479
6590
  });
6480
6591
  }
package/dist/index.d.cts CHANGED
@@ -99,11 +99,17 @@ interface ImportStatement extends BaseNode {
99
99
  type: "ImportStatement";
100
100
  /** `import Default from '...'` */
101
101
  defaultImport?: Identifier;
102
+ /** `import * as Module from '...'` — the module's exports as one value. */
103
+ namespaceImport?: Identifier;
104
+ /** `import type { A } from '...'`: every name it brings in is a type and
105
+ * may only be used as one — never as a value. It exists for the type
106
+ * checker alone, and leaves nothing in compiled code. */
107
+ isTypeOnly?: boolean;
102
108
  /** `import { a, b as c } from '...'` */
103
109
  specifiers: ImportSpecifier[];
104
110
  source: StringLiteral;
105
111
  }
106
- /** `export const x = 1`, `export let y = 2`, `export const function f() end` */
112
+ /** `export const x = 1`, `export let y = 2`, `export function f() end` */
107
113
  interface ExportStatement extends BaseNode {
108
114
  type: "ExportStatement";
109
115
  declaration: VariableDeclaration | FunctionDeclaration;
@@ -214,17 +220,19 @@ interface ArrayPatternElement extends BaseNode {
214
220
  value: BindingTarget;
215
221
  default?: Expression;
216
222
  }
217
- /** `const function f() ... end` / `let function f() ... end` a named,
218
- * self-referential (recursive) function binding. */
223
+ /** `function f() ... end` declares `f` in the enclosing scope, visible to
224
+ * its own body (so it can recurse). Like TypeScript's function declaration,
225
+ * the name cannot be reassigned. `function a.b() end` and `function T:m() end`
226
+ * assign to a member instead: see `FunctionDeclarationStatement`. */
219
227
  interface FunctionDeclaration extends BaseNode {
220
228
  type: "FunctionDeclaration";
221
- kind: "const" | "let";
222
229
  name: Identifier;
223
230
  func: FunctionBody;
224
231
  attributes?: string[];
225
232
  /** TS-style overload signatures preceding the implementation (`func`). */
226
233
  signatures?: FunctionSignature[];
227
234
  }
235
+ /** `function a.b() end` / `function T:m() end` — defines a member. */
228
236
  interface FunctionDeclarationStatement extends BaseNode {
229
237
  type: "FunctionDeclarationStatement";
230
238
  target: FunctionName;
@@ -758,8 +766,12 @@ interface Binding {
758
766
  * bindings are never given a `declarationNode` from assignment
759
767
  * inference, since they're not really "defined" in this file. */
760
768
  isBuiltin?: boolean;
761
- /** True for a `const` binding reassigning it is an error. */
769
+ /** True for a binding that cannot be reassigned: a `const`, an import, or
770
+ * a function declaration. */
762
771
  isConst?: boolean;
772
+ /** Set when the binding comes from something other than `const` / `let`,
773
+ * which is also what an error about reassigning it names. */
774
+ declaredBy?: "import" | "namespace" | "function" | "type";
763
775
  }
764
776
  interface ScopeDiagnostic {
765
777
  /** the offending node (redeclaration site, or assignment target) */
@@ -774,7 +786,7 @@ interface ScopeDiagnostic {
774
786
  };
775
787
  };
776
788
  message: string;
777
- kind: "redeclare" | "const-assign";
789
+ kind: "redeclare" | "const-assign" | "type-only";
778
790
  }
779
791
  interface ScopeAnalysis {
780
792
  /** Every Identifier that appears in a variable *usage* position (i.e.
@@ -1132,7 +1144,7 @@ interface ExportedType {
1132
1144
  }
1133
1145
  /** What a module makes available to `import`. See `moduleExports`. */
1134
1146
  interface ModuleExports {
1135
- /** `export const` / `export let` / `export const function` names. */
1147
+ /** `export const` / `export let` / `export function` names. */
1136
1148
  readonly values: ReadonlyMap<string, Type>;
1137
1149
  /** `export type` names. */
1138
1150
  readonly types: ReadonlyMap<string, ExportedType>;
package/dist/index.d.ts CHANGED
@@ -99,11 +99,17 @@ interface ImportStatement extends BaseNode {
99
99
  type: "ImportStatement";
100
100
  /** `import Default from '...'` */
101
101
  defaultImport?: Identifier;
102
+ /** `import * as Module from '...'` — the module's exports as one value. */
103
+ namespaceImport?: Identifier;
104
+ /** `import type { A } from '...'`: every name it brings in is a type and
105
+ * may only be used as one — never as a value. It exists for the type
106
+ * checker alone, and leaves nothing in compiled code. */
107
+ isTypeOnly?: boolean;
102
108
  /** `import { a, b as c } from '...'` */
103
109
  specifiers: ImportSpecifier[];
104
110
  source: StringLiteral;
105
111
  }
106
- /** `export const x = 1`, `export let y = 2`, `export const function f() end` */
112
+ /** `export const x = 1`, `export let y = 2`, `export function f() end` */
107
113
  interface ExportStatement extends BaseNode {
108
114
  type: "ExportStatement";
109
115
  declaration: VariableDeclaration | FunctionDeclaration;
@@ -214,17 +220,19 @@ interface ArrayPatternElement extends BaseNode {
214
220
  value: BindingTarget;
215
221
  default?: Expression;
216
222
  }
217
- /** `const function f() ... end` / `let function f() ... end` a named,
218
- * self-referential (recursive) function binding. */
223
+ /** `function f() ... end` declares `f` in the enclosing scope, visible to
224
+ * its own body (so it can recurse). Like TypeScript's function declaration,
225
+ * the name cannot be reassigned. `function a.b() end` and `function T:m() end`
226
+ * assign to a member instead: see `FunctionDeclarationStatement`. */
219
227
  interface FunctionDeclaration extends BaseNode {
220
228
  type: "FunctionDeclaration";
221
- kind: "const" | "let";
222
229
  name: Identifier;
223
230
  func: FunctionBody;
224
231
  attributes?: string[];
225
232
  /** TS-style overload signatures preceding the implementation (`func`). */
226
233
  signatures?: FunctionSignature[];
227
234
  }
235
+ /** `function a.b() end` / `function T:m() end` — defines a member. */
228
236
  interface FunctionDeclarationStatement extends BaseNode {
229
237
  type: "FunctionDeclarationStatement";
230
238
  target: FunctionName;
@@ -758,8 +766,12 @@ interface Binding {
758
766
  * bindings are never given a `declarationNode` from assignment
759
767
  * inference, since they're not really "defined" in this file. */
760
768
  isBuiltin?: boolean;
761
- /** True for a `const` binding reassigning it is an error. */
769
+ /** True for a binding that cannot be reassigned: a `const`, an import, or
770
+ * a function declaration. */
762
771
  isConst?: boolean;
772
+ /** Set when the binding comes from something other than `const` / `let`,
773
+ * which is also what an error about reassigning it names. */
774
+ declaredBy?: "import" | "namespace" | "function" | "type";
763
775
  }
764
776
  interface ScopeDiagnostic {
765
777
  /** the offending node (redeclaration site, or assignment target) */
@@ -774,7 +786,7 @@ interface ScopeDiagnostic {
774
786
  };
775
787
  };
776
788
  message: string;
777
- kind: "redeclare" | "const-assign";
789
+ kind: "redeclare" | "const-assign" | "type-only";
778
790
  }
779
791
  interface ScopeAnalysis {
780
792
  /** Every Identifier that appears in a variable *usage* position (i.e.
@@ -1132,7 +1144,7 @@ interface ExportedType {
1132
1144
  }
1133
1145
  /** What a module makes available to `import`. See `moduleExports`. */
1134
1146
  interface ModuleExports {
1135
- /** `export const` / `export let` / `export const function` names. */
1147
+ /** `export const` / `export let` / `export function` names. */
1136
1148
  readonly values: ReadonlyMap<string, Type>;
1137
1149
  /** `export type` names. */
1138
1150
  readonly types: ReadonlyMap<string, ExportedType>;
package/dist/index.js CHANGED
@@ -796,23 +796,14 @@ var Parser = class {
796
796
  if (t.type === "Punctuator" && t.value === "@") {
797
797
  const { attributes, start } = this.parseAttributes();
798
798
  const next = this.current();
799
- if (next.type === "Keyword" && (next.value === "const" || next.value === "let")) {
800
- const stmt = this.parseVariableDeclaration();
801
- if (stmt.type === "FunctionDeclaration") {
802
- stmt.attributes = attributes;
803
- stmt.line.start = start.line.start;
804
- stmt.column.start = start.column.start;
805
- }
806
- return stmt;
807
- }
808
799
  if (next.type === "Keyword" && next.value === "function") {
809
- const stmt = this.parseFunctionDeclarationStatement();
800
+ const stmt = this.parseFunctionStatement();
810
801
  stmt.attributes = attributes;
811
802
  stmt.line.start = start.line.start;
812
803
  stmt.column.start = start.column.start;
813
804
  return stmt;
814
805
  }
815
- throw new ParseError("Expected 'function', 'const', or 'let' after attribute", next.line.start, next.column.start);
806
+ throw new ParseError("Expected 'function' after an attribute", next.line.start, next.column.start);
816
807
  }
817
808
  if (t.type === "Keyword") {
818
809
  switch (t.value) {
@@ -830,7 +821,7 @@ var Parser = class {
830
821
  case "for":
831
822
  return this.parseForStatement();
832
823
  case "function":
833
- return this.parseFunctionDeclarationStatement();
824
+ return this.parseFunctionStatement();
834
825
  case "return":
835
826
  return this.parseReturnStatement();
836
827
  case "import":
@@ -932,20 +923,30 @@ var Parser = class {
932
923
  parseImportStatement() {
933
924
  const start = this.current();
934
925
  this.advance();
926
+ const next = this.peek(1);
927
+ const isTypeOnly = this.checkIdentifierValue("type") && (next.type === "Punctuator" && next.value === "{" || next.type === "Operator" && next.value === "*" || next.type === "Identifier");
928
+ if (isTypeOnly) this.advance();
935
929
  let defaultImport;
936
930
  const specifiers = [];
937
- if (this.checkType("Identifier")) {
938
- const nameTok = this.expectIdentifier();
939
- defaultImport = { type: "Identifier", name: nameTok.value, ...spanFrom(nameTok, nameTok) };
940
- if (this.matchPunctuator(",")) {
941
- this.expectPunctuator("{");
942
- this.parseImportSpecifierList(specifiers);
943
- this.expectPunctuator("}");
931
+ let namespaceImport;
932
+ const parseBindings = () => {
933
+ if (this.checkOperator("*")) {
934
+ this.advance();
935
+ if (!this.checkKeyword("as")) this.error("Expected 'as' after 'import *'");
936
+ this.advance();
937
+ namespaceImport = this.parseIdentifier();
938
+ return;
944
939
  }
945
- } else {
946
940
  this.expectPunctuator("{");
947
941
  this.parseImportSpecifierList(specifiers);
948
942
  this.expectPunctuator("}");
943
+ };
944
+ if (this.checkType("Identifier")) {
945
+ const nameTok = this.expectIdentifier();
946
+ defaultImport = { type: "Identifier", name: nameTok.value, ...spanFrom(nameTok, nameTok) };
947
+ if (this.matchPunctuator(",")) parseBindings();
948
+ } else {
949
+ parseBindings();
949
950
  }
950
951
  if (!this.checkKeyword("from")) {
951
952
  this.error("Expected 'from' in import statement");
@@ -962,7 +963,15 @@ var Parser = class {
962
963
  raw: sourceTok.raw,
963
964
  ...spanFrom(sourceTok, sourceTok)
964
965
  };
965
- return { type: "ImportStatement", defaultImport, specifiers, source, ...spanFrom(start, this.previous()) };
966
+ return {
967
+ type: "ImportStatement",
968
+ defaultImport,
969
+ namespaceImport,
970
+ specifiers,
971
+ source,
972
+ isTypeOnly: isTypeOnly || void 0,
973
+ ...spanFrom(start, this.previous())
974
+ };
966
975
  }
967
976
  parseImportSpecifierList(out) {
968
977
  if (this.checkPunctuator("}")) return;
@@ -998,7 +1007,7 @@ var Parser = class {
998
1007
  ...spanFrom(sourceTok, sourceTok)
999
1008
  };
1000
1009
  }
1001
- // `export const ...` / `export let ...` / `export const function ...` /
1010
+ // `export const ...` / `export let ...` / `export function ...` /
1002
1011
  // `export type ...` / `export default <expr>`
1003
1012
  parseExportStatement() {
1004
1013
  const start = this.current();
@@ -1016,6 +1025,11 @@ var Parser = class {
1016
1025
  const declaration = this.parseVariableDeclaration();
1017
1026
  return { type: "ExportStatement", declaration, ...spanFrom(start, this.previous()) };
1018
1027
  }
1028
+ if (this.checkKeyword("function")) {
1029
+ const declaration = this.parseFunctionStatement();
1030
+ if (declaration.type !== "FunctionDeclaration") this.error("An exported function needs a plain name: 'export function name()'");
1031
+ return { type: "ExportStatement", declaration, ...spanFrom(start, this.previous()) };
1032
+ }
1019
1033
  if (this.checkPunctuator("{")) {
1020
1034
  this.advance();
1021
1035
  const specifiers = [];
@@ -1039,15 +1053,15 @@ var Parser = class {
1039
1053
  const source = this.parseModuleSource();
1040
1054
  return { type: "ExportAllStatement", source, ...spanFrom(start, this.previous()) };
1041
1055
  }
1042
- this.error("Expected 'const', 'let', 'type', 'default', '{' or '*' after 'export'");
1056
+ this.error("Expected 'const', 'let', 'function', 'type', 'default', '{' or '*' after 'export'");
1043
1057
  }
1044
- // `const x = ...` / `let x, y = ...` / `const function f() ... end`.
1058
+ // `const x = ...` / `let x, y = ...`.
1045
1059
  // luaut has no `local` — `const` bindings are immutable, `let` mutable.
1046
1060
  parseVariableDeclaration() {
1047
1061
  const start = this.current();
1048
1062
  const kind = this.advance().value;
1049
- if (this.matchKeyword("function")) {
1050
- return this.parseFunctionDeclarationRest(start, kind);
1063
+ if (this.checkKeyword("function")) {
1064
+ this.error(`A function is declared as 'function name()'; '${kind}' does not apply to functions`);
1051
1065
  }
1052
1066
  const names = [this.parseBindingTarget(true)];
1053
1067
  while (this.matchPunctuator(",")) {
@@ -1061,31 +1075,6 @@ var Parser = class {
1061
1075
  }
1062
1076
  return { type: "VariableDeclaration", kind, names, init, ...spanFrom(start, this.previous()) };
1063
1077
  }
1064
- /** `const/let function` — `function` already consumed. Collects TS-style
1065
- * overload signatures. */
1066
- parseFunctionDeclarationRest(start, kind) {
1067
- const name = this.parseIdentifier();
1068
- const signatures = [];
1069
- while (true) {
1070
- const head = this.parseFunctionHead();
1071
- if (this.isOverloadContinuation(name.name, kind)) {
1072
- signatures.push(this.headToSignature(head));
1073
- this.advance();
1074
- this.expectKeyword("function");
1075
- this.parseIdentifier();
1076
- continue;
1077
- }
1078
- const func = this.headToBody(head);
1079
- return {
1080
- type: "FunctionDeclaration",
1081
- kind,
1082
- name,
1083
- func,
1084
- signatures: signatures.length ? signatures : void 0,
1085
- ...spanFrom(start, this.previous())
1086
- };
1087
- }
1088
- }
1089
1078
  parseIfStatement() {
1090
1079
  const start = this.current();
1091
1080
  this.expectKeyword("if");
@@ -1175,7 +1164,9 @@ var Parser = class {
1175
1164
  ...spanFrom(start, this.previous())
1176
1165
  };
1177
1166
  }
1178
- parseFunctionDeclarationStatement() {
1167
+ /** `function name() end` declares `name`; `function a.b() end` and
1168
+ * `function T:m() end` define a member. */
1169
+ parseFunctionStatement() {
1179
1170
  const start = this.current();
1180
1171
  this.expectKeyword("function");
1181
1172
  const target = this.parseFunctionName();
@@ -1191,6 +1182,15 @@ var Parser = class {
1191
1182
  continue;
1192
1183
  }
1193
1184
  const func = this.headToBody(head);
1185
+ if (simpleName !== void 0) {
1186
+ return {
1187
+ type: "FunctionDeclaration",
1188
+ name: target.base,
1189
+ func,
1190
+ signatures: signatures.length ? signatures : void 0,
1191
+ ...spanFrom(start, this.previous())
1192
+ };
1193
+ }
1194
1194
  if (isMethod) {
1195
1195
  func.params.unshift({ type: "FunctionParameter", name: "self", ...spanFrom(target, target) });
1196
1196
  func.isMethod = true;
@@ -1207,12 +1207,8 @@ var Parser = class {
1207
1207
  }
1208
1208
  /** After a bodyless function head, is the next token the start of another
1209
1209
  * declaration for the same simple `name` (making the head an overload
1210
- * signature rather than an implementation)? `kind` is set for a
1211
- * `const/let function` group, undefined for a bare `function` group. */
1212
- isOverloadContinuation(name, kind) {
1213
- if (kind) {
1214
- return this.checkKeyword(kind) && this.peek(1).type === "Keyword" && this.peek(1).value === "function" && this.peek(2).type === "Identifier" && this.peek(2).value === name;
1215
- }
1210
+ * signature rather than an implementation)? */
1211
+ isOverloadContinuation(name) {
1216
1212
  return this.checkKeyword("function") && this.peek(1).type === "Identifier" && this.peek(1).value === name;
1217
1213
  }
1218
1214
  parseFunctionName() {
@@ -2519,7 +2515,7 @@ var Analyzer = class {
2519
2515
  };
2520
2516
  }
2521
2517
  // ---------------- declaration / resolution primitives ----------------
2522
- declare(scope, name, kind, node, isConst = false) {
2518
+ declare(scope, name, kind, node, isConst = false, declaredBy) {
2523
2519
  if (scope.declarations.has(name) && scope !== this.globalScope) {
2524
2520
  this.diagnostics.push({
2525
2521
  node,
@@ -2528,7 +2524,7 @@ var Analyzer = class {
2528
2524
  });
2529
2525
  }
2530
2526
  const id = this.nextId++;
2531
- this.bindings.set(id, { id, name, kind, declarationNode: node, references: [], isConst });
2527
+ this.bindings.set(id, { id, name, kind, declarationNode: node, references: [], isConst, declaredBy });
2532
2528
  scope.declarations.set(name, id);
2533
2529
  return id;
2534
2530
  }
@@ -2562,6 +2558,19 @@ var Analyzer = class {
2562
2558
  const id = this.resolve(scope, identifier.name);
2563
2559
  this.bindingOf.set(identifier, id);
2564
2560
  this.bindings.get(id).references.push(identifier);
2561
+ if (this.typeQueryDepth === 0) this.checkTypeOnly(id, identifier);
2562
+ }
2563
+ /** Inside `typeof x` in a type, where a type-only import may be named. */
2564
+ typeQueryDepth = 0;
2565
+ /** A name from `import type` used as a value. */
2566
+ checkTypeOnly(id, node) {
2567
+ const b = this.bindings.get(id);
2568
+ if (b.declaredBy !== "type") return;
2569
+ this.diagnostics.push({
2570
+ node,
2571
+ message: `'${b.name}' is imported with 'import type' and can only be used as a type`,
2572
+ kind: "type-only"
2573
+ });
2565
2574
  }
2566
2575
  /** For assignment-like targets (`x = ...`, `function foo() end`): if
2567
2576
  * this resolved to a global with no declaration site yet, treat this
@@ -2578,14 +2587,34 @@ var Analyzer = class {
2578
2587
  this.bindingOf.set(identifier, id);
2579
2588
  this.bindings.get(id).references.push(identifier);
2580
2589
  this.recordPossibleGlobalDefinition(id, identifier);
2590
+ this.checkTypeOnly(id, identifier);
2581
2591
  this.checkConstAssign(id, identifier);
2582
2592
  }
2593
+ /** `Module.x = 1` through `import * as Module`: a module's exports belong
2594
+ * to it and are read-only, as in ES modules. Deeper writes (`Module.x.y`)
2595
+ * change the value, not the module, and are fine. */
2596
+ checkModuleWrite(target) {
2597
+ if (target.type !== "MemberExpression" && target.type !== "IndexExpression") return;
2598
+ if (target.object.type !== "Identifier") return;
2599
+ const id = this.bindingOf.get(target.object);
2600
+ if (id !== void 0 && this.bindings.get(id).declaredBy === "namespace") {
2601
+ this.moduleWriteError(target.object.name, target);
2602
+ }
2603
+ }
2604
+ moduleWriteError(name, node) {
2605
+ this.diagnostics.push({
2606
+ node,
2607
+ message: `Cannot assign to a member of '${name}' \u2014 a module's exports are read-only`,
2608
+ kind: "const-assign"
2609
+ });
2610
+ }
2583
2611
  checkConstAssign(id, node) {
2584
2612
  const b = this.bindings.get(id);
2613
+ if (b.declaredBy === "type") return;
2585
2614
  if (b.isConst) {
2586
2615
  this.diagnostics.push({
2587
2616
  node,
2588
- message: `Cannot assign to '${b.name}' \u2014 it is a const`,
2617
+ message: `Cannot assign to '${b.name}' \u2014 it is ${b.declaredBy === "import" || b.declaredBy === "namespace" ? "an import" : b.declaredBy === "function" ? "a function" : "a const"}`,
2589
2618
  kind: "const-assign"
2590
2619
  });
2591
2620
  }
@@ -2626,6 +2655,7 @@ var Analyzer = class {
2626
2655
  const id = this.resolve(scope, t.name);
2627
2656
  this.bindingOf.set(t, id);
2628
2657
  this.recordPossibleGlobalDefinition(id, t);
2658
+ this.checkTypeOnly(id, t);
2629
2659
  this.checkConstAssign(id, t);
2630
2660
  return;
2631
2661
  }
@@ -2669,7 +2699,7 @@ var Analyzer = class {
2669
2699
  return;
2670
2700
  }
2671
2701
  case "FunctionDeclaration": {
2672
- this.declare(scope, stmt.name.name, "local", stmt.name, stmt.kind === "const");
2702
+ this.declare(scope, stmt.name.name, "local", stmt.name, true, "function");
2673
2703
  for (const signature of stmt.signatures ?? []) this.visitSignature(signature, scope);
2674
2704
  this.visitFunctionBody(stmt.func, scope);
2675
2705
  return;
@@ -2679,6 +2709,11 @@ var Analyzer = class {
2679
2709
  this.referenceAsAssignmentTarget(scope, stmt.target.base);
2680
2710
  } else {
2681
2711
  this.reference(scope, stmt.target.base);
2712
+ const id = this.bindingOf.get(stmt.target.base);
2713
+ const depth = stmt.target.path.length + (stmt.target.method ? 1 : 0);
2714
+ if (id !== void 0 && depth === 1 && this.bindings.get(id).declaredBy === "namespace") {
2715
+ this.moduleWriteError(stmt.target.base.name, stmt.target);
2716
+ }
2682
2717
  }
2683
2718
  for (const signature of stmt.signatures ?? []) this.visitSignature(signature, scope);
2684
2719
  this.visitFunctionBody(stmt.func, scope, stmt.isMethod);
@@ -2693,6 +2728,7 @@ var Analyzer = class {
2693
2728
  this.assignPattern(scope, target);
2694
2729
  } else {
2695
2730
  this.visitExpression(target, scope);
2731
+ this.checkModuleWrite(target);
2696
2732
  }
2697
2733
  }
2698
2734
  return;
@@ -2705,6 +2741,7 @@ var Analyzer = class {
2705
2741
  if (id !== void 0) this.checkConstAssign(id, stmt.target);
2706
2742
  } else {
2707
2743
  this.visitExpression(stmt.target, scope);
2744
+ this.checkModuleWrite(stmt.target);
2708
2745
  }
2709
2746
  return;
2710
2747
  }
@@ -2766,11 +2803,15 @@ var Analyzer = class {
2766
2803
  this.visitType(stmt.definition, scope);
2767
2804
  return;
2768
2805
  case "ImportStatement": {
2806
+ const typeOnly = stmt.isTypeOnly ? "type" : void 0;
2769
2807
  if (stmt.defaultImport) {
2770
- this.declare(scope, stmt.defaultImport.name, "local", stmt.defaultImport);
2808
+ this.declare(scope, stmt.defaultImport.name, "local", stmt.defaultImport, true, typeOnly ?? "import");
2809
+ }
2810
+ if (stmt.namespaceImport) {
2811
+ this.declare(scope, stmt.namespaceImport.name, "local", stmt.namespaceImport, true, typeOnly ?? "namespace");
2771
2812
  }
2772
2813
  for (const spec of stmt.specifiers) {
2773
- this.declare(scope, spec.local.name, "local", spec.local);
2814
+ this.declare(scope, spec.local.name, "local", spec.local, true, typeOnly ?? "import");
2774
2815
  }
2775
2816
  return;
2776
2817
  }
@@ -2829,7 +2870,12 @@ var Analyzer = class {
2829
2870
  return;
2830
2871
  }
2831
2872
  if (value.type === "TypeofTypeNode") {
2832
- this.visitExpression(value.expression, scope);
2873
+ this.typeQueryDepth++;
2874
+ try {
2875
+ this.visitExpression(value.expression, scope);
2876
+ } finally {
2877
+ this.typeQueryDepth--;
2878
+ }
2833
2879
  return;
2834
2880
  }
2835
2881
  for (const key of Object.keys(value)) {
@@ -3988,6 +4034,13 @@ var TypeAnalyzer = class {
3988
4034
  if (stmt.type !== "ImportStatement") continue;
3989
4035
  const exports = this.moduleFor(stmt.source.value);
3990
4036
  if (!exports) continue;
4037
+ if (stmt.namespaceImport) {
4038
+ for (const [name, exported] of exports.types) {
4039
+ const qualified = `${stmt.namespaceImport.name}.${name}`;
4040
+ this.importedTypes.set(qualified, exported);
4041
+ this.aliases.set(qualified, exported.type);
4042
+ }
4043
+ }
3991
4044
  for (const s of stmt.specifiers) {
3992
4045
  const exported = exports.types.get(s.imported.name);
3993
4046
  if (exported) {
@@ -4182,6 +4235,16 @@ var TypeAnalyzer = class {
4182
4235
  });
4183
4236
  return subst;
4184
4237
  }
4238
+ /** An imported type, with its type arguments applied. */
4239
+ importedType(imported, typeArguments) {
4240
+ if (!imported.params.length) return imported.type;
4241
+ const subst = /* @__PURE__ */ new Map();
4242
+ imported.params.forEach((name, i) => {
4243
+ const arg = typeArguments[i];
4244
+ subst.set(name, arg ? this.resolveType(arg) : unknownType);
4245
+ });
4246
+ return this.reduceType(substitute(imported.type, subst));
4247
+ }
4185
4248
  // --------------------------------------------------------
4186
4249
  // TypeNode -> Type
4187
4250
  // --------------------------------------------------------
@@ -4232,17 +4295,11 @@ var TypeAnalyzer = class {
4232
4295
  });
4233
4296
  }
4234
4297
  const imported = this.importedTypes.get(node.base);
4235
- if (imported) {
4236
- if (!imported.params.length) return imported.type;
4237
- const subst = /* @__PURE__ */ new Map();
4238
- imported.params.forEach((name2, i) => {
4239
- const arg = node.typeArguments[i];
4240
- subst.set(name2, arg ? this.resolveType(arg) : unknownType);
4241
- });
4242
- return this.reduceType(substitute(imported.type, subst));
4243
- }
4298
+ if (imported) return this.importedType(imported, node.typeArguments);
4244
4299
  const lib = this.options.libTypes?.[node.base];
4245
4300
  if (lib) return lib;
4301
+ } else if (this.importedTypes.has(name)) {
4302
+ return this.importedType(this.importedTypes.get(name), node.typeArguments);
4246
4303
  } else if (this.aliasDefs.has(name)) {
4247
4304
  return this.expand({
4248
4305
  kind: "genericRef",
@@ -4846,6 +4903,14 @@ var TypeAnalyzer = class {
4846
4903
  };
4847
4904
  if (resolving && !exports) report(stmt.source, `Cannot find module '${specifier}'`);
4848
4905
  const usable = exports && !exports.partial ? exports : void 0;
4906
+ if (stmt.namespaceImport) {
4907
+ const id = this.bindingIdByName(stmt.namespaceImport.name, stmt.namespaceImport);
4908
+ if (id !== void 0) {
4909
+ const members = [...usable?.values ?? []].map(([name, type]) => [name, { type, optional: false, readonly: true }]);
4910
+ if (usable?.default) members.push(["default", { type: usable.default, optional: false, readonly: true }]);
4911
+ this.bindingType.set(id, usable ? objectType(members) : anyType);
4912
+ }
4913
+ }
4849
4914
  if (stmt.defaultImport) {
4850
4915
  if (usable && usable.default === void 0) {
4851
4916
  report(stmt.defaultImport, `Module '${specifier}' has no default export`);
@@ -5003,7 +5068,10 @@ var TypeAnalyzer = class {
5003
5068
  applyContext(expr, expected) {
5004
5069
  let e = expr;
5005
5070
  while (e.type === "ParenthesizedExpression") e = e.expression;
5006
- if (e.type !== "FunctionExpression" || !expected) return;
5071
+ if (!expected) return;
5072
+ if (e.type === "ArrayExpression") return this.applyArrayContext(e, expected);
5073
+ if (e.type === "TableExpression") return this.applyTableContext(e, expected);
5074
+ if (e.type !== "FunctionExpression") return;
5007
5075
  const members = expected.kind === "union" ? expected.types : [expected];
5008
5076
  const signatures = members.flatMap((m) => this.overloadsOf(this.expand(m)));
5009
5077
  if (!signatures.length) return;
@@ -5019,6 +5087,46 @@ var TypeAnalyzer = class {
5019
5087
  this.contextualParams.set(p, containsTypeParam(t) ? anyType : t);
5020
5088
  });
5021
5089
  }
5090
+ /** What an array literal is expected to be: an empty one takes that type
5091
+ * outright — `let queue: thread[] = []` is a `thread[]`, as in TypeScript
5092
+ * — and the elements of any other get the element type as their own
5093
+ * context. */
5094
+ contextualArrays = /* @__PURE__ */ new WeakMap();
5095
+ applyArrayContext(e, expected) {
5096
+ const target = this.expectedMembers(expected).find((m) => m.kind === "array" || m.kind === "tuple");
5097
+ if (!target) return;
5098
+ if (!e.elements.length) {
5099
+ if (!containsTypeParam(target)) this.contextualArrays.set(e, target);
5100
+ return;
5101
+ }
5102
+ e.elements.forEach((element, i) => {
5103
+ if (element.type === "SpreadElement") return;
5104
+ const elementType = target.kind === "array" ? target.element : target.elements[i];
5105
+ this.applyContext(element, elementType);
5106
+ });
5107
+ }
5108
+ /** `{ list: [] }` where `{ list: thread[] }` is expected: each field's
5109
+ * value gets its property's type as context. */
5110
+ applyTableContext(e, expected) {
5111
+ const objects = this.expectedMembers(expected).filter((m) => m.kind === "object");
5112
+ if (!objects.length) return;
5113
+ for (const field of e.fields) {
5114
+ if (field.type !== "TableFieldNamed") continue;
5115
+ const key = field.key.type === "Identifier" ? field.key.name : field.key.value;
5116
+ const types = objects.flatMap((o) => {
5117
+ const property = o.properties.get(key);
5118
+ return property ? [property.type] : o.indexer ? [o.indexer.value] : [];
5119
+ });
5120
+ if (types.length) this.applyContext(field.value, union(types));
5121
+ }
5122
+ }
5123
+ /** The members of an expected type worth matching a literal against:
5124
+ * aliases seen through, `nil` left out. */
5125
+ expectedMembers(expected) {
5126
+ const t = this.expand(expected);
5127
+ const members = t.kind === "union" ? t.types : [t];
5128
+ return members.map((m) => this.expand(m)).filter((m) => !(m.kind === "primitive" && m.name === "nil"));
5129
+ }
5022
5130
  /** The parameter type each written argument lands on, across `fns`. */
5023
5131
  expectedArguments(written, fns, selfOf) {
5024
5132
  return written.map((_, j) => {
@@ -5593,8 +5701,9 @@ var TypeAnalyzer = class {
5593
5701
  return this.resolveType(expr.typeAnnotation);
5594
5702
  }
5595
5703
  case "SatisfiesExpression": {
5596
- const actual = this.infer(expr.expression, env);
5597
5704
  const declared = this.resolveType(expr.typeAnnotation);
5705
+ this.applyContext(expr.expression, declared);
5706
+ const actual = this.infer(expr.expression, env);
5598
5707
  if (this.emitDiagnostics && declared.kind !== "any" && !this.fitsAnnotation(expr.expression, declared, actual, env)) {
5599
5708
  this.diagnostics.push({
5600
5709
  node: expr,
@@ -5722,6 +5831,8 @@ var TypeAnalyzer = class {
5722
5831
  }
5723
5832
  }
5724
5833
  inferArray(expr, env, asConst) {
5834
+ const contextual = this.contextualArrays.get(expr);
5835
+ if (contextual && !asConst) return contextual;
5725
5836
  const elems = [];
5726
5837
  let hadSpread = false;
5727
5838
  for (const el of expr.elements) {
@@ -6375,13 +6486,13 @@ function resolveTypeLibraries(config, host = nodeHost) {
6375
6486
  else problems.push({ file: config.path, message: `'${entry}' has no ${ENTRY_FILE} (or 'luaut.types' in its package.json)`, ...entryPosition(config, entry) });
6376
6487
  continue;
6377
6488
  }
6378
- const names = entry.startsWith("@") || entry.includes("/") ? [entry] : [`@luaut/${entry}`, entry];
6379
- const found = names.map((name) => findPackage(name, config.directory, host)).find(Boolean);
6489
+ const name = entry.startsWith("@luaut/") ? entry : `@luaut/${entry}`;
6490
+ const found = findPackage(name, config.directory, host);
6380
6491
  if (found) addPackage(found.directory, found.file, /* @__PURE__ */ new Set());
6381
6492
  else {
6382
6493
  problems.push({
6383
6494
  file: config.path,
6384
- message: `Cannot find type library '${entry}'. Install it with: npm i -D ${names[0]}`,
6495
+ message: `Cannot find type library '${name}'. Install it with: npm i -D ${name}`,
6385
6496
  ...entryPosition(config, entry)
6386
6497
  });
6387
6498
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luaut-parser",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "luaut parser for roblox",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",