storm-lua-minify 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.github/workflows/ci.yml +39 -0
  2. package/.github/workflows/publish.yml +39 -0
  3. package/.prettierignore +3 -0
  4. package/.prettierrc.json +1 -0
  5. package/LICENSE +21 -21
  6. package/README.md +37 -15
  7. package/dist/ast2lua.js +188 -85
  8. package/dist/cli.js +16 -6
  9. package/dist/index.js +3 -19
  10. package/dist/linker.js +96 -0
  11. package/dist/minifier.js +174 -53
  12. package/dist/output.js +33 -0
  13. package/dist/renamer.js +87 -0
  14. package/dist/resolver.js +303 -0
  15. package/eslint.config.js +29 -0
  16. package/package.json +33 -27
  17. package/src/ast2lua.ts +940 -812
  18. package/src/cli.ts +86 -56
  19. package/src/linker.ts +108 -0
  20. package/src/minifier.ts +284 -118
  21. package/src/output.ts +71 -0
  22. package/src/renamer.ts +134 -0
  23. package/src/resolver.ts +378 -0
  24. package/test/circular-require.test.ts +19 -0
  25. package/test/fixtures/bare-require/main.lua +2 -0
  26. package/test/fixtures/bare-require/mod.lua +1 -0
  27. package/test/fixtures/bitwise-precedence/main.lua +10 -0
  28. package/test/fixtures/circular-require/a.lua +2 -0
  29. package/test/fixtures/circular-require/b.lua +2 -0
  30. package/test/fixtures/circular-require/main.lua +2 -0
  31. package/test/fixtures/dofile/greet.lua +1 -0
  32. package/test/fixtures/dofile/main.lua +2 -0
  33. package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +2 -0
  34. package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +2 -0
  35. package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +2 -0
  36. package/test/fixtures/entry-scope-many-requires/dep_delta.lua +2 -0
  37. package/test/fixtures/entry-scope-many-requires/dep_echo.lua +2 -0
  38. package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +2 -0
  39. package/test/fixtures/entry-scope-many-requires/dep_golf.lua +2 -0
  40. package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +2 -0
  41. package/test/fixtures/entry-scope-many-requires/dep_india.lua +2 -0
  42. package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +2 -0
  43. package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +2 -0
  44. package/test/fixtures/entry-scope-many-requires/main.lua +25 -0
  45. package/test/fixtures/multi-require/common.lua +1 -0
  46. package/test/fixtures/multi-require/main.lua +3 -0
  47. package/test/fixtures/nested-module/main.lua +2 -0
  48. package/test/fixtures/nested-module/sub/deep.lua +1 -0
  49. package/test/fixtures/require-call/main.lua +2 -0
  50. package/test/fixtures/require-call/mod.lua +5 -0
  51. package/test/fixtures/require-in-expression/main.lua +1 -0
  52. package/test/fixtures/require-in-expression/mod.lua +1 -0
  53. package/test/fixtures/require-string-call/main.lua +2 -0
  54. package/test/fixtures/require-string-call/mod.lua +5 -0
  55. package/test/fixtures/single-file/main.lua +15 -0
  56. package/test/identifier-collision.test.ts +28 -0
  57. package/test/lib/collision.ts +131 -0
  58. package/test/lib/helpers.ts +124 -0
  59. package/test/no-rename.test.ts +19 -0
  60. package/test/output.test.ts +95 -0
  61. package/test/precedence.test.ts +28 -0
  62. package/test/renamer.test.ts +130 -0
  63. package/test/resolver.test.ts +206 -0
  64. package/test/roundtrip.test.ts +26 -0
  65. package/test/snapshot.test.ts +27 -0
  66. package/test/snapshots/bare-require.sl.lua +1 -0
  67. package/test/snapshots/bitwise-precedence.sl.lua +2 -0
  68. package/test/snapshots/dofile.sl.lua +1 -0
  69. package/test/snapshots/entry-scope-many-requires.m.lua +14 -0
  70. package/test/snapshots/multi-require.m.lua +4 -0
  71. package/test/snapshots/multi-require.sl.lua +1 -0
  72. package/test/snapshots/nested-module.m.lua +4 -0
  73. package/test/snapshots/require-call.m.lua +5 -0
  74. package/test/snapshots/require-call.sl.lua +1 -0
  75. package/test/snapshots/require-in-expression.sl.lua +1 -0
  76. package/test/snapshots/require-string-call.m.lua +5 -0
  77. package/test/snapshots/single-file.sl.lua +6 -0
  78. package/test/sourcemap.test.ts +105 -0
  79. package/tsconfig.eslint.json +8 -0
  80. package/tsconfig.json +111 -109
  81. package/.eslintrc.json +0 -20
package/src/output.ts ADDED
@@ -0,0 +1,71 @@
1
+ import path from "path";
2
+ import { SourceNode } from "source-map";
3
+
4
+ export interface MinifiedOutput {
5
+ code: string;
6
+ map: string;
7
+ }
8
+
9
+ export type SourceMappingUrlStyle = "legacy" | "line" | "strict";
10
+
11
+ export interface BuildMinifiedOutputOptions {
12
+ /**
13
+ * sourceMappingURLアノテーションの出力形式。省略時は"legacy"。
14
+ *
15
+ * - "legacy"(既定): 旧storm-lua-minifyと同じ複数行の`--[[ ... ]]`ブロック
16
+ * コメント(`--[[\n//# sourceMappingURL=...\n]]`)をそのまま出力する。
17
+ * 互換性優先で、この形式を前提に読み込む既存ツールと組み合わせて使う。
18
+ * Source Map仕様が定める「アノテーションは生成コードの最後の行(末尾が
19
+ * 空行の場合はその直前の行)に置く」というルールには厳密には従わない
20
+ * (アノテーション行の後に`]]`が続き最終行にならないため)。
21
+ * - "line": 単一行の`--`ラインコメント(`-- //# sourceMappingURL=...`)。
22
+ * 末尾の空行を除けば出力の最終行になるため「最終行ルール」は満たすが、
23
+ * マーカー文字列自体は`-- `に続く形になる(行頭が厳密に`//`であることを
24
+ * 要求する一部のツールからは認識されない可能性がある)。有効なLuaの
25
+ * ままである。
26
+ * - "strict": Luaコメントで一切包まず`//# sourceMappingURL=...`をそのまま
27
+ * 出力する。マーカー文字列・最終行ルールの両方を厳密に満たす。
28
+ *
29
+ * 【Lua文法との両立について】Lua 5.1〜5.4のどのバージョンでも`/`(除算)や
30
+ * `//`(5.3以降の整数除算)は二項演算子のトークンであり、文の先頭に
31
+ * 来ることはできない(Luaの文は識別子・キーワード・`::`・`;`のいずれかで
32
+ * 始まらなければならない)。そのため`//`から始まる行を、一切のコメント化
33
+ * なしにLuaの文として構文解析可能にする方法は存在しない。したがって
34
+ * "strict"を選択した場合、出力ファイルの最終行は有効なLua文ではなくなる
35
+ * (luaparseで再パースするとエラーになる)。この制約はLuaの言語仕様上
36
+ * 回避不能であることを確認済み。
37
+ */
38
+ sourceMappingUrlStyle?: SourceMappingUrlStyle;
39
+ }
40
+
41
+ /**
42
+ * ミニファイ済みSourceNodeに、sourceMappingURLアノテーションとSource Mapを
43
+ * 付加した最終的な出力を組み立てる。アノテーションの出力形式は
44
+ * `BuildMinifiedOutputOptions.sourceMappingUrlStyle`で選択する。
45
+ */
46
+ export function buildMinifiedOutput(
47
+ sourceNode: SourceNode,
48
+ minFileName: string,
49
+ mapFileName: string,
50
+ options: BuildMinifiedOutputOptions = {},
51
+ ): MinifiedOutput {
52
+ const style = options.sourceMappingUrlStyle ?? "legacy";
53
+ const marker = "//# sourceMappingURL=" + path.basename(mapFileName);
54
+
55
+ if (style === "legacy") {
56
+ // 旧storm-lua-minifyと完全に同じ出力(末尾に改行は付加しない)。
57
+ sourceNode.add("\n--[[\n" + marker + "\n]]");
58
+ } else if (style === "line") {
59
+ sourceNode.add("\n-- " + marker + "\n");
60
+ } else {
61
+ sourceNode.add("\n" + marker + "\n");
62
+ }
63
+
64
+ const sourceAndMap = sourceNode.toStringWithSourceMap({
65
+ file: path.basename(minFileName),
66
+ });
67
+ return {
68
+ code: sourceAndMap.code,
69
+ map: JSON.stringify(sourceAndMap.map),
70
+ };
71
+ }
package/src/renamer.ts ADDED
@@ -0,0 +1,134 @@
1
+ // Renameパス(#20): Resolveパス(#19)が構築したシンボルテーブルをもとに、
2
+ // シンボル単位で短縮識別子を割り当てる。printer(ast2lua.ts)は出力時の
3
+ // その場リネームを行わず、このパスが決めた名前を参照するだけになる。
4
+ //
5
+ // 割当は2段階で行う。
6
+ // 1. スロット割当: スコープ木を辿り、各シンボルに整数のスロットを割り当てる。
7
+ // 兄弟スコープ同士(親子関係にないスコープ同士)は生存区間が重ならないため、
8
+ // 同じスロットを再利用できる(圧縮率の向上)。同一スコープ内のシンボル同士・
9
+ // 祖先スコープのシンボルとは必ず異なるスロットになるため、シンボルの生存
10
+ // スコープ内で名前が衝突することはない。
11
+ // 2. 名前割当: スロットごとの参照回数(頻度)を集計し、頻度の高いスロットから
12
+ // 順に短い名前を割り当てる。頻度の高いシンボルほど短い名前になるため、
13
+ // 同じスロット数でも出力サイズが最小化される。
14
+ //
15
+ // 予約語・"self"・呼び出し側が指定する予約名(グローバル参照やStormworks APIなど)
16
+ // は、どのシンボルにも割り当てられない。
17
+ import Parser from "luaparse";
18
+ import { Scope, Symbol, ResolveResult } from "./resolver";
19
+ import { IDENTIFIER_PARTS, isKeyword } from "./ast2lua";
20
+
21
+ export interface RenameResult {
22
+ // identifierがResolveパスで解決済みのローカルシンボルに対応する場合、
23
+ // 割り当てられた短縮名を返す。グローバル参照やフィールド名など対応する
24
+ // シンボルが無い場合はundefinedを返す(呼び出し側は元の名前を使う)。
25
+ nameOf(identifier: Parser.Identifier): string | undefined;
26
+ // このモジュールが実際に割り当てた短縮名の集合。requireの展開先が
27
+ // 呼び出し元と同じLuaスコープに直接展開される場合(dofile等、関数で
28
+ // 包まれない展開)があるため、他モジュールの割当と衝突しないよう
29
+ // 呼び出し側(Minifier)はこれを後続モジュールの予約名に積み増す。
30
+ readonly usedNames: ReadonlySet<string>;
31
+ }
32
+
33
+ function isAvailable(id: string, reserved: ReadonlySet<string>): boolean {
34
+ return id !== "self" && !isKeyword(id) && !reserved.has(id);
35
+ }
36
+
37
+ // 0始まりのカウンタから短縮名候補を生成する(バイジェクティブ基数記数法)。
38
+ // 通常の位取り記数法と違い同じ文字列を2つのカウンタ値が指すことがないため、
39
+ // カウンタを増やし続けるだけで重複なく識別子候補を列挙できる。
40
+ function generateCandidate(counter: number): string {
41
+ const l = IDENTIFIER_PARTS.length;
42
+ let num = counter + 1;
43
+ let id = "";
44
+ while (num > 0) {
45
+ const rem = (num - 1) % l;
46
+ id = IDENTIFIER_PARTS[rem] + id;
47
+ num = Math.floor((num - 1) / l);
48
+ }
49
+ return id;
50
+ }
51
+
52
+ /**
53
+ * スコープ木のDFSでシンボルごとにスロット番号を割り当てる。
54
+ * `active`は祖先スコープ(自分を含む)で既に使われているスロットの集合。
55
+ * 兄弟スコープには同じ`active`のコピーが渡されるため、互いの割当は影響しない。
56
+ */
57
+ function assignSlots(
58
+ scope: Scope,
59
+ active: ReadonlySet<number>,
60
+ ): Map<Symbol, number> {
61
+ const slotOf = new Map<Symbol, number>();
62
+ const used = new Set(active);
63
+
64
+ scope.symbols.forEach((symbol) => {
65
+ let slot = 0;
66
+ while (used.has(slot)) {
67
+ slot++;
68
+ }
69
+ slotOf.set(symbol, slot);
70
+ used.add(slot);
71
+ });
72
+
73
+ scope.children.forEach((child) => {
74
+ assignSlots(child, used).forEach((slot, symbol) => {
75
+ slotOf.set(symbol, slot);
76
+ });
77
+ });
78
+
79
+ return slotOf;
80
+ }
81
+
82
+ export function assignRenames(
83
+ resolveResult: ResolveResult,
84
+ reserved: ReadonlySet<string>,
85
+ ): RenameResult {
86
+ const slotOf = assignSlots(resolveResult.chunkScope, new Set());
87
+
88
+ // スロットの通算参照回数(宣言自体も1回として数える)を集計する。
89
+ const weightOfSlot = new Map<number, number>();
90
+ resolveResult.symbols.forEach((symbol) => {
91
+ const slot = slotOf.get(symbol);
92
+ if (slot === undefined) {
93
+ return;
94
+ }
95
+ const weight = symbol.references.length + 1;
96
+ weightOfSlot.set(slot, (weightOfSlot.get(slot) ?? 0) + weight);
97
+ });
98
+
99
+ const orderedSlots = [...weightOfSlot.keys()].sort(
100
+ (a, b) => (weightOfSlot.get(b) ?? 0) - (weightOfSlot.get(a) ?? 0),
101
+ );
102
+
103
+ const nameOfSlot = new Map<number, string>();
104
+ let counter = 0;
105
+ orderedSlots.forEach((slot) => {
106
+ let candidate: string;
107
+ do {
108
+ candidate = generateCandidate(counter++);
109
+ } while (!isAvailable(candidate, reserved));
110
+ nameOfSlot.set(slot, candidate);
111
+ });
112
+
113
+ const nameOfSymbol = new Map<Symbol, string>();
114
+ slotOf.forEach((slot, symbol) => {
115
+ const name = nameOfSlot.get(slot);
116
+ if (name !== undefined) {
117
+ nameOfSymbol.set(symbol, name);
118
+ }
119
+ });
120
+
121
+ return {
122
+ nameOf: (identifier) => {
123
+ // メソッド定義の暗黙のselfパラメータは慣習的な名前のため常に維持する
124
+ // (呼び出し側から見える名前ではないため短縮しても安全ではあるが、
125
+ // 可読性のために元の名前のままにする)。
126
+ if (identifier.name === "self") {
127
+ return undefined;
128
+ }
129
+ const symbol = resolveResult.symbolOf(identifier);
130
+ return symbol ? nameOfSymbol.get(symbol) : undefined;
131
+ },
132
+ usedNames: new Set(nameOfSlot.values()),
133
+ };
134
+ }
@@ -0,0 +1,378 @@
1
+ // Resolveパス(#19): 宣言ごとに一意なシンボルを持つスコープツリーを構築し、
2
+ // 識別子の参照を対応する宣言シンボルに解決する。グローバル参照も識別する。
3
+ // このパスは解析のみを行い、ASTやその出力を一切変更しない。
4
+ import Parser from "luaparse";
5
+
6
+ export type SymbolKind = "local" | "param" | "for" | "label";
7
+
8
+ export interface Scope {
9
+ readonly kind: "chunk" | "function" | "block";
10
+ readonly parent: Scope | null;
11
+ readonly children: Scope[];
12
+ // このスコープで直接宣言されたシンボル(宣言順。同名の再宣言もすべて含む)
13
+ readonly symbols: Symbol[];
14
+ }
15
+
16
+ export interface Symbol {
17
+ readonly id: number;
18
+ readonly name: string;
19
+ readonly kind: SymbolKind;
20
+ readonly scope: Scope;
21
+ readonly declaration: Parser.Identifier;
22
+ // このシンボルを指す参照(宣言箇所自体は含まない)
23
+ readonly references: Parser.Identifier[];
24
+ }
25
+
26
+ export interface GlobalBinding {
27
+ readonly name: string;
28
+ readonly references: Parser.Identifier[];
29
+ }
30
+
31
+ export interface ResolveResult {
32
+ readonly chunkScope: Scope;
33
+ // チャンク全体で宣言された全シンボル(宣言順)
34
+ readonly symbols: Symbol[];
35
+ readonly globals: Map<string, GlobalBinding>;
36
+ // 宣言・参照どちらの識別子ノードからも対応するシンボルを引ける。
37
+ // グローバル参照やフィールド名など、シンボルを持たない識別子はundefinedを返す。
38
+ symbolOf(identifier: Parser.Identifier): Symbol | undefined;
39
+ }
40
+
41
+ interface MutableScope extends Scope {
42
+ readonly parent: MutableScope | null;
43
+ readonly children: MutableScope[];
44
+ readonly bindings: Map<string, Symbol>;
45
+ readonly labels: Map<string, Symbol>;
46
+ }
47
+
48
+ export function resolveScopes(chunk: Parser.Chunk): ResolveResult {
49
+ let nextSymbolId = 0;
50
+ const allSymbols: Symbol[] = [];
51
+ const globals = new Map<string, GlobalBinding>();
52
+ const identifierSymbols = new WeakMap<Parser.Identifier, Symbol>();
53
+
54
+ function createScope(
55
+ kind: Scope["kind"],
56
+ parent: MutableScope | null,
57
+ ): MutableScope {
58
+ const scope: MutableScope = {
59
+ kind,
60
+ parent,
61
+ children: [],
62
+ symbols: [],
63
+ bindings: new Map(),
64
+ labels: new Map(),
65
+ };
66
+ parent?.children.push(scope);
67
+ return scope;
68
+ }
69
+
70
+ function declare(
71
+ scope: MutableScope,
72
+ node: Parser.Identifier,
73
+ kind: SymbolKind,
74
+ ): Symbol {
75
+ const symbol: Symbol = {
76
+ id: nextSymbolId++,
77
+ name: node.name,
78
+ kind,
79
+ scope,
80
+ declaration: node,
81
+ references: [],
82
+ };
83
+ // 同名の再宣言はスコープ内の以後の参照から見た束縛を上書きする(Luaの通常のシャドーイング)
84
+ scope.symbols.push(symbol);
85
+ scope.bindings.set(node.name, symbol);
86
+ allSymbols.push(symbol);
87
+ identifierSymbols.set(node, symbol);
88
+ return symbol;
89
+ }
90
+
91
+ function declareLabel(scope: MutableScope, node: Parser.Identifier): Symbol {
92
+ const symbol: Symbol = {
93
+ id: nextSymbolId++,
94
+ name: node.name,
95
+ kind: "label",
96
+ scope,
97
+ declaration: node,
98
+ references: [],
99
+ };
100
+ scope.symbols.push(symbol);
101
+ scope.labels.set(node.name, symbol);
102
+ allSymbols.push(symbol);
103
+ identifierSymbols.set(node, symbol);
104
+ return symbol;
105
+ }
106
+
107
+ function lookupBinding(
108
+ scope: MutableScope,
109
+ name: string,
110
+ ): Symbol | undefined {
111
+ for (let s: MutableScope | null = scope; s; s = s.parent) {
112
+ const found = s.bindings.get(name);
113
+ if (found) {
114
+ return found;
115
+ }
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ function lookupLabel(scope: MutableScope, name: string): Symbol | undefined {
121
+ for (let s: MutableScope | null = scope; s; s = s.parent) {
122
+ const found = s.labels.get(name);
123
+ if (found) {
124
+ return found;
125
+ }
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ function reference(scope: MutableScope, node: Parser.Identifier) {
131
+ const symbol = lookupBinding(scope, node.name);
132
+ if (symbol) {
133
+ symbol.references.push(node);
134
+ identifierSymbols.set(node, symbol);
135
+ return;
136
+ }
137
+ let binding = globals.get(node.name);
138
+ if (!binding) {
139
+ binding = { name: node.name, references: [] };
140
+ globals.set(node.name, binding);
141
+ }
142
+ binding.references.push(node);
143
+ }
144
+
145
+ // ラベルはブロック内での宣言位置に関わらずブロック全体から参照できる
146
+ // (前方goto)ため、通常の宣言文と違い先読みで登録する。
147
+ function hoistLabels(body: Parser.Statement[], scope: MutableScope) {
148
+ body.forEach((statement) => {
149
+ if (statement.type === "LabelStatement") {
150
+ declareLabel(scope, statement.label);
151
+ }
152
+ });
153
+ }
154
+
155
+ function resolveBlock(body: Parser.Statement[], scope: MutableScope) {
156
+ hoistLabels(body, scope);
157
+ body.forEach((statement) => {
158
+ resolveStatement(statement, scope);
159
+ });
160
+ }
161
+
162
+ function resolveStatement(
163
+ statement: Parser.Statement,
164
+ scope: MutableScope,
165
+ ): void {
166
+ switch (statement.type) {
167
+ case "LocalStatement":
168
+ // 初期化式は新しいローカルが宣言される前の束縛で解決する
169
+ // (`local x = x` の右辺は外側のxを指す)
170
+ statement.init.forEach((expr) => {
171
+ resolveExpression(expr, scope);
172
+ });
173
+ statement.variables.forEach((v) => declare(scope, v, "local"));
174
+ return;
175
+ case "AssignmentStatement":
176
+ statement.init.forEach((expr) => {
177
+ resolveExpression(expr, scope);
178
+ });
179
+ statement.variables.forEach((v) => {
180
+ if (v.type === "Identifier") {
181
+ reference(scope, v);
182
+ } else {
183
+ resolveExpression(v, scope);
184
+ }
185
+ });
186
+ return;
187
+ case "CallStatement":
188
+ resolveExpression(statement.expression, scope);
189
+ return;
190
+ case "DoStatement": {
191
+ const inner = createScope("block", scope);
192
+ resolveBlock(statement.body, inner);
193
+ return;
194
+ }
195
+ case "WhileStatement": {
196
+ resolveExpression(statement.condition, scope);
197
+ const inner = createScope("block", scope);
198
+ resolveBlock(statement.body, inner);
199
+ return;
200
+ }
201
+ case "RepeatStatement": {
202
+ // `until`の条件式は本体で宣言されたローカルを参照できる
203
+ const inner = createScope("block", scope);
204
+ resolveBlock(statement.body, inner);
205
+ resolveExpression(statement.condition, inner);
206
+ return;
207
+ }
208
+ case "IfStatement":
209
+ statement.clauses.forEach((clause) => {
210
+ if (clause.type !== "ElseClause") {
211
+ resolveExpression(clause.condition, scope);
212
+ }
213
+ const inner = createScope("block", scope);
214
+ resolveBlock(clause.body, inner);
215
+ });
216
+ return;
217
+ case "ForNumericStatement": {
218
+ resolveExpression(statement.start, scope);
219
+ resolveExpression(statement.end, scope);
220
+ if (statement.step) {
221
+ resolveExpression(statement.step, scope);
222
+ }
223
+ const inner = createScope("block", scope);
224
+ declare(inner, statement.variable, "for");
225
+ resolveBlock(statement.body, inner);
226
+ return;
227
+ }
228
+ case "ForGenericStatement": {
229
+ statement.iterators.forEach((iterator) => {
230
+ resolveExpression(iterator, scope);
231
+ });
232
+ const inner = createScope("block", scope);
233
+ statement.variables.forEach((v) => declare(inner, v, "for"));
234
+ resolveBlock(statement.body, inner);
235
+ return;
236
+ }
237
+ case "FunctionDeclaration":
238
+ resolveFunctionDeclaration(statement, scope);
239
+ return;
240
+ case "ReturnStatement":
241
+ statement.arguments.forEach((argument) => {
242
+ resolveExpression(argument, scope);
243
+ });
244
+ return;
245
+ case "BreakStatement":
246
+ return;
247
+ case "LabelStatement":
248
+ // hoistLabelsで宣言済みのため、ここでは何もしない
249
+ return;
250
+ case "GotoStatement": {
251
+ const symbol = lookupLabel(scope, statement.label.name);
252
+ if (symbol) {
253
+ symbol.references.push(statement.label);
254
+ identifierSymbols.set(statement.label, symbol);
255
+ }
256
+ return;
257
+ }
258
+ default: {
259
+ const exhaustive: never = statement;
260
+ throw new TypeError(
261
+ "Unknown statement type: `" + JSON.stringify(exhaustive) + "`",
262
+ );
263
+ }
264
+ }
265
+ }
266
+
267
+ function resolveFunctionDeclaration(
268
+ fn: Parser.FunctionDeclaration,
269
+ scope: MutableScope,
270
+ ) {
271
+ if (fn.identifier) {
272
+ if (fn.identifier.type === "Identifier") {
273
+ if (fn.isLocal) {
274
+ // `local function`は再帰呼び出しのため、本体を解決する前に自身を宣言する
275
+ declare(scope, fn.identifier, "local");
276
+ } else {
277
+ // 非local: 既存のローカル/グローバルへの代入として扱う(新規宣言ではない)
278
+ reference(scope, fn.identifier);
279
+ }
280
+ } else {
281
+ resolveExpression(fn.identifier, scope);
282
+ }
283
+ }
284
+ const inner = createScope("function", scope);
285
+ fn.parameters.forEach((parameter) => {
286
+ if (parameter.type === "Identifier") {
287
+ declare(inner, parameter, "param");
288
+ }
289
+ });
290
+ resolveBlock(fn.body, inner);
291
+ }
292
+
293
+ function resolveExpression(
294
+ expr: Parser.Expression,
295
+ scope: MutableScope,
296
+ ): void {
297
+ switch (expr.type) {
298
+ case "Identifier":
299
+ reference(scope, expr);
300
+ return;
301
+ case "StringLiteral":
302
+ case "NumericLiteral":
303
+ case "BooleanLiteral":
304
+ case "NilLiteral":
305
+ case "VarargLiteral":
306
+ return;
307
+ case "LogicalExpression":
308
+ case "BinaryExpression":
309
+ resolveExpression(expr.left, scope);
310
+ resolveExpression(expr.right, scope);
311
+ return;
312
+ case "UnaryExpression":
313
+ resolveExpression(expr.argument, scope);
314
+ return;
315
+ case "CallExpression":
316
+ resolveExpression(expr.base, scope);
317
+ expr.arguments.forEach((argument) => {
318
+ resolveExpression(argument, scope);
319
+ });
320
+ return;
321
+ case "TableCallExpression":
322
+ resolveExpression(expr.base, scope);
323
+ resolveExpression(expr.arguments, scope);
324
+ return;
325
+ case "StringCallExpression":
326
+ resolveExpression(expr.base, scope);
327
+ resolveExpression(expr.argument, scope);
328
+ return;
329
+ case "IndexExpression":
330
+ resolveExpression(expr.base, scope);
331
+ resolveExpression(expr.index, scope);
332
+ return;
333
+ case "MemberExpression":
334
+ resolveExpression(expr.base, scope);
335
+ // フィールド名(`.identifier`)は変数参照ではないため解決しない
336
+ return;
337
+ case "FunctionDeclaration": {
338
+ const inner = createScope("function", scope);
339
+ expr.parameters.forEach((parameter) => {
340
+ if (parameter.type === "Identifier") {
341
+ declare(inner, parameter, "param");
342
+ }
343
+ });
344
+ resolveBlock(expr.body, inner);
345
+ return;
346
+ }
347
+ case "TableConstructorExpression":
348
+ expr.fields.forEach((field) => {
349
+ if (field.type === "TableKey") {
350
+ resolveExpression(field.key, scope);
351
+ resolveExpression(field.value, scope);
352
+ } else if (field.type === "TableValue") {
353
+ resolveExpression(field.value, scope);
354
+ } else {
355
+ // TableKeyString: キー名(`{ key = value }`のkey)は変数参照ではない
356
+ resolveExpression(field.value, scope);
357
+ }
358
+ });
359
+ return;
360
+ default: {
361
+ const exhaustive: never = expr;
362
+ throw new TypeError(
363
+ "Unknown expression type: `" + JSON.stringify(exhaustive) + "`",
364
+ );
365
+ }
366
+ }
367
+ }
368
+
369
+ const chunkScope = createScope("chunk", null);
370
+ resolveBlock(chunk.body, chunkScope);
371
+
372
+ return {
373
+ chunkScope,
374
+ symbols: allSymbols,
375
+ globals,
376
+ symbolOf: (identifier) => identifierSymbols.get(identifier),
377
+ };
378
+ }
@@ -0,0 +1,19 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Minifier } from "../src/minifier";
4
+ import { LUAPARSE_SETTINGS, fixtureEntryPath } from "./lib/helpers";
5
+
6
+ // require/dofileの参照グラフに循環がある場合、Linkパスがエラーを投げて
7
+ // 出力を開始しないことを検証する(#18)。
8
+
9
+ void test("circular require is rejected with a clear error", () => {
10
+ const minifier = new Minifier(
11
+ fixtureEntryPath("circular-require"),
12
+ LUAPARSE_SETTINGS,
13
+ { moduleLikeLua: true },
14
+ );
15
+ assert.throws(
16
+ () => minifier.parse(),
17
+ /Circular require\/dofile detected: a -> b -> a/,
18
+ );
19
+ });
@@ -0,0 +1,2 @@
1
+ require("mod")
2
+ print("done")
@@ -0,0 +1 @@
1
+ print("hello from mod")
@@ -0,0 +1,10 @@
1
+ local a, b, c = 1, 2, 3
2
+
3
+ print(a | b & c)
4
+ print((a | b) & c)
5
+ print(a & b | c)
6
+ print(a ~ b & c)
7
+ print(a << b + c)
8
+ print((a << b) + c)
9
+ print(a // b // c)
10
+ print(~a & b)
@@ -0,0 +1,2 @@
1
+ local b = require("b")
2
+ return { fromA = b }
@@ -0,0 +1,2 @@
1
+ local a = require("a")
2
+ return { fromB = a }
@@ -0,0 +1,2 @@
1
+ local a = require("a")
2
+ print(a)
@@ -0,0 +1 @@
1
+ print("hello from greet")
@@ -0,0 +1,2 @@
1
+ dofile("greet")
2
+ print("done")
@@ -0,0 +1,2 @@
1
+ local valueAlpha = "dep_alpha"
2
+ return { id = valueAlpha }
@@ -0,0 +1,2 @@
1
+ local valueBravo = "dep_bravo"
2
+ return { id = valueBravo }
@@ -0,0 +1,2 @@
1
+ local valueCharlie = "dep_charlie"
2
+ return { id = valueCharlie }
@@ -0,0 +1,2 @@
1
+ local valueDelta = "dep_delta"
2
+ return { id = valueDelta }
@@ -0,0 +1,2 @@
1
+ local valueEcho = "dep_echo"
2
+ return { id = valueEcho }
@@ -0,0 +1,2 @@
1
+ local valueFoxtrot = "dep_foxtrot"
2
+ return { id = valueFoxtrot }
@@ -0,0 +1,2 @@
1
+ local valueGolf = "dep_golf"
2
+ return { id = valueGolf }
@@ -0,0 +1,2 @@
1
+ local valueHotel = "dep_hotel"
2
+ return { id = valueHotel }
@@ -0,0 +1,2 @@
1
+ local valueIndia = "dep_india"
2
+ return { id = valueIndia }
@@ -0,0 +1,2 @@
1
+ local valueJuliet = "dep_juliet"
2
+ return { id = valueJuliet }