storm-lua-minify 0.1.2 → 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.
- package/.github/workflows/ci.yml +39 -0
- package/.github/workflows/publish.yml +39 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +1 -0
- package/LICENSE +21 -21
- package/README.md +37 -15
- package/dist/ast2lua.js +188 -85
- package/dist/cli.js +17 -7
- package/dist/index.js +3 -19
- package/dist/linker.js +96 -0
- package/dist/minifier.js +176 -51
- package/dist/output.js +33 -0
- package/dist/renamer.js +87 -0
- package/dist/resolver.js +303 -0
- package/eslint.config.js +29 -0
- package/package.json +33 -27
- package/src/ast2lua.ts +940 -812
- package/src/cli.ts +86 -56
- package/src/linker.ts +108 -0
- package/src/minifier.ts +284 -114
- package/src/output.ts +71 -0
- package/src/renamer.ts +134 -0
- package/src/resolver.ts +378 -0
- package/test/circular-require.test.ts +19 -0
- package/test/fixtures/bare-require/main.lua +2 -0
- package/test/fixtures/bare-require/mod.lua +1 -0
- package/test/fixtures/bitwise-precedence/main.lua +10 -0
- package/test/fixtures/circular-require/a.lua +2 -0
- package/test/fixtures/circular-require/b.lua +2 -0
- package/test/fixtures/circular-require/main.lua +2 -0
- package/test/fixtures/dofile/greet.lua +1 -0
- package/test/fixtures/dofile/main.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_delta.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_echo.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_golf.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_india.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +2 -0
- package/test/fixtures/entry-scope-many-requires/main.lua +25 -0
- package/test/fixtures/multi-require/common.lua +1 -0
- package/test/fixtures/multi-require/main.lua +3 -0
- package/test/fixtures/nested-module/main.lua +2 -0
- package/test/fixtures/nested-module/sub/deep.lua +1 -0
- package/test/fixtures/require-call/main.lua +2 -0
- package/test/fixtures/require-call/mod.lua +5 -0
- package/test/fixtures/require-in-expression/main.lua +1 -0
- package/test/fixtures/require-in-expression/mod.lua +1 -0
- package/test/fixtures/require-string-call/main.lua +2 -0
- package/test/fixtures/require-string-call/mod.lua +5 -0
- package/test/fixtures/single-file/main.lua +15 -0
- package/test/identifier-collision.test.ts +28 -0
- package/test/lib/collision.ts +131 -0
- package/test/lib/helpers.ts +124 -0
- package/test/no-rename.test.ts +19 -0
- package/test/output.test.ts +95 -0
- package/test/precedence.test.ts +28 -0
- package/test/renamer.test.ts +130 -0
- package/test/resolver.test.ts +206 -0
- package/test/roundtrip.test.ts +26 -0
- package/test/snapshot.test.ts +27 -0
- package/test/snapshots/bare-require.sl.lua +1 -0
- package/test/snapshots/bitwise-precedence.sl.lua +2 -0
- package/test/snapshots/dofile.sl.lua +1 -0
- package/test/snapshots/entry-scope-many-requires.m.lua +14 -0
- package/test/snapshots/multi-require.m.lua +4 -0
- package/test/snapshots/multi-require.sl.lua +1 -0
- package/test/snapshots/nested-module.m.lua +4 -0
- package/test/snapshots/require-call.m.lua +5 -0
- package/test/snapshots/require-call.sl.lua +1 -0
- package/test/snapshots/require-in-expression.sl.lua +1 -0
- package/test/snapshots/require-string-call.m.lua +5 -0
- package/test/snapshots/single-file.sl.lua +6 -0
- package/test/sourcemap.test.ts +105 -0
- package/tsconfig.eslint.json +8 -0
- package/tsconfig.json +111 -109
- package/.eslintrc.json +0 -20
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
local depAlpha = require("dep_alpha")
|
|
2
|
+
local depBravo = require("dep_bravo")
|
|
3
|
+
local depCharlie = require("dep_charlie")
|
|
4
|
+
local depDelta = require("dep_delta")
|
|
5
|
+
local depEcho = require("dep_echo")
|
|
6
|
+
local depFoxtrot = require("dep_foxtrot")
|
|
7
|
+
local depGolf = require("dep_golf")
|
|
8
|
+
local depHotel = require("dep_hotel")
|
|
9
|
+
local depIndia = require("dep_india")
|
|
10
|
+
local depJuliet = require("dep_juliet")
|
|
11
|
+
local depKilo = require("dep_kilo")
|
|
12
|
+
|
|
13
|
+
print(
|
|
14
|
+
depAlpha,
|
|
15
|
+
depBravo,
|
|
16
|
+
depCharlie,
|
|
17
|
+
depDelta,
|
|
18
|
+
depEcho,
|
|
19
|
+
depFoxtrot,
|
|
20
|
+
depGolf,
|
|
21
|
+
depHotel,
|
|
22
|
+
depIndia,
|
|
23
|
+
depJuliet,
|
|
24
|
+
depKilo
|
|
25
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
return { value = 42 }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
return { value = 1 }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
print(require("mod").hello())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
return { hello = function() return "hello" end }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { WORKING_CASES, KNOWN_BUG_CASES, runMinifier } from "./lib/helpers";
|
|
4
|
+
import { findIdentifierCollisions } from "./lib/collision";
|
|
5
|
+
|
|
6
|
+
// 出力コードの同一スコープ内で、同じ短縮識別子が異なるオリジナル識別子に
|
|
7
|
+
// 割り当てられていないことを検証する(#12の回帰防止)。
|
|
8
|
+
|
|
9
|
+
for (const c of WORKING_CASES) {
|
|
10
|
+
void test(`identifier collision: ${c.label}`, async () => {
|
|
11
|
+
const { code, map } = runMinifier(c);
|
|
12
|
+
const collisions = await findIdentifierCollisions(code, map);
|
|
13
|
+
assert.deepEqual(collisions, []);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const c of KNOWN_BUG_CASES) {
|
|
18
|
+
const issue = String(c.issue);
|
|
19
|
+
void test(
|
|
20
|
+
`identifier collision (known bug, issue #${issue}): ${c.label}`,
|
|
21
|
+
{ todo: `#${issue} の本修正待ち` },
|
|
22
|
+
async () => {
|
|
23
|
+
const { code, map } = runMinifier(c);
|
|
24
|
+
const collisions = await findIdentifierCollisions(code, map);
|
|
25
|
+
assert.deepEqual(collisions, []);
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import Parser from "luaparse";
|
|
2
|
+
import { RawSourceMap, SourceMapConsumer } from "source-map";
|
|
3
|
+
|
|
4
|
+
export interface IdentifierCollision {
|
|
5
|
+
scope: string;
|
|
6
|
+
shortName: string;
|
|
7
|
+
originalNames: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Declaration {
|
|
11
|
+
name: string;
|
|
12
|
+
original: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 出力(minify後)のLuaコードを対象に、同一スコープ内で同じ短縮名が
|
|
16
|
+
// 「別のオリジナル識別子」に割り当てられていないかを検証する(#12の回帰防止)。
|
|
17
|
+
//
|
|
18
|
+
// 既知の制限(テストハーネスの簡易実装):
|
|
19
|
+
// - 文(Statement)としてのスコープのみを辿る。式中の無名関数
|
|
20
|
+
// (例: `local t = { f = function() ... end }`)の内部は対象外。
|
|
21
|
+
export async function findIdentifierCollisions(
|
|
22
|
+
code: string,
|
|
23
|
+
rawMap: RawSourceMap,
|
|
24
|
+
): Promise<IdentifierCollision[]> {
|
|
25
|
+
const ast = Parser.parse(code, { luaVersion: "5.3", locations: true });
|
|
26
|
+
const collisions: IdentifierCollision[] = [];
|
|
27
|
+
|
|
28
|
+
await SourceMapConsumer.with(rawMap, null, (consumer) => {
|
|
29
|
+
const originalNameFor = (node: {
|
|
30
|
+
loc?: Parser.Node["loc"];
|
|
31
|
+
}): string | null => {
|
|
32
|
+
if (!node.loc) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const pos = consumer.originalPositionFor({
|
|
36
|
+
line: node.loc.start.line,
|
|
37
|
+
column: node.loc.start.column,
|
|
38
|
+
});
|
|
39
|
+
return pos.name;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const checkScope = (scopeName: string, declarations: Declaration[]) => {
|
|
43
|
+
const byShortName = new Map<string, Set<string>>();
|
|
44
|
+
for (const decl of declarations) {
|
|
45
|
+
if (decl.original === null) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!byShortName.has(decl.name)) {
|
|
49
|
+
byShortName.set(decl.name, new Set());
|
|
50
|
+
}
|
|
51
|
+
byShortName.get(decl.name)?.add(decl.original);
|
|
52
|
+
}
|
|
53
|
+
for (const [shortName, originalNames] of byShortName) {
|
|
54
|
+
if (originalNames.size > 1) {
|
|
55
|
+
collisions.push({
|
|
56
|
+
scope: scopeName,
|
|
57
|
+
shortName,
|
|
58
|
+
originalNames: [...originalNames],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let scopeCounter = 0;
|
|
65
|
+
|
|
66
|
+
const walkBlock = (scopeLabel: string, body: Parser.Statement[]) => {
|
|
67
|
+
const declarations: Declaration[] = [];
|
|
68
|
+
for (const statement of body) {
|
|
69
|
+
collectFromStatement(statement, declarations);
|
|
70
|
+
}
|
|
71
|
+
checkScope(scopeLabel, declarations);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const collectFromStatement = (
|
|
75
|
+
statement: Parser.Statement,
|
|
76
|
+
declarations: Declaration[],
|
|
77
|
+
) => {
|
|
78
|
+
switch (statement.type) {
|
|
79
|
+
case "LocalStatement":
|
|
80
|
+
for (const v of statement.variables) {
|
|
81
|
+
declarations.push({ name: v.name, original: originalNameFor(v) });
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case "ForNumericStatement":
|
|
85
|
+
declarations.push({
|
|
86
|
+
name: statement.variable.name,
|
|
87
|
+
original: originalNameFor(statement.variable),
|
|
88
|
+
});
|
|
89
|
+
walkBlock(`for#${String(scopeCounter++)}`, statement.body);
|
|
90
|
+
break;
|
|
91
|
+
case "ForGenericStatement":
|
|
92
|
+
for (const v of statement.variables) {
|
|
93
|
+
declarations.push({ name: v.name, original: originalNameFor(v) });
|
|
94
|
+
}
|
|
95
|
+
walkBlock(`for#${String(scopeCounter++)}`, statement.body);
|
|
96
|
+
break;
|
|
97
|
+
case "FunctionDeclaration": {
|
|
98
|
+
const paramDeclarations: Declaration[] = statement.parameters
|
|
99
|
+
.filter((p): p is Parser.Identifier => p.type === "Identifier")
|
|
100
|
+
.map((p) => ({ name: p.name, original: originalNameFor(p) }));
|
|
101
|
+
const bodyDeclarations = [...paramDeclarations];
|
|
102
|
+
for (const inner of statement.body) {
|
|
103
|
+
collectFromStatement(inner, bodyDeclarations);
|
|
104
|
+
}
|
|
105
|
+
checkScope(`function#${String(scopeCounter++)}`, bodyDeclarations);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "DoStatement":
|
|
109
|
+
walkBlock(`do#${String(scopeCounter++)}`, statement.body);
|
|
110
|
+
break;
|
|
111
|
+
case "WhileStatement":
|
|
112
|
+
walkBlock(`while#${String(scopeCounter++)}`, statement.body);
|
|
113
|
+
break;
|
|
114
|
+
case "RepeatStatement":
|
|
115
|
+
walkBlock(`repeat#${String(scopeCounter++)}`, statement.body);
|
|
116
|
+
break;
|
|
117
|
+
case "IfStatement":
|
|
118
|
+
for (const clause of statement.clauses) {
|
|
119
|
+
walkBlock(`if#${String(scopeCounter++)}`, clause.body);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
walkBlock("chunk", ast.body);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return collisions;
|
|
131
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Options } from "luaparse";
|
|
3
|
+
import { RawSourceMap } from "source-map";
|
|
4
|
+
import { Minifier, MinifierMode } from "../../src/minifier";
|
|
5
|
+
|
|
6
|
+
export const FIXTURES_DIR = path.join(__dirname, "..", "fixtures");
|
|
7
|
+
export const SNAPSHOTS_DIR = path.join(__dirname, "..", "snapshots");
|
|
8
|
+
|
|
9
|
+
export const LUAPARSE_SETTINGS: Partial<Options> = {
|
|
10
|
+
locations: true,
|
|
11
|
+
luaVersion: "5.3",
|
|
12
|
+
ranges: true,
|
|
13
|
+
scope: true,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface FixtureCase {
|
|
17
|
+
label: string;
|
|
18
|
+
fixture: string;
|
|
19
|
+
entry?: string;
|
|
20
|
+
mode: MinifierMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KnownBugCase extends FixtureCase {
|
|
24
|
+
issue: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function fixtureEntryPath(fixture: string, entry = "main.lua"): string {
|
|
28
|
+
return path.join(FIXTURES_DIR, fixture, entry);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function slug(c: Pick<FixtureCase, "fixture" | "mode">): string {
|
|
32
|
+
return `${c.fixture}.${c.mode.moduleLikeLua ? "m" : "sl"}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function runMinifier(c: FixtureCase): {
|
|
36
|
+
code: string;
|
|
37
|
+
map: RawSourceMap;
|
|
38
|
+
} {
|
|
39
|
+
const minifier = new Minifier(
|
|
40
|
+
fixtureEntryPath(c.fixture, c.entry),
|
|
41
|
+
LUAPARSE_SETTINGS,
|
|
42
|
+
c.mode,
|
|
43
|
+
);
|
|
44
|
+
const sourceNode = minifier.parse();
|
|
45
|
+
const { code, map } = sourceNode.toStringWithSourceMap({
|
|
46
|
+
file: c.fixture + ".min.lua",
|
|
47
|
+
});
|
|
48
|
+
return { code, map: map.toJSON() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// require() / require "m" / dofile が正しく解決される、動作が確認できているケース。
|
|
52
|
+
// スナップショット・ラウンドトリップ・識別子衝突検知の全テストで「衝突・構文エラーが無い」ことを期待する。
|
|
53
|
+
export const WORKING_CASES: FixtureCase[] = [
|
|
54
|
+
{
|
|
55
|
+
label: "単一ファイル",
|
|
56
|
+
fixture: "single-file",
|
|
57
|
+
mode: { moduleLikeLua: false },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: 'require("m") 構文 (-m モード)',
|
|
61
|
+
fixture: "require-call",
|
|
62
|
+
mode: { moduleLikeLua: true },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "dofile (SLモード)",
|
|
66
|
+
fixture: "dofile",
|
|
67
|
+
mode: { moduleLikeLua: false },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "同一モジュールの多重require (-m モード)",
|
|
71
|
+
fixture: "multi-require",
|
|
72
|
+
mode: { moduleLikeLua: true },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "エントリ直下で多数のrequire (-m モード, #12回帰防止)",
|
|
76
|
+
fixture: "entry-scope-many-requires",
|
|
77
|
+
mode: { moduleLikeLua: true },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "ビット演算子・整数除算の優先順序 (#27回帰防止)",
|
|
81
|
+
fixture: "bitwise-precedence",
|
|
82
|
+
mode: { moduleLikeLua: false },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label:
|
|
86
|
+
'require "m" (括弧なし文字列呼び出し構文) が解決される (-m モード, #18/#11修正確認)',
|
|
87
|
+
fixture: "require-string-call",
|
|
88
|
+
mode: { moduleLikeLua: true },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
label:
|
|
92
|
+
"SLモードでrequireしたモジュールが文としてその場展開される (#18/#11修正確認)",
|
|
93
|
+
fixture: "require-call",
|
|
94
|
+
mode: { moduleLikeLua: false },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label:
|
|
98
|
+
"SLモードで同一モジュールを多重requireすると呼び出しごとに独立して文展開される (#18/#11修正確認)",
|
|
99
|
+
fixture: "multi-require",
|
|
100
|
+
mode: { moduleLikeLua: false },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label:
|
|
104
|
+
"SLモードで戻り値を使わない単独のrequire文はdofileと同様functionで包まずそのまま展開される (#29対応確認)",
|
|
105
|
+
fixture: "bare-require",
|
|
106
|
+
mode: { moduleLikeLua: false },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label:
|
|
110
|
+
"SLモードでrequireが式の一部(ネスト位置)に現れる場合はIIFEにフォールバックする (#29対応確認)",
|
|
111
|
+
fixture: "require-in-expression",
|
|
112
|
+
mode: { moduleLikeLua: false },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label:
|
|
116
|
+
"ドット区切りのモジュール名(サブディレクトリ)が解決される (-m モード, #21 Source Map確認)",
|
|
117
|
+
fixture: "nested-module",
|
|
118
|
+
mode: { moduleLikeLua: true },
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// 既知バグの再現ケース。現状は failing のため `test.todo` で登録する。
|
|
123
|
+
// 修正が入ったら .todo を外して WORKING_CASES に合流させること。
|
|
124
|
+
export const KNOWN_BUG_CASES: KnownBugCase[] = [];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { runMinifier } from "./lib/helpers";
|
|
4
|
+
|
|
5
|
+
// #20: CLIオプション(--no-rename)相当の`mode.rename = false`で識別子の短縮を
|
|
6
|
+
// 無効化できることを確認する(デバッグ用途)。局所変数名は元のまま出力され、
|
|
7
|
+
// 空白の除去などそれ以外のminify処理はそのまま行われる。
|
|
8
|
+
void test("mode.rename = false disables identifier shortening", () => {
|
|
9
|
+
const { code } = runMinifier({
|
|
10
|
+
label: "single-file (no rename)",
|
|
11
|
+
fixture: "single-file",
|
|
12
|
+
mode: { moduleLikeLua: false, rename: false },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.match(code, /local function add\(first,second\)/);
|
|
16
|
+
assert.match(code, /local total=0/);
|
|
17
|
+
assert.match(code, /for index=1,10 do total=add\(total,index\)end/);
|
|
18
|
+
assert.match(code, /print\(total,i\)/);
|
|
19
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import Parser from "luaparse";
|
|
4
|
+
import { SourceNode } from "source-map";
|
|
5
|
+
import { buildMinifiedOutput } from "../src/output";
|
|
6
|
+
|
|
7
|
+
function makeNode(): SourceNode {
|
|
8
|
+
return new SourceNode(1, 0, "main.lua", "print(1)");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
void test("buildMinifiedOutput: mapにfileフィールドが設定される", () => {
|
|
12
|
+
const { map } = buildMinifiedOutput(
|
|
13
|
+
makeNode(),
|
|
14
|
+
"main.min.lua",
|
|
15
|
+
"main.lua.map",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const parsed = JSON.parse(map) as { file?: string };
|
|
19
|
+
assert.equal(parsed.file, "main.min.lua");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// "legacy"(既定・未指定): 旧storm-lua-minifyと完全に同じ複数行の`--[[ ]]`
|
|
23
|
+
// ブロックコメントを出力する。互換性維持のため、既定はこの形式のままにする。
|
|
24
|
+
void test('buildMinifiedOutput: 既定("legacy")は旧バージョンと同じ複数行ブロックコメントを出力し、有効なLuaのままである', () => {
|
|
25
|
+
const { code } = buildMinifiedOutput(
|
|
26
|
+
makeNode(),
|
|
27
|
+
"main.min.lua",
|
|
28
|
+
"main.lua.map",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.equal(code, "print(1)\n--[[\n//# sourceMappingURL=main.lua.map\n]]");
|
|
32
|
+
assert.doesNotThrow(() => Parser.parse(code, { luaVersion: "5.3" }));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
void test('buildMinifiedOutput: sourceMappingUrlStyle: "legacy"を明示しても既定と同じ出力になる', () => {
|
|
36
|
+
const { code } = buildMinifiedOutput(
|
|
37
|
+
makeNode(),
|
|
38
|
+
"main.min.lua",
|
|
39
|
+
"main.lua.map",
|
|
40
|
+
{
|
|
41
|
+
sourceMappingUrlStyle: "legacy",
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
assert.equal(code, "print(1)\n--[[\n//# sourceMappingURL=main.lua.map\n]]");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// "line": 単一行の`--`ラインコメント。Source Map仕様の「アノテーションは
|
|
49
|
+
// 最終行(末尾が空行の場合はその直前の行)に置く」というルールを満たしつつ、
|
|
50
|
+
// 有効なLuaのままにするための妥協案。
|
|
51
|
+
void test('buildMinifiedOutput: sourceMappingUrlStyle: "line"はsourceMappingURLが出力の最終行(末尾空行を除く)になり、有効なLuaのままである', () => {
|
|
52
|
+
const { code } = buildMinifiedOutput(
|
|
53
|
+
makeNode(),
|
|
54
|
+
"main.min.lua",
|
|
55
|
+
"main.lua.map",
|
|
56
|
+
{
|
|
57
|
+
sourceMappingUrlStyle: "line",
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const lines = code.split("\n");
|
|
62
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
63
|
+
lines.pop();
|
|
64
|
+
}
|
|
65
|
+
const lastLine = lines[lines.length - 1];
|
|
66
|
+
|
|
67
|
+
assert.equal(lastLine, "-- //# sourceMappingURL=main.lua.map");
|
|
68
|
+
assert.ok(lastLine.startsWith("--") && !lastLine.startsWith("--[["));
|
|
69
|
+
assert.doesNotThrow(() => Parser.parse(code, { luaVersion: "5.3" }));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// "strict": Luaコメントで一切包まず、Source Map仕様のマーカー文字列
|
|
73
|
+
// (`//# sourceMappingURL=...`)をそのまま出力する。Lua 5.1〜5.4のいずれでも
|
|
74
|
+
// `/`・`//`は二項演算子のトークンであり文の先頭には来られないため、この形式は
|
|
75
|
+
// 有効なLuaと両立できない(調査の結果、回避不能と判断した)。この副作用を
|
|
76
|
+
// テストで固定化しておく。
|
|
77
|
+
void test('buildMinifiedOutput: sourceMappingUrlStyle: "strict"はLuaコメントで包まずアノテーションを出力し、Luaとしては構文エラーになる', () => {
|
|
78
|
+
const { code } = buildMinifiedOutput(
|
|
79
|
+
makeNode(),
|
|
80
|
+
"main.min.lua",
|
|
81
|
+
"main.lua.map",
|
|
82
|
+
{
|
|
83
|
+
sourceMappingUrlStyle: "strict",
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const lines = code.split("\n");
|
|
88
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
89
|
+
lines.pop();
|
|
90
|
+
}
|
|
91
|
+
const lastLine = lines[lines.length - 1];
|
|
92
|
+
|
|
93
|
+
assert.equal(lastLine, "//# sourceMappingURL=main.lua.map");
|
|
94
|
+
assert.throws(() => Parser.parse(code, { luaVersion: "5.3" }));
|
|
95
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { runMinifier } from "./lib/helpers";
|
|
4
|
+
|
|
5
|
+
// #27: PRECEDENCE テーブルにビット演算子(| ~ & << >>)と整数除算(//)が
|
|
6
|
+
// 定義されておらず、比較結果が常に偽になることで必要な括弧が失われ、
|
|
7
|
+
// 意味が変わってしまうバグの回帰防止テスト。
|
|
8
|
+
// 括弧が必要な箇所では保持され、不要な箇所では省略されることを確認する。
|
|
9
|
+
void test("ビット演算子・整数除算の優先順序が壊れない (#27)", () => {
|
|
10
|
+
const { code } = runMinifier({
|
|
11
|
+
label: "bitwise-precedence",
|
|
12
|
+
fixture: "bitwise-precedence",
|
|
13
|
+
mode: { moduleLikeLua: false },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// `&` は `|` より強いので、意味を変えずに括弧を省略できる
|
|
17
|
+
assert.match(code, /print\(a\|b&c\)/);
|
|
18
|
+
// `(a|b)&c` は括弧を落とすと意味が変わるため保持する必要がある
|
|
19
|
+
assert.match(code, /print\(\(a\|b\)&c\)/);
|
|
20
|
+
assert.match(code, /print\(a&b\|c\)/);
|
|
21
|
+
assert.match(code, /print\(a~b&c\)/);
|
|
22
|
+
// `+` は `<<` より強いので括弧を省略できる
|
|
23
|
+
assert.match(code, /print\(a<<b\+c\)/);
|
|
24
|
+
// `(a<<b)+c` は括弧を落とすと意味が変わるため保持する必要がある
|
|
25
|
+
assert.match(code, /print\(\(a<<b\)\+c\)/);
|
|
26
|
+
assert.match(code, /print\(a\/\/b\/\/c\)/);
|
|
27
|
+
assert.match(code, /print\(~a&b\)/);
|
|
28
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import Parser from "luaparse";
|
|
4
|
+
import { resolveScopes } from "../src/resolver";
|
|
5
|
+
import { assignRenames } from "../src/renamer";
|
|
6
|
+
|
|
7
|
+
// Renameパス(#20)の単体テスト。スコープに基づくスロット再利用、頻度順の名前割当、
|
|
8
|
+
// 予約名との非衝突を検証する。
|
|
9
|
+
|
|
10
|
+
function parse(code: string): Parser.Chunk {
|
|
11
|
+
return Parser.parse(code, { luaVersion: "5.3" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
void test("sibling scopes (non-overlapping) reuse the same short name", () => {
|
|
15
|
+
const chunk = parse(`
|
|
16
|
+
do
|
|
17
|
+
local x = 1
|
|
18
|
+
print(x)
|
|
19
|
+
end
|
|
20
|
+
do
|
|
21
|
+
local y = 2
|
|
22
|
+
print(y)
|
|
23
|
+
end
|
|
24
|
+
`);
|
|
25
|
+
const resolved = resolveScopes(chunk);
|
|
26
|
+
const result = assignRenames(resolved, new Set());
|
|
27
|
+
|
|
28
|
+
const firstDecl = (
|
|
29
|
+
(chunk.body[0] as Parser.DoStatement).body[0] as Parser.LocalStatement
|
|
30
|
+
).variables[0];
|
|
31
|
+
const secondDecl = (
|
|
32
|
+
(chunk.body[1] as Parser.DoStatement).body[0] as Parser.LocalStatement
|
|
33
|
+
).variables[0];
|
|
34
|
+
|
|
35
|
+
const firstName = result.nameOf(firstDecl);
|
|
36
|
+
const secondName = result.nameOf(secondDecl);
|
|
37
|
+
assert.ok(firstName);
|
|
38
|
+
assert.equal(firstName, secondName);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
void test("symbols live in the same scope never receive the same short name", () => {
|
|
42
|
+
const chunk = parse(`
|
|
43
|
+
local a = 1
|
|
44
|
+
local b = 2
|
|
45
|
+
local c = 3
|
|
46
|
+
print(a, b, c)
|
|
47
|
+
`);
|
|
48
|
+
const resolved = resolveScopes(chunk);
|
|
49
|
+
const result = assignRenames(resolved, new Set());
|
|
50
|
+
|
|
51
|
+
const names = (chunk.body.slice(0, 3) as Parser.LocalStatement[]).map((s) =>
|
|
52
|
+
result.nameOf(s.variables[0]),
|
|
53
|
+
);
|
|
54
|
+
assert.equal(new Set(names).size, 3);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
void test("more frequently referenced symbols get shorter names", () => {
|
|
58
|
+
const chunk = parse(`
|
|
59
|
+
local hot = 1
|
|
60
|
+
local cold = 2
|
|
61
|
+
print(hot, hot, hot, hot)
|
|
62
|
+
print(cold)
|
|
63
|
+
`);
|
|
64
|
+
const resolved = resolveScopes(chunk);
|
|
65
|
+
const result = assignRenames(resolved, new Set());
|
|
66
|
+
|
|
67
|
+
const hotDecl = (chunk.body[0] as Parser.LocalStatement).variables[0];
|
|
68
|
+
const coldDecl = (chunk.body[1] as Parser.LocalStatement).variables[0];
|
|
69
|
+
|
|
70
|
+
const hotName = result.nameOf(hotDecl);
|
|
71
|
+
const coldName = result.nameOf(coldDecl);
|
|
72
|
+
assert.ok(hotName);
|
|
73
|
+
assert.ok(coldName);
|
|
74
|
+
assert.ok(hotName.length <= coldName.length);
|
|
75
|
+
assert.notEqual(hotName, coldName);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
void test("reserved names (globals/keywords) are never assigned to a symbol", () => {
|
|
79
|
+
const chunk = parse(`
|
|
80
|
+
local x = 1
|
|
81
|
+
print(x)
|
|
82
|
+
`);
|
|
83
|
+
const resolved = resolveScopes(chunk);
|
|
84
|
+
// "a" は本来最初に割り当てられるはずの名前。予約済みとして渡すと避けられる。
|
|
85
|
+
const result = assignRenames(resolved, new Set(["a"]));
|
|
86
|
+
|
|
87
|
+
const decl = (chunk.body[0] as Parser.LocalStatement).variables[0];
|
|
88
|
+
assert.notEqual(result.nameOf(decl), "a");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
void test("self is never renamed and never assigned to another symbol", () => {
|
|
92
|
+
const chunk = parse(`
|
|
93
|
+
local function m(self, x)
|
|
94
|
+
return self, x
|
|
95
|
+
end
|
|
96
|
+
`);
|
|
97
|
+
const resolved = resolveScopes(chunk);
|
|
98
|
+
const result = assignRenames(resolved, new Set());
|
|
99
|
+
|
|
100
|
+
const fnDecl = chunk.body[0] as Parser.FunctionDeclaration;
|
|
101
|
+
const selfParam = fnDecl.parameters[0] as Parser.Identifier;
|
|
102
|
+
const xParam = fnDecl.parameters[1] as Parser.Identifier;
|
|
103
|
+
|
|
104
|
+
assert.equal(result.nameOf(selfParam), undefined);
|
|
105
|
+
assert.notEqual(result.nameOf(xParam), "self");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
void test("usedNames reflects exactly the short names handed out", () => {
|
|
109
|
+
const chunk = parse(`
|
|
110
|
+
local x = 1
|
|
111
|
+
do
|
|
112
|
+
local y = 2
|
|
113
|
+
print(y)
|
|
114
|
+
end
|
|
115
|
+
print(x)
|
|
116
|
+
`);
|
|
117
|
+
const resolved = resolveScopes(chunk);
|
|
118
|
+
const result = assignRenames(resolved, new Set());
|
|
119
|
+
|
|
120
|
+
const xDecl = (chunk.body[0] as Parser.LocalStatement).variables[0];
|
|
121
|
+
const yDecl = (
|
|
122
|
+
(chunk.body[1] as Parser.DoStatement).body[0] as Parser.LocalStatement
|
|
123
|
+
).variables[0];
|
|
124
|
+
|
|
125
|
+
const xName = result.nameOf(xDecl);
|
|
126
|
+
const yName = result.nameOf(yDecl);
|
|
127
|
+
assert.ok(xName);
|
|
128
|
+
assert.ok(yName);
|
|
129
|
+
assert.deepEqual(result.usedNames, new Set([xName, yName]));
|
|
130
|
+
});
|