storm-lua-minify 0.3.0 → 0.9.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.
Files changed (43) hide show
  1. package/README.md +119 -41
  2. package/dist/aggregateSpecialization.js +406 -0
  3. package/dist/ast2lua.js +156 -68
  4. package/dist/astWalk.js +162 -0
  5. package/dist/callGraph.js +372 -0
  6. package/dist/cli.js +53 -58
  7. package/dist/cliOptions.js +36 -0
  8. package/dist/cliProgress.js +87 -0
  9. package/dist/config.js +73 -0
  10. package/dist/constantFold.js +798 -0
  11. package/dist/controlFlow.js +266 -0
  12. package/dist/functionRewrites.js +580 -0
  13. package/dist/generatedAst.js +108 -0
  14. package/dist/generatedNode.js +23 -0
  15. package/dist/interproceduralAnalysis.js +842 -0
  16. package/dist/interproceduralConstants.js +120 -0
  17. package/dist/luaString.js +157 -0
  18. package/dist/minifier.js +1178 -44
  19. package/dist/optimizerAnalysis.js +43 -0
  20. package/dist/optimizerDiagnostics.js +65 -0
  21. package/dist/optimizerFacts.js +529 -0
  22. package/dist/optimizerPass.js +96 -0
  23. package/dist/optimizerTransaction.js +56 -0
  24. package/dist/optimizerValueDomain.js +180 -0
  25. package/dist/options.js +233 -0
  26. package/dist/progress.js +2 -0
  27. package/dist/removeUnused.js +145 -0
  28. package/dist/renamer.js +223 -54
  29. package/dist/resolver.js +28 -11
  30. package/dist/runtimeEnvironment.js +105 -0
  31. package/dist/sourceMetadata.js +314 -0
  32. package/dist/statementDataflow.js +259 -0
  33. package/dist/statementScheduler.js +595 -0
  34. package/dist/symbolLiveness.js +92 -0
  35. package/dist/tableEffects.js +356 -0
  36. package/dist/transform.js +10 -371
  37. package/dist/valueFlow.js +409 -0
  38. package/dist/wholeProgramExports.js +646 -0
  39. package/dist/wholeProgramFieldRenames.js +583 -0
  40. package/dist/wholeProgramFields.js +672 -0
  41. package/dist/wholeProgramObjects.js +783 -0
  42. package/package.json +11 -2
  43. package/dist/index.js +0 -27
package/dist/ast2lua.js CHANGED
@@ -8,6 +8,7 @@ const source_map_1 = require("source-map");
8
8
  const linker_1 = require("./linker");
9
9
  const keywordLocator_1 = require("./keywordLocator");
10
10
  const transform_1 = require("./transform");
11
+ const sourceMetadata_1 = require("./sourceMetadata");
11
12
  const PRECEDENCE = {
12
13
  or: 1,
13
14
  and: 2,
@@ -137,6 +138,10 @@ function isKeyword(id) {
137
138
  }
138
139
  return false;
139
140
  }
141
+ // 末尾が16進整数リテラル(0x/0Xの後に16進数字が1つ以上続く)かどうか。
142
+ // a〜fは識別子の文字集合とも重なるため、isNeedSeparatorのalpha分岐だけでは
143
+ // 区別できない(#53)。
144
+ const HEX_INT_LITERAL_TAIL = /0[xX][0-9a-fA-F]+$/;
140
145
  function isNeedSeparator(a, b) {
141
146
  const lastCharA = a.slice(-1);
142
147
  const firstCharB = b.charAt(0);
@@ -152,11 +157,15 @@ function isNeedSeparator(a, b) {
152
157
  // e.g. `local a` + `local b`
153
158
  return true;
154
159
  }
155
- else {
156
- // e.g. `not` + `(2>3 or 3<2)`
157
- // e.g. `x` + `^`
158
- return false;
160
+ if (firstCharB == "." && HEX_INT_LITERAL_TAIL.test(a)) {
161
+ // e.g. `0xff` + `..` : 16進の浮動小数点(`0xff.8`)も読めてしまうため、
162
+ // 続く`.`をそのまま出すと`0xff.`まで数値として読まれ、10進では起きない
163
+ // 区切り落ち(malformed number)になる(#53)。
164
+ return true;
159
165
  }
166
+ // e.g. `not` + `(2>3 or 3<2)`
167
+ // e.g. `x` + `^`
168
+ return false;
160
169
  }
161
170
  if (regexDigits.test(lastCharA)) {
162
171
  if (firstCharB == "(" ||
@@ -184,7 +193,18 @@ function isNeedSeparator(a, b) {
184
193
  }
185
194
  return false;
186
195
  }
187
- function addWithSeparator(val, adding, separator = " ") {
196
+ const requiredWhitespaceByNode = new WeakMap();
197
+ function whitespaceOf(...values) {
198
+ for (const value of values) {
199
+ if (value instanceof source_map_1.SourceNode) {
200
+ const whitespace = requiredWhitespaceByNode.get(value);
201
+ if (whitespace)
202
+ return whitespace;
203
+ }
204
+ }
205
+ return "\n";
206
+ }
207
+ function addWithSeparator(val, adding, separator = whitespaceOf(val)) {
188
208
  if (isNeedSeparator(val.toString(), wrapArray(adding)
189
209
  .map((p) => p.toString())
190
210
  .join())) {
@@ -193,7 +213,7 @@ function addWithSeparator(val, adding, separator = " ") {
193
213
  val.add(adding);
194
214
  return val;
195
215
  }
196
- function prependWithSeparator(val, prepending, separator = " ") {
216
+ function prependWithSeparator(val, prepending, separator = whitespaceOf(val)) {
197
217
  if (isNeedSeparator(wrapArray(prepending)
198
218
  .map((p) => p.toString())
199
219
  .join(), val.toString())) {
@@ -202,7 +222,7 @@ function prependWithSeparator(val, prepending, separator = " ") {
202
222
  val.prepend(prepending);
203
223
  return val;
204
224
  }
205
- function insertSeparator(a, b, separator = " ") {
225
+ function insertSeparator(a, b, separator = whitespaceOf(a, b)) {
206
226
  return isNeedSeparator(a.toString(), b.toString()) ? separator : undefined;
207
227
  }
208
228
  class MinifyFile {
@@ -211,28 +231,29 @@ class MinifyFile {
211
231
  ast;
212
232
  minifier;
213
233
  mode;
234
+ requiredWhitespace;
214
235
  constructor(fileName, moduleName, ast, minifier, mode) {
215
236
  this.fileName = fileName;
216
237
  this.moduleName = moduleName;
217
238
  this.ast = ast;
218
239
  this.minifier = minifier;
219
240
  this.mode = mode;
241
+ this.requiredWhitespace = mode.requiredWhitespace ?? "\n";
220
242
  }
221
- parse(noComment) {
243
+ /** 合成文同士の境界も、通常の文リストと同じ区切り判定で出力する。 */
244
+ printGeneratedStatements(statements) {
245
+ return this.formatStatementList(statements);
246
+ }
247
+ parse() {
222
248
  const body = this.formatStatementList(this.ast.body);
223
- if (!noComment && this.ast.comments) {
224
- const comments = this.ast.comments;
225
- comments
226
- .reverse()
227
- .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
228
- .forEach((comment) => {
229
- body.prepend([this.sourceNodeHelper(comment, comment.raw), "\n"]);
230
- });
231
- return body;
232
- }
233
- else {
234
- return body;
235
- }
249
+ this.minifier
250
+ .getSourceMetadata(this.moduleName)
251
+ .afterModuleComments()
252
+ .filter(sourceMetadata_1.isPreservedComment)
253
+ .forEach((comment) => {
254
+ body.add(["\n", this.sourceNodeHelper(comment, comment.raw)]);
255
+ });
256
+ return body;
236
257
  }
237
258
  /**
238
259
  * モジュール本体の最後の文が「単一の式を返すreturn文」であるときに限り、
@@ -240,7 +261,7 @@ class MinifyFile {
240
261
  * 展開したい呼び出し側(#29のレビュー対応)が利用する。
241
262
  * 該当しない場合はundefinedを返し、呼び出し側はIIFE方式へフォールバックする。
242
263
  */
243
- parseAsStatementsAndFinalExpression(noComment) {
264
+ parseAsStatementsAndFinalExpression() {
244
265
  const body = this.ast.body;
245
266
  const last = body[body.length - 1];
246
267
  if (!last ||
@@ -249,18 +270,20 @@ class MinifyFile {
249
270
  return undefined;
250
271
  }
251
272
  const statements = this.formatStatementList(body.slice(0, -1));
252
- if (!noComment && this.ast.comments) {
253
- this.ast.comments
254
- .slice()
255
- .reverse()
256
- .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
257
- .forEach((comment) => {
258
- statements.prepend([
259
- this.sourceNodeHelper(comment, comment.raw),
260
- "\n",
261
- ]);
262
- });
263
- }
273
+ const metadata = this.minifier.getSourceMetadata(this.moduleName);
274
+ [
275
+ ...metadata.beforeOf(last),
276
+ ...metadata.trailingOf(last),
277
+ ...metadata.afterModuleComments(),
278
+ ]
279
+ .filter(sourceMetadata_1.isPreservedComment)
280
+ .forEach((comment) => {
281
+ statements.add([
282
+ "\n",
283
+ this.sourceNodeHelper(comment, comment.raw),
284
+ "\n",
285
+ ]);
286
+ });
264
287
  const finalExpression = this.formatExpression(last.arguments[0]);
265
288
  return { statements, finalExpression };
266
289
  }
@@ -269,7 +292,9 @@ class MinifyFile {
269
292
  const column = node?.loc?.start.column;
270
293
  // this.fileNameは常にこのMinifyFileインスタンスが担当するモジュール自身の
271
294
  // ファイル名(Linkパスで解決済み)なので、ここで出力するノードの由来ファイルとして正しい。
272
- return new source_map_1.SourceNode(line == undefined ? null : line, column == undefined ? null : column, this.fileName, chuncks, name);
295
+ const sourceNode = new source_map_1.SourceNode(line == undefined ? null : line, column == undefined ? null : column, this.fileName, chuncks, name);
296
+ requiredWhitespaceByNode.set(sourceNode, this.requiredWhitespace);
297
+ return sourceNode;
273
298
  }
274
299
  _keywordLocator;
275
300
  /**
@@ -296,6 +321,9 @@ class MinifyFile {
296
321
  * (入れ子の`if...then`等)を誤って拾うことはない。
297
322
  */
298
323
  keywordAfter(anchor, value) {
324
+ if (!anchor.loc?.end) {
325
+ return this.sourceNodeHelper(undefined, value);
326
+ }
299
327
  return this.keywordFrom(anchor.loc?.end, value);
300
328
  }
301
329
  keywordFrom(from, value) {
@@ -314,8 +342,29 @@ class MinifyFile {
314
342
  }
315
343
  formatStatementList(body) {
316
344
  const result = this.sourceNodeHelper(undefined, []);
345
+ const metadata = this.minifier.getSourceMetadata(this.moduleName);
317
346
  wrapArray(body).forEach((statement) => {
318
- addWithSeparator(result, this.formatStatement(statement), "\n");
347
+ const statementNode = this.sourceNodeHelper(undefined, []);
348
+ const sourceStatement = statement.type === "ModuleSplice" ? undefined : statement;
349
+ (sourceStatement ? metadata.beforeOf(sourceStatement) : [])
350
+ .filter(sourceMetadata_1.isPreservedComment)
351
+ .forEach((comment) => {
352
+ statementNode.add([
353
+ this.sourceNodeHelper(comment, comment.raw),
354
+ "\n",
355
+ ]);
356
+ });
357
+ statementNode.add(this.formatStatement(statement));
358
+ (sourceStatement ? metadata.trailingOf(sourceStatement) : [])
359
+ .filter(sourceMetadata_1.isPreservedComment)
360
+ .forEach((comment) => {
361
+ statementNode.add([
362
+ this.requiredWhitespace,
363
+ this.sourceNodeHelper(comment, comment.raw),
364
+ "\n",
365
+ ]);
366
+ });
367
+ addWithSeparator(result, statementNode, "\n");
319
368
  });
320
369
  return result;
321
370
  }
@@ -330,7 +379,7 @@ class MinifyFile {
330
379
  * (呼び出し側は従来のIIFE方式にフォールバックする)。
331
380
  */
332
381
  trySpliceRequireStatement(statement) {
333
- if (this.mode.moduleLikeLua) {
382
+ if (this.mode.requireWrapper) {
334
383
  return undefined;
335
384
  }
336
385
  if (statement.variables.length !== 1 || statement.init.length !== 1) {
@@ -351,7 +400,10 @@ class MinifyFile {
351
400
  : this.formatExpression(target);
352
401
  const result = this.sourceNodeHelper(statement, []);
353
402
  addWithSeparator(result, spliced.statements);
354
- addWithSeparator(result, isLocal ? ["local ", targetNode] : [targetNode]);
403
+ if (isLocal) {
404
+ addWithSeparator(result, "local");
405
+ }
406
+ addWithSeparator(result, targetNode);
355
407
  addWithSeparator(result, "=");
356
408
  addWithSeparator(result, spliced.finalExpression);
357
409
  return result;
@@ -362,16 +414,26 @@ class MinifyFile {
362
414
  * 近い、LifeBoat Modeでの一般的な使い方に合わせるための対応。#29のレビュー対応)。
363
415
  */
364
416
  tryInlineBareRequireStatement(expr) {
365
- if (this.mode.moduleLikeLua) {
417
+ if (this.mode.requireWrapper) {
366
418
  return undefined;
367
419
  }
368
420
  const moduleRef = this.matchModuleCallExpression(expr);
369
421
  if (!moduleRef || moduleRef.kind !== "require") {
370
422
  return undefined;
371
423
  }
424
+ // 戻り値を使わないrequireでは、依存モジュール末尾のreturn式も不要になる。
425
+ // return文ごと展開すると呼び出し元を途中でreturnし、後続文がある場合は
426
+ // 構文上も不正になるため、分離可能なら副作用を持つ先行文だけを出力する。
427
+ const spliced = this.minifier.splitModuleForStatementSplice(moduleRef.moduleName);
428
+ if (spliced) {
429
+ return spliced.statements;
430
+ }
372
431
  return this.minifier.printModuleInline(moduleRef.moduleName);
373
432
  }
374
433
  formatStatement(statement) {
434
+ if (statement.type === "ModuleSplice") {
435
+ return this.minifier.printModuleInline(statement.moduleName);
436
+ }
375
437
  if (statement.type == "AssignmentStatement" ||
376
438
  statement.type == "LocalStatement") {
377
439
  const spliced = this.trySpliceRequireStatement(statement);
@@ -396,10 +458,8 @@ class MinifyFile {
396
458
  const variables = statement.variables
397
459
  .map((variable) => [this.formatExpression(variable), ","])
398
460
  .flat();
399
- const result = this.sourceNodeHelper(statement, [
400
- "local ",
401
- this.sourceNodeHelper(undefined, variables.slice(0, -1)),
402
- ]);
461
+ const result = this.sourceNodeHelper(statement, "local");
462
+ addWithSeparator(result, this.sourceNodeHelper(undefined, variables.slice(0, -1)));
403
463
  if (statement.init.length) {
404
464
  const inits = statement.init
405
465
  .map((init) => [this.formatExpression(init), ","])
@@ -480,7 +540,10 @@ class MinifyFile {
480
540
  return result;
481
541
  }
482
542
  else if (statement.type == "FunctionDeclaration") {
483
- const result = this.sourceNodeHelper(statement, (statement.isLocal ? "local " : "") + "function ");
543
+ const result = this.sourceNodeHelper(statement, statement.isLocal ? "local" : "function");
544
+ if (statement.isLocal) {
545
+ addWithSeparator(result, "function");
546
+ }
484
547
  if (statement.identifier) {
485
548
  addWithSeparator(result, this.formatExpression(statement.identifier));
486
549
  }
@@ -547,10 +610,9 @@ class MinifyFile {
547
610
  }
548
611
  else if (statement.type == "GotoStatement") {
549
612
  // The identifier names in a `GotoStatement` can safely be renamed
550
- return this.sourceNodeHelper(statement, [
551
- "goto ",
552
- this.generateIdentifier(statement.label),
553
- ]);
613
+ const result = this.sourceNodeHelper(statement, "goto");
614
+ addWithSeparator(result, this.generateIdentifier(statement.label));
615
+ return result;
554
616
  }
555
617
  else {
556
618
  throw TypeError("Unknown statement type: `" + JSON.stringify(statement) + "`");
@@ -563,8 +625,13 @@ class MinifyFile {
563
625
  if (expression.type == "Identifier") {
564
626
  return this.generateIdentifier(expression);
565
627
  }
566
- else if (expression.type == "StringLiteral" ||
567
- expression.type == "NumericLiteral" ||
628
+ else if (expression.type == "StringLiteral") {
629
+ const fieldRename = this.minifier.getFieldRename(expression);
630
+ return fieldRename
631
+ ? this.sourceNodeHelper(expression, JSON.stringify(fieldRename.name), fieldRename.originalName)
632
+ : this.sourceNodeHelper(expression, expression.raw);
633
+ }
634
+ else if (expression.type == "NumericLiteral" ||
568
635
  expression.type == "BooleanLiteral" ||
569
636
  expression.type == "NilLiteral" ||
570
637
  expression.type == "VarargLiteral") {
@@ -593,7 +660,11 @@ class MinifyFile {
593
660
  if (operator == "^" || operator == "..") {
594
661
  associativity = "right";
595
662
  }
596
- else if (currentPrecedence < options.precedence ||
663
+ // 上のif(結合則の決定)とは独立に判定する必要がある。else ifだと`^`と`..`が
664
+ // ここに来ず、優先順位の高い演算子の中に現れても丸括弧が付かなくなる
665
+ // (#52。例: `0.5 % (2 .. 16)`が`0.5%2 ..16`になり、`%`の方が優先順位が
666
+ // 高いため意味の違うコードとして読み直されてしまっていた)。
667
+ if (currentPrecedence < options.precedence ||
597
668
  (currentPrecedence == options.precedence &&
598
669
  associativity != options.direction &&
599
670
  options.parent != "+" &&
@@ -686,12 +757,15 @@ class MinifyFile {
686
757
  ]);
687
758
  }
688
759
  else if (expression.type == "MemberExpression") {
760
+ const fieldRename = this.minifier.getFieldRename(expression.identifier);
689
761
  return this.sourceNodeHelper(expression, [
690
762
  this.formatBase(expression.base),
691
763
  expression.indexer,
692
- this.formatExpression(expression.identifier, {
693
- preserveIdentifiers: true,
694
- }),
764
+ fieldRename
765
+ ? this.sourceNodeHelper(expression.identifier, fieldRename.name, fieldRename.originalName)
766
+ : this.formatExpression(expression.identifier, {
767
+ preserveIdentifiers: true,
768
+ }),
695
769
  ]);
696
770
  }
697
771
  else if (expression.type == "FunctionDeclaration") {
@@ -742,8 +816,13 @@ class MinifyFile {
742
816
  else {
743
817
  // at this point, `field.type == 'TableKeyString'`
744
818
  // TODO: keep track of nested scopes (#18)
819
+ const fieldRename = this.minifier.getFieldRename(field.key);
745
820
  return this.sourceNodeHelper(field, [
746
- this.formatExpression(field.key, { preserveIdentifiers: true }),
821
+ fieldRename
822
+ ? this.sourceNodeHelper(field.key, fieldRename.name, fieldRename.originalName)
823
+ : this.formatExpression(field.key, {
824
+ preserveIdentifiers: true,
825
+ }),
747
826
  "=",
748
827
  this.formatExpression(field.value),
749
828
  comma,
@@ -759,16 +838,25 @@ class MinifyFile {
759
838
  throw TypeError("Unknown expression type: `" + JSON.stringify(expression) + "`");
760
839
  }
761
840
  }
841
+ /**
842
+ * インデックス・メンバー参照・呼び出しの基底を出力する。
843
+ *
844
+ * Luaの文法で基底にそのまま置けるのは、変数・インデックス式・関数呼び出しだけで
845
+ * ある。それ以外の式は丸括弧で包まないと基底に置けない(`3 .x` も `"a":upper()`
846
+ * も構文エラーになる)。そのため、包む必要のある型を数え上げるのではなく、
847
+ * そのまま置ける形かどうかだけを見る。式の種類が増えても、元のソースには現れない
848
+ * 式を基底へ置く変換(定数畳み込みなど)が入っても、この判定だけで正しい括弧が付く。
849
+ */
762
850
  formatBase(base) {
763
851
  const type = base.type;
764
- const needsParens = type == "CallExpression" ||
765
- type == "BinaryExpression" ||
766
- type == "FunctionDeclaration" ||
767
- type == "TableConstructorExpression" ||
768
- type == "LogicalExpression" ||
769
- type == "StringLiteral";
852
+ const canStandAsBase = type == "Identifier" ||
853
+ type == "MemberExpression" ||
854
+ type == "IndexExpression" ||
855
+ type == "CallExpression" ||
856
+ type == "StringCallExpression" ||
857
+ type == "TableCallExpression";
770
858
  const result = this.sourceNodeHelper(base, this.formatExpression(base));
771
- if (needsParens) {
859
+ if (!canStandAsBase) {
772
860
  prependWithSeparator(result, "(");
773
861
  addWithSeparator(result, ")");
774
862
  }
@@ -805,16 +893,16 @@ class MinifyFile {
805
893
  // dofileは呼び出しごとに毎回展開しなおす(キャッシュしない)
806
894
  return this.minifier.printModuleInline(ref.moduleName);
807
895
  }
808
- if (!this.mode.moduleLikeLua) {
896
+ if (!this.mode.requireWrapper) {
809
897
  // SLモード(無オプション): requireもキャッシュせずその場展開する
810
898
  // (挙動互換性のため、ホイストした共有ローカルへの参照にはしない)。
811
899
  // 式の位置に置けるようにIIFEで包む。
812
900
  const body = this.minifier.printModuleInline(ref.moduleName);
813
- return this.sourceNodeHelper(expression, [
814
- "(function() ",
815
- body,
816
- " end)()",
817
- ]);
901
+ const result = this.sourceNodeHelper(expression, "(function()");
902
+ addWithSeparator(result, body);
903
+ addWithSeparator(result, "end");
904
+ result.add(")()");
905
+ return result;
818
906
  }
819
907
  // -mモードのrequireはそのまま呼び出しとして出力し、実行時のrequire関数に委ねる
820
908
  return undefined;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.walkStatement = walkStatement;
4
+ exports.walkExpression = walkExpression;
5
+ exports.walkBlockDeep = walkBlockDeep;
6
+ function walkStatement(statement, visitor) {
7
+ visitor.onStatement?.(statement);
8
+ switch (statement.type) {
9
+ case "LocalStatement":
10
+ statement.init.forEach((expr) => {
11
+ walkExpression(expr, visitor);
12
+ });
13
+ return;
14
+ case "AssignmentStatement":
15
+ statement.variables.forEach((variable) => {
16
+ if (variable.type === "Identifier") {
17
+ visitor.onIdentifierReference?.(variable);
18
+ }
19
+ else {
20
+ walkExpression(variable, visitor);
21
+ }
22
+ });
23
+ statement.init.forEach((expr) => {
24
+ walkExpression(expr, visitor);
25
+ });
26
+ return;
27
+ case "CallStatement":
28
+ walkExpression(statement.expression, visitor);
29
+ return;
30
+ case "DoStatement":
31
+ visitor.onBlock?.(statement.body);
32
+ return;
33
+ case "WhileStatement":
34
+ walkExpression(statement.condition, visitor);
35
+ visitor.onBlock?.(statement.body);
36
+ return;
37
+ case "RepeatStatement":
38
+ visitor.onBlock?.(statement.body);
39
+ walkExpression(statement.condition, visitor);
40
+ return;
41
+ case "IfStatement":
42
+ statement.clauses.forEach((clause) => {
43
+ if (clause.type !== "ElseClause") {
44
+ walkExpression(clause.condition, visitor);
45
+ }
46
+ visitor.onBlock?.(clause.body);
47
+ });
48
+ return;
49
+ case "ForNumericStatement":
50
+ walkExpression(statement.start, visitor);
51
+ walkExpression(statement.end, visitor);
52
+ if (statement.step)
53
+ walkExpression(statement.step, visitor);
54
+ visitor.onBlock?.(statement.body);
55
+ return;
56
+ case "ForGenericStatement":
57
+ statement.iterators.forEach((iterator) => {
58
+ walkExpression(iterator, visitor);
59
+ });
60
+ visitor.onBlock?.(statement.body);
61
+ return;
62
+ case "FunctionDeclaration":
63
+ if (statement.identifier) {
64
+ if (statement.identifier.type === "Identifier") {
65
+ if (!statement.isLocal) {
66
+ visitor.onIdentifierReference?.(statement.identifier);
67
+ }
68
+ }
69
+ else {
70
+ walkExpression(statement.identifier, visitor);
71
+ }
72
+ }
73
+ visitor.onFunction?.(statement);
74
+ visitor.onBlock?.(statement.body);
75
+ return;
76
+ case "ReturnStatement":
77
+ statement.arguments.forEach((argument) => {
78
+ walkExpression(argument, visitor);
79
+ });
80
+ return;
81
+ case "BreakStatement":
82
+ case "LabelStatement":
83
+ case "GotoStatement":
84
+ return;
85
+ default: {
86
+ const exhaustive = statement;
87
+ throw new TypeError("Unknown statement type: `" + JSON.stringify(exhaustive) + "`");
88
+ }
89
+ }
90
+ }
91
+ function walkExpression(expression, visitor) {
92
+ visitor.onExpression?.(expression);
93
+ switch (expression.type) {
94
+ case "Identifier":
95
+ visitor.onIdentifierReference?.(expression);
96
+ return;
97
+ case "StringLiteral":
98
+ case "NumericLiteral":
99
+ case "BooleanLiteral":
100
+ case "NilLiteral":
101
+ case "VarargLiteral":
102
+ return;
103
+ case "LogicalExpression":
104
+ case "BinaryExpression":
105
+ walkExpression(expression.left, visitor);
106
+ walkExpression(expression.right, visitor);
107
+ return;
108
+ case "UnaryExpression":
109
+ walkExpression(expression.argument, visitor);
110
+ return;
111
+ case "CallExpression":
112
+ walkExpression(expression.base, visitor);
113
+ expression.arguments.forEach((argument) => {
114
+ walkExpression(argument, visitor);
115
+ });
116
+ return;
117
+ case "TableCallExpression":
118
+ walkExpression(expression.base, visitor);
119
+ walkExpression(expression.arguments, visitor);
120
+ return;
121
+ case "StringCallExpression":
122
+ walkExpression(expression.base, visitor);
123
+ walkExpression(expression.argument, visitor);
124
+ return;
125
+ case "IndexExpression":
126
+ walkExpression(expression.base, visitor);
127
+ walkExpression(expression.index, visitor);
128
+ return;
129
+ case "MemberExpression":
130
+ walkExpression(expression.base, visitor);
131
+ return;
132
+ case "FunctionDeclaration":
133
+ visitor.onFunction?.(expression);
134
+ visitor.onBlock?.(expression.body);
135
+ return;
136
+ case "TableConstructorExpression":
137
+ expression.fields.forEach((field) => {
138
+ if (field.type === "TableKey") {
139
+ walkExpression(field.key, visitor);
140
+ }
141
+ walkExpression(field.value, visitor);
142
+ });
143
+ return;
144
+ default: {
145
+ const exhaustive = expression;
146
+ throw new TypeError("Unknown expression type: `" + JSON.stringify(exhaustive) + "`");
147
+ }
148
+ }
149
+ }
150
+ /** Walk a complete execution tree while preserving the shared node classification above. */
151
+ function walkBlockDeep(body, visitor) {
152
+ const deepVisitor = {
153
+ ...visitor,
154
+ onBlock: (nested) => {
155
+ visitor.onBlock?.(nested);
156
+ walkBlockDeep(nested, visitor);
157
+ },
158
+ };
159
+ body.forEach((statement) => {
160
+ walkStatement(statement, deepVisitor);
161
+ });
162
+ }