storm-lua-minify 0.2.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 (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +157 -37
  3. package/dist/aggregateSpecialization.js +406 -0
  4. package/dist/ast2lua.js +223 -82
  5. package/dist/astWalk.js +162 -0
  6. package/dist/callGraph.js +372 -0
  7. package/dist/cli.js +53 -32
  8. package/dist/cliOptions.js +36 -0
  9. package/dist/cliProgress.js +87 -0
  10. package/dist/config.js +73 -0
  11. package/dist/constantFold.js +798 -0
  12. package/dist/controlFlow.js +266 -0
  13. package/dist/functionRewrites.js +580 -0
  14. package/dist/generatedAst.js +108 -0
  15. package/dist/generatedNode.js +23 -0
  16. package/dist/globalRename.js +35 -0
  17. package/dist/interproceduralAnalysis.js +842 -0
  18. package/dist/interproceduralConstants.js +120 -0
  19. package/dist/keywordLocator.js +68 -0
  20. package/dist/linker.js +13 -0
  21. package/dist/luaString.js +157 -0
  22. package/dist/minifier.js +1243 -35
  23. package/dist/optimizerAnalysis.js +43 -0
  24. package/dist/optimizerDiagnostics.js +65 -0
  25. package/dist/optimizerFacts.js +529 -0
  26. package/dist/optimizerPass.js +96 -0
  27. package/dist/optimizerTransaction.js +56 -0
  28. package/dist/optimizerValueDomain.js +180 -0
  29. package/dist/options.js +233 -0
  30. package/dist/progress.js +2 -0
  31. package/dist/removeUnused.js +145 -0
  32. package/dist/renamer.js +236 -53
  33. package/dist/resolver.js +37 -14
  34. package/dist/runtimeEnvironment.js +105 -0
  35. package/dist/sourceMetadata.js +314 -0
  36. package/dist/statementDataflow.js +259 -0
  37. package/dist/statementScheduler.js +595 -0
  38. package/dist/symbolLiveness.js +92 -0
  39. package/dist/tableEffects.js +356 -0
  40. package/dist/transform.js +40 -0
  41. package/dist/valueFlow.js +409 -0
  42. package/dist/wholeProgramExports.js +646 -0
  43. package/dist/wholeProgramFieldRenames.js +583 -0
  44. package/dist/wholeProgramFields.js +672 -0
  45. package/dist/wholeProgramObjects.js +783 -0
  46. package/package.json +50 -33
  47. package/.github/workflows/ci.yml +0 -39
  48. package/.github/workflows/publish.yml +0 -39
  49. package/.prettierignore +0 -3
  50. package/.prettierrc.json +0 -1
  51. package/dist/index.js +0 -27
  52. package/eslint.config.js +0 -29
  53. package/src/ast2lua.ts +0 -940
  54. package/src/cli.ts +0 -86
  55. package/src/linker.ts +0 -108
  56. package/src/minifier.ts +0 -284
  57. package/src/output.ts +0 -71
  58. package/src/renamer.ts +0 -134
  59. package/src/resolver.ts +0 -378
  60. package/test/circular-require.test.ts +0 -19
  61. package/test/fixtures/bare-require/main.lua +0 -2
  62. package/test/fixtures/bare-require/mod.lua +0 -1
  63. package/test/fixtures/bitwise-precedence/main.lua +0 -10
  64. package/test/fixtures/circular-require/a.lua +0 -2
  65. package/test/fixtures/circular-require/b.lua +0 -2
  66. package/test/fixtures/circular-require/main.lua +0 -2
  67. package/test/fixtures/dofile/greet.lua +0 -1
  68. package/test/fixtures/dofile/main.lua +0 -2
  69. package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +0 -2
  70. package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +0 -2
  71. package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +0 -2
  72. package/test/fixtures/entry-scope-many-requires/dep_delta.lua +0 -2
  73. package/test/fixtures/entry-scope-many-requires/dep_echo.lua +0 -2
  74. package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +0 -2
  75. package/test/fixtures/entry-scope-many-requires/dep_golf.lua +0 -2
  76. package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +0 -2
  77. package/test/fixtures/entry-scope-many-requires/dep_india.lua +0 -2
  78. package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +0 -2
  79. package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +0 -2
  80. package/test/fixtures/entry-scope-many-requires/main.lua +0 -25
  81. package/test/fixtures/multi-require/common.lua +0 -1
  82. package/test/fixtures/multi-require/main.lua +0 -3
  83. package/test/fixtures/nested-module/main.lua +0 -2
  84. package/test/fixtures/nested-module/sub/deep.lua +0 -1
  85. package/test/fixtures/require-call/main.lua +0 -2
  86. package/test/fixtures/require-call/mod.lua +0 -5
  87. package/test/fixtures/require-in-expression/main.lua +0 -1
  88. package/test/fixtures/require-in-expression/mod.lua +0 -1
  89. package/test/fixtures/require-string-call/main.lua +0 -2
  90. package/test/fixtures/require-string-call/mod.lua +0 -5
  91. package/test/fixtures/single-file/main.lua +0 -15
  92. package/test/identifier-collision.test.ts +0 -28
  93. package/test/lib/collision.ts +0 -131
  94. package/test/lib/helpers.ts +0 -124
  95. package/test/no-rename.test.ts +0 -19
  96. package/test/output.test.ts +0 -95
  97. package/test/precedence.test.ts +0 -28
  98. package/test/renamer.test.ts +0 -130
  99. package/test/resolver.test.ts +0 -206
  100. package/test/roundtrip.test.ts +0 -26
  101. package/test/snapshot.test.ts +0 -27
  102. package/test/snapshots/bare-require.sl.lua +0 -1
  103. package/test/snapshots/bitwise-precedence.sl.lua +0 -2
  104. package/test/snapshots/dofile.sl.lua +0 -1
  105. package/test/snapshots/entry-scope-many-requires.m.lua +0 -14
  106. package/test/snapshots/multi-require.m.lua +0 -4
  107. package/test/snapshots/multi-require.sl.lua +0 -1
  108. package/test/snapshots/nested-module.m.lua +0 -4
  109. package/test/snapshots/require-call.m.lua +0 -5
  110. package/test/snapshots/require-call.sl.lua +0 -1
  111. package/test/snapshots/require-in-expression.sl.lua +0 -1
  112. package/test/snapshots/require-string-call.m.lua +0 -5
  113. package/test/snapshots/single-file.sl.lua +0 -6
  114. package/test/sourcemap.test.ts +0 -105
  115. package/tsconfig.eslint.json +0 -8
  116. package/tsconfig.json +0 -111
package/dist/ast2lua.js CHANGED
@@ -6,6 +6,9 @@ exports.MinifyFile = exports.IDENTIFIER_PARTS = void 0;
6
6
  exports.isKeyword = isKeyword;
7
7
  const source_map_1 = require("source-map");
8
8
  const linker_1 = require("./linker");
9
+ const keywordLocator_1 = require("./keywordLocator");
10
+ const transform_1 = require("./transform");
11
+ const sourceMetadata_1 = require("./sourceMetadata");
9
12
  const PRECEDENCE = {
10
13
  or: 1,
11
14
  and: 2,
@@ -135,6 +138,10 @@ function isKeyword(id) {
135
138
  }
136
139
  return false;
137
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]+$/;
138
145
  function isNeedSeparator(a, b) {
139
146
  const lastCharA = a.slice(-1);
140
147
  const firstCharB = b.charAt(0);
@@ -150,11 +157,15 @@ function isNeedSeparator(a, b) {
150
157
  // e.g. `local a` + `local b`
151
158
  return true;
152
159
  }
153
- else {
154
- // e.g. `not` + `(2>3 or 3<2)`
155
- // e.g. `x` + `^`
156
- 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;
157
165
  }
166
+ // e.g. `not` + `(2>3 or 3<2)`
167
+ // e.g. `x` + `^`
168
+ return false;
158
169
  }
159
170
  if (regexDigits.test(lastCharA)) {
160
171
  if (firstCharB == "(" ||
@@ -182,7 +193,18 @@ function isNeedSeparator(a, b) {
182
193
  }
183
194
  return false;
184
195
  }
185
- 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)) {
186
208
  if (isNeedSeparator(val.toString(), wrapArray(adding)
187
209
  .map((p) => p.toString())
188
210
  .join())) {
@@ -191,7 +213,7 @@ function addWithSeparator(val, adding, separator = " ") {
191
213
  val.add(adding);
192
214
  return val;
193
215
  }
194
- function prependWithSeparator(val, prepending, separator = " ") {
216
+ function prependWithSeparator(val, prepending, separator = whitespaceOf(val)) {
195
217
  if (isNeedSeparator(wrapArray(prepending)
196
218
  .map((p) => p.toString())
197
219
  .join(), val.toString())) {
@@ -200,7 +222,7 @@ function prependWithSeparator(val, prepending, separator = " ") {
200
222
  val.prepend(prepending);
201
223
  return val;
202
224
  }
203
- function insertSeparator(a, b, separator = " ") {
225
+ function insertSeparator(a, b, separator = whitespaceOf(a, b)) {
204
226
  return isNeedSeparator(a.toString(), b.toString()) ? separator : undefined;
205
227
  }
206
228
  class MinifyFile {
@@ -209,28 +231,29 @@ class MinifyFile {
209
231
  ast;
210
232
  minifier;
211
233
  mode;
234
+ requiredWhitespace;
212
235
  constructor(fileName, moduleName, ast, minifier, mode) {
213
236
  this.fileName = fileName;
214
237
  this.moduleName = moduleName;
215
238
  this.ast = ast;
216
239
  this.minifier = minifier;
217
240
  this.mode = mode;
241
+ this.requiredWhitespace = mode.requiredWhitespace ?? "\n";
218
242
  }
219
- parse(noComment) {
243
+ /** 合成文同士の境界も、通常の文リストと同じ区切り判定で出力する。 */
244
+ printGeneratedStatements(statements) {
245
+ return this.formatStatementList(statements);
246
+ }
247
+ parse() {
220
248
  const body = this.formatStatementList(this.ast.body);
221
- if (!noComment && this.ast.comments) {
222
- const comments = this.ast.comments;
223
- comments
224
- .reverse()
225
- .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
226
- .forEach((comment) => {
227
- body.prepend([this.sourceNodeHelper(comment, comment.raw), "\n"]);
228
- });
229
- return body;
230
- }
231
- else {
232
- return body;
233
- }
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;
234
257
  }
235
258
  /**
236
259
  * モジュール本体の最後の文が「単一の式を返すreturn文」であるときに限り、
@@ -238,7 +261,7 @@ class MinifyFile {
238
261
  * 展開したい呼び出し側(#29のレビュー対応)が利用する。
239
262
  * 該当しない場合はundefinedを返し、呼び出し側はIIFE方式へフォールバックする。
240
263
  */
241
- parseAsStatementsAndFinalExpression(noComment) {
264
+ parseAsStatementsAndFinalExpression() {
242
265
  const body = this.ast.body;
243
266
  const last = body[body.length - 1];
244
267
  if (!last ||
@@ -247,18 +270,20 @@ class MinifyFile {
247
270
  return undefined;
248
271
  }
249
272
  const statements = this.formatStatementList(body.slice(0, -1));
250
- if (!noComment && this.ast.comments) {
251
- this.ast.comments
252
- .slice()
253
- .reverse()
254
- .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
255
- .forEach((comment) => {
256
- statements.prepend([
257
- this.sourceNodeHelper(comment, comment.raw),
258
- "\n",
259
- ]);
260
- });
261
- }
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
+ });
262
287
  const finalExpression = this.formatExpression(last.arguments[0]);
263
288
  return { statements, finalExpression };
264
289
  }
@@ -267,12 +292,79 @@ class MinifyFile {
267
292
  const column = node?.loc?.start.column;
268
293
  // this.fileNameは常にこのMinifyFileインスタンスが担当するモジュール自身の
269
294
  // ファイル名(Linkパスで解決済み)なので、ここで出力するノードの由来ファイルとして正しい。
270
- 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;
298
+ }
299
+ _keywordLocator;
300
+ /**
301
+ * luaparseのASTはキーワード単体の位置を持たないため、`then`/`do`/`until`の
302
+ * ような「文・式の間に挟まるキーワード」の正確な位置は、モジュールの
303
+ * ソースを再トークン化して求める(#14)。初回アクセス時に1回だけ構築する。
304
+ */
305
+ keywordLocator() {
306
+ if (!this._keywordLocator) {
307
+ const sourceText = this.minifier.moduleSourceText.get(this.moduleName);
308
+ if (sourceText == undefined) {
309
+ throw new Error(this.moduleName + " is not found");
310
+ }
311
+ this._keywordLocator = new keywordLocator_1.KeywordLocator(sourceText, this.minifier.luaParseSettings);
312
+ }
313
+ return this._keywordLocator;
314
+ }
315
+ /**
316
+ * `anchor`ノードの直後(`anchor.loc.end`以降)で最初に現れる`value`という
317
+ * 値のキーワードトークンを探し、その位置を持つ`SourceNode`を返す。
318
+ * `anchor`は「探しているキーワードの直前に必ず存在する、既に位置が分かって
319
+ * いるノード」(例: `then`の場合は条件式)を渡す。ASTの入れ子構造上、
320
+ * その直前ノードの`loc.end`以降を走査すれば、ネストした同名キーワード
321
+ * (入れ子の`if...then`等)を誤って拾うことはない。
322
+ */
323
+ keywordAfter(anchor, value) {
324
+ if (!anchor.loc?.end) {
325
+ return this.sourceNodeHelper(undefined, value);
326
+ }
327
+ return this.keywordFrom(anchor.loc?.end, value);
328
+ }
329
+ keywordFrom(from, value) {
330
+ const pos = from ? this.keywordLocator().findFrom(from, value) : undefined;
331
+ return new source_map_1.SourceNode(pos?.line ?? null, pos?.column ?? null, this.fileName, value);
332
+ }
333
+ /**
334
+ * `if`/`while`/`do`/`for`/`function`を締める`end`キーワードの位置を返す。
335
+ * これらの文は必ず`end`で終わり、末尾に余計な内容が続かないため、
336
+ * 文全体の`loc.end`(luaparseが既に計算済み)から`end`の文字数(3)を
337
+ * 引くだけで、再トークン化なしに正確な位置を求められる。
338
+ */
339
+ endKeyword(node) {
340
+ const end = node.loc?.end;
341
+ return new source_map_1.SourceNode(end?.line ?? null, end == undefined ? null : end.column - 3, this.fileName, "end");
271
342
  }
272
343
  formatStatementList(body) {
273
344
  const result = this.sourceNodeHelper(undefined, []);
345
+ const metadata = this.minifier.getSourceMetadata(this.moduleName);
274
346
  wrapArray(body).forEach((statement) => {
275
- 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");
276
368
  });
277
369
  return result;
278
370
  }
@@ -287,7 +379,7 @@ class MinifyFile {
287
379
  * (呼び出し側は従来のIIFE方式にフォールバックする)。
288
380
  */
289
381
  trySpliceRequireStatement(statement) {
290
- if (this.mode.moduleLikeLua) {
382
+ if (this.mode.requireWrapper) {
291
383
  return undefined;
292
384
  }
293
385
  if (statement.variables.length !== 1 || statement.init.length !== 1) {
@@ -308,7 +400,10 @@ class MinifyFile {
308
400
  : this.formatExpression(target);
309
401
  const result = this.sourceNodeHelper(statement, []);
310
402
  addWithSeparator(result, spliced.statements);
311
- addWithSeparator(result, isLocal ? ["local ", targetNode] : [targetNode]);
403
+ if (isLocal) {
404
+ addWithSeparator(result, "local");
405
+ }
406
+ addWithSeparator(result, targetNode);
312
407
  addWithSeparator(result, "=");
313
408
  addWithSeparator(result, spliced.finalExpression);
314
409
  return result;
@@ -319,16 +414,26 @@ class MinifyFile {
319
414
  * 近い、LifeBoat Modeでの一般的な使い方に合わせるための対応。#29のレビュー対応)。
320
415
  */
321
416
  tryInlineBareRequireStatement(expr) {
322
- if (this.mode.moduleLikeLua) {
417
+ if (this.mode.requireWrapper) {
323
418
  return undefined;
324
419
  }
325
420
  const moduleRef = this.matchModuleCallExpression(expr);
326
421
  if (!moduleRef || moduleRef.kind !== "require") {
327
422
  return undefined;
328
423
  }
424
+ // 戻り値を使わないrequireでは、依存モジュール末尾のreturn式も不要になる。
425
+ // return文ごと展開すると呼び出し元を途中でreturnし、後続文がある場合は
426
+ // 構文上も不正になるため、分離可能なら副作用を持つ先行文だけを出力する。
427
+ const spliced = this.minifier.splitModuleForStatementSplice(moduleRef.moduleName);
428
+ if (spliced) {
429
+ return spliced.statements;
430
+ }
329
431
  return this.minifier.printModuleInline(moduleRef.moduleName);
330
432
  }
331
433
  formatStatement(statement) {
434
+ if (statement.type === "ModuleSplice") {
435
+ return this.minifier.printModuleInline(statement.moduleName);
436
+ }
332
437
  if (statement.type == "AssignmentStatement" ||
333
438
  statement.type == "LocalStatement") {
334
439
  const spliced = this.trySpliceRequireStatement(statement);
@@ -353,10 +458,8 @@ class MinifyFile {
353
458
  const variables = statement.variables
354
459
  .map((variable) => [this.formatExpression(variable), ","])
355
460
  .flat();
356
- const result = this.sourceNodeHelper(statement, [
357
- "local ",
358
- this.sourceNodeHelper(undefined, variables.slice(0, -1)),
359
- ]);
461
+ const result = this.sourceNodeHelper(statement, "local");
462
+ addWithSeparator(result, this.sourceNodeHelper(undefined, variables.slice(0, -1)));
360
463
  if (statement.init.length) {
361
464
  const inits = statement.init
362
465
  .map((init) => [this.formatExpression(init), ","])
@@ -380,12 +483,12 @@ class MinifyFile {
380
483
  if (clause.type == "IfClause") {
381
484
  addWithSeparator(clauseMap, "if");
382
485
  addWithSeparator(clauseMap, this.formatExpression(clause.condition));
383
- addWithSeparator(clauseMap, "then");
486
+ addWithSeparator(clauseMap, this.keywordAfter(clause.condition, "then"));
384
487
  }
385
488
  else if (clause.type == "ElseifClause") {
386
489
  addWithSeparator(clauseMap, "elseif");
387
490
  addWithSeparator(clauseMap, this.formatExpression(clause.condition));
388
- addWithSeparator(clauseMap, "then");
491
+ addWithSeparator(clauseMap, this.keywordAfter(clause.condition, "then"));
389
492
  }
390
493
  else {
391
494
  addWithSeparator(clauseMap, "else");
@@ -393,21 +496,21 @@ class MinifyFile {
393
496
  addWithSeparator(clauseMap, this.formatStatementList(clause.body));
394
497
  addWithSeparator(result, clauseMap);
395
498
  });
396
- addWithSeparator(result, "end");
499
+ addWithSeparator(result, this.endKeyword(statement));
397
500
  return result;
398
501
  }
399
502
  else if (statement.type == "WhileStatement") {
400
503
  const result = this.sourceNodeHelper(statement, "while");
401
504
  addWithSeparator(result, this.formatExpression(statement.condition));
402
- addWithSeparator(result, "do");
505
+ addWithSeparator(result, this.keywordAfter(statement.condition, "do"));
403
506
  addWithSeparator(result, this.formatStatementList(statement.body));
404
- addWithSeparator(result, "end");
507
+ addWithSeparator(result, this.endKeyword(statement));
405
508
  return result;
406
509
  }
407
510
  else if (statement.type == "DoStatement") {
408
511
  const result = this.sourceNodeHelper(statement, "do");
409
512
  addWithSeparator(result, this.formatStatementList(statement.body));
410
- addWithSeparator(result, "end");
513
+ addWithSeparator(result, this.endKeyword(statement));
411
514
  return result;
412
515
  }
413
516
  else if (statement.type == "ReturnStatement") {
@@ -426,12 +529,21 @@ class MinifyFile {
426
529
  else if (statement.type == "RepeatStatement") {
427
530
  const result = this.sourceNodeHelper(statement, "repeat");
428
531
  addWithSeparator(result, this.formatStatementList(statement.body));
429
- addWithSeparator(result, "until");
532
+ // body内の最後の文があればその直後、無ければ`repeat`キーワード自身の
533
+ // 開始位置から`until`を探す(bodyが空でも、その手前には`repeat`しか
534
+ // 存在しないため安全に検索できる)。
535
+ const untilAnchor = statement.body.length
536
+ ? statement.body[statement.body.length - 1].loc?.end
537
+ : statement.loc?.start;
538
+ addWithSeparator(result, this.keywordFrom(untilAnchor, "until"));
430
539
  addWithSeparator(result, this.formatExpression(statement.condition));
431
540
  return result;
432
541
  }
433
542
  else if (statement.type == "FunctionDeclaration") {
434
- 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
+ }
435
547
  if (statement.identifier) {
436
548
  addWithSeparator(result, this.formatExpression(statement.identifier));
437
549
  }
@@ -451,7 +563,7 @@ class MinifyFile {
451
563
  }
452
564
  addWithSeparator(result, ")");
453
565
  addWithSeparator(result, this.formatStatementList(statement.body));
454
- addWithSeparator(result, "end");
566
+ addWithSeparator(result, this.endKeyword(statement));
455
567
  return result;
456
568
  }
457
569
  else if (statement.type == "ForGenericStatement") {
@@ -466,9 +578,9 @@ class MinifyFile {
466
578
  addWithSeparator(result, variables.slice(0, -1));
467
579
  addWithSeparator(result, "in");
468
580
  addWithSeparator(result, iterators.slice(0, -1));
469
- addWithSeparator(result, "do");
581
+ addWithSeparator(result, this.keywordAfter(statement.iterators[statement.iterators.length - 1], "do"));
470
582
  addWithSeparator(result, this.formatStatementList(statement.body));
471
- addWithSeparator(result, "end");
583
+ addWithSeparator(result, this.endKeyword(statement));
472
584
  return result;
473
585
  }
474
586
  else if (statement.type == "ForNumericStatement") {
@@ -483,9 +595,9 @@ class MinifyFile {
483
595
  addWithSeparator(result, ",");
484
596
  addWithSeparator(result, this.formatExpression(statement.step));
485
597
  }
486
- addWithSeparator(result, "do");
598
+ addWithSeparator(result, this.keywordAfter(statement.step ?? statement.end, "do"));
487
599
  addWithSeparator(result, this.formatStatementList(statement.body));
488
- addWithSeparator(result, "end");
600
+ addWithSeparator(result, this.endKeyword(statement));
489
601
  return result;
490
602
  }
491
603
  else if (statement.type == "LabelStatement") {
@@ -498,10 +610,9 @@ class MinifyFile {
498
610
  }
499
611
  else if (statement.type == "GotoStatement") {
500
612
  // The identifier names in a `GotoStatement` can safely be renamed
501
- return this.sourceNodeHelper(statement, [
502
- "goto ",
503
- this.generateIdentifier(statement.label),
504
- ]);
613
+ const result = this.sourceNodeHelper(statement, "goto");
614
+ addWithSeparator(result, this.generateIdentifier(statement.label));
615
+ return result;
505
616
  }
506
617
  else {
507
618
  throw TypeError("Unknown statement type: `" + JSON.stringify(statement) + "`");
@@ -514,8 +625,13 @@ class MinifyFile {
514
625
  if (expression.type == "Identifier") {
515
626
  return this.generateIdentifier(expression);
516
627
  }
517
- else if (expression.type == "StringLiteral" ||
518
- 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" ||
519
635
  expression.type == "BooleanLiteral" ||
520
636
  expression.type == "NilLiteral" ||
521
637
  expression.type == "VarargLiteral") {
@@ -544,7 +660,11 @@ class MinifyFile {
544
660
  if (operator == "^" || operator == "..") {
545
661
  associativity = "right";
546
662
  }
547
- 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 ||
548
668
  (currentPrecedence == options.precedence &&
549
669
  associativity != options.direction &&
550
670
  options.parent != "+" &&
@@ -637,12 +757,15 @@ class MinifyFile {
637
757
  ]);
638
758
  }
639
759
  else if (expression.type == "MemberExpression") {
760
+ const fieldRename = this.minifier.getFieldRename(expression.identifier);
640
761
  return this.sourceNodeHelper(expression, [
641
762
  this.formatBase(expression.base),
642
763
  expression.indexer,
643
- this.formatExpression(expression.identifier, {
644
- preserveIdentifiers: true,
645
- }),
764
+ fieldRename
765
+ ? this.sourceNodeHelper(expression.identifier, fieldRename.name, fieldRename.originalName)
766
+ : this.formatExpression(expression.identifier, {
767
+ preserveIdentifiers: true,
768
+ }),
646
769
  ]);
647
770
  }
648
771
  else if (expression.type == "FunctionDeclaration") {
@@ -663,7 +786,7 @@ class MinifyFile {
663
786
  result.add(")");
664
787
  const body = this.formatStatementList(expression.body);
665
788
  addWithSeparator(result, body);
666
- addWithSeparator(result, "end");
789
+ addWithSeparator(result, this.endKeyword(expression));
667
790
  return result;
668
791
  }
669
792
  else if (expression.type == "TableConstructorExpression") {
@@ -693,8 +816,13 @@ class MinifyFile {
693
816
  else {
694
817
  // at this point, `field.type == 'TableKeyString'`
695
818
  // TODO: keep track of nested scopes (#18)
819
+ const fieldRename = this.minifier.getFieldRename(field.key);
696
820
  return this.sourceNodeHelper(field, [
697
- 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
+ }),
698
826
  "=",
699
827
  this.formatExpression(field.value),
700
828
  comma,
@@ -710,16 +838,25 @@ class MinifyFile {
710
838
  throw TypeError("Unknown expression type: `" + JSON.stringify(expression) + "`");
711
839
  }
712
840
  }
841
+ /**
842
+ * インデックス・メンバー参照・呼び出しの基底を出力する。
843
+ *
844
+ * Luaの文法で基底にそのまま置けるのは、変数・インデックス式・関数呼び出しだけで
845
+ * ある。それ以外の式は丸括弧で包まないと基底に置けない(`3 .x` も `"a":upper()`
846
+ * も構文エラーになる)。そのため、包む必要のある型を数え上げるのではなく、
847
+ * そのまま置ける形かどうかだけを見る。式の種類が増えても、元のソースには現れない
848
+ * 式を基底へ置く変換(定数畳み込みなど)が入っても、この判定だけで正しい括弧が付く。
849
+ */
713
850
  formatBase(base) {
714
851
  const type = base.type;
715
- const needsParens = type == "CallExpression" ||
716
- type == "BinaryExpression" ||
717
- type == "FunctionDeclaration" ||
718
- type == "TableConstructorExpression" ||
719
- type == "LogicalExpression" ||
720
- type == "StringLiteral";
852
+ const canStandAsBase = type == "Identifier" ||
853
+ type == "MemberExpression" ||
854
+ type == "IndexExpression" ||
855
+ type == "CallExpression" ||
856
+ type == "StringCallExpression" ||
857
+ type == "TableCallExpression";
721
858
  const result = this.sourceNodeHelper(base, this.formatExpression(base));
722
- if (needsParens) {
859
+ if (!canStandAsBase) {
723
860
  prependWithSeparator(result, "(");
724
861
  addWithSeparator(result, ")");
725
862
  }
@@ -756,16 +893,16 @@ class MinifyFile {
756
893
  // dofileは呼び出しごとに毎回展開しなおす(キャッシュしない)
757
894
  return this.minifier.printModuleInline(ref.moduleName);
758
895
  }
759
- if (!this.mode.moduleLikeLua) {
896
+ if (!this.mode.requireWrapper) {
760
897
  // SLモード(無オプション): requireもキャッシュせずその場展開する
761
898
  // (挙動互換性のため、ホイストした共有ローカルへの参照にはしない)。
762
899
  // 式の位置に置けるようにIIFEで包む。
763
900
  const body = this.minifier.printModuleInline(ref.moduleName);
764
- return this.sourceNodeHelper(expression, [
765
- "(function() ",
766
- body,
767
- " end)()",
768
- ]);
901
+ const result = this.sourceNodeHelper(expression, "(function()");
902
+ addWithSeparator(result, body);
903
+ addWithSeparator(result, "end");
904
+ result.add(")()");
905
+ return result;
769
906
  }
770
907
  // -mモードのrequireはそのまま呼び出しとして出力し、実行時のrequire関数に委ねる
771
908
  return undefined;
@@ -776,7 +913,11 @@ class MinifyFile {
776
913
  const renamed = this.minifier
777
914
  .getRenameResult(this.moduleName)
778
915
  .nameOf(nameItem);
779
- return this.sourceNodeHelper(nameItem, renamed ?? nameItem.name, nameItem.name);
916
+ // #8bのエイリアス化はノードの`.name`自体を書き換えるため、loc(元のソース上の
917
+ // 位置)はそのままでもnameItem.nameはもう元の識別子名ではない。Source Mapの
918
+ // namesフィールドには常に「元のソースで書かれていた名前」を載せる必要がある。
919
+ const originalName = (0, transform_1.originalNameOf)(nameItem) ?? nameItem.name;
920
+ return this.sourceNodeHelper(nameItem, renamed ?? nameItem.name, originalName);
780
921
  }
781
922
  }
782
923
  exports.MinifyFile = MinifyFile;