@gwigz/slua-tstl-plugin 1.1.0 → 1.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/README.md +46 -0
- package/dist/define.d.ts +19 -0
- package/dist/define.js +83 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +129 -143
- package/dist/lua-ast-walk.d.ts +10 -0
- package/dist/lua-ast-walk.js +422 -0
- package/dist/lua-transforms.d.ts +41 -0
- package/dist/lua-transforms.js +302 -0
- package/dist/optimize.d.ts +2 -0
- package/dist/optimize.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,22 @@ This only applies when the argument is directly a `/` expression. `Math.floor(x)
|
|
|
180
180
|
> [!WARNING]
|
|
181
181
|
> JavaScript integer truncation idioms `~~x` and `x | 0` do **not** map cleanly to Luau. `~~x` emits `bit32.bnot(bit32.bnot(x))` and `x | 0` emits `bit32.bor(x, 0)`, neither of which preserves correct semantics for negative numbers (the `bit32` library operates on unsigned 32-bit integers). Use `math.floor(x)` for floor truncation instead.
|
|
182
182
|
|
|
183
|
+
### Passthrough arrow closures
|
|
184
|
+
|
|
185
|
+
Zero-parameter arrow functions that just call another zero-parameter function are collapsed to a direct function reference:
|
|
186
|
+
|
|
187
|
+
| TypeScript | Lua output |
|
|
188
|
+
| ------------------------------------------- | ----------------------------------- |
|
|
189
|
+
| `LLTimers.once(1, () => patchNext())` | `LLTimers:once(1, patchNext)` |
|
|
190
|
+
| `LLEvents.on("on_rez", () => refreshUrl())` | `LLEvents:on("on_rez", refreshUrl)` |
|
|
191
|
+
|
|
192
|
+
This applies when:
|
|
193
|
+
|
|
194
|
+
- The arrow has zero parameters
|
|
195
|
+
- The body is a single call with zero arguments
|
|
196
|
+
- The callee is a simple identifier (not a method call)
|
|
197
|
+
- The callee's type signature has zero parameters (so extra args from the caller are harmlessly ignored)
|
|
198
|
+
|
|
183
199
|
## Optimizations
|
|
184
200
|
|
|
185
201
|
Pass `optimize: true` to enable all optimizations, or pass an object to pick individual flags. All flags default to `false` when not specified.
|
|
@@ -306,6 +322,36 @@ local msg = "items: " .. count
|
|
|
306
322
|
|
|
307
323
|
Non-numeric types (booleans, `any`, etc.) still get wrapped in `tostring()`.
|
|
308
324
|
|
|
325
|
+
### `defaultParams`
|
|
326
|
+
|
|
327
|
+
Collapses default-parameter nil-checks into a single `or` expression.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
function respondPoll(extraHtml = "") {
|
|
331
|
+
// ...
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Default output:
|
|
336
|
+
|
|
337
|
+
```lua
|
|
338
|
+
function respondPoll(extraHtml)
|
|
339
|
+
if extraHtml == nil then
|
|
340
|
+
extraHtml = ""
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Optimized output:
|
|
346
|
+
|
|
347
|
+
```lua
|
|
348
|
+
function respondPoll(extraHtml)
|
|
349
|
+
extraHtml = extraHtml or ""
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Safe for string and number defaults (both truthy in Lua). Not applied to `false` defaults.
|
|
354
|
+
|
|
309
355
|
## Keeping output small
|
|
310
356
|
|
|
311
357
|
Some TypeScript patterns pull in large TSTL runtime helpers. Here are recommendations for keeping output lean:
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
export type DefineMap = Map<string, boolean | number | string>;
|
|
3
|
+
/**
|
|
4
|
+
* Attempt to statically evaluate a TS expression against the define map.
|
|
5
|
+
* Returns the resolved value, or `undefined` if the expression cannot be
|
|
6
|
+
* statically resolved (i.e. it should be left to the normal transpiler).
|
|
7
|
+
*
|
|
8
|
+
* Supported forms:
|
|
9
|
+
* - Bare identifier: `CONFIG_X` -> lookup
|
|
10
|
+
* - Negation: `!CONFIG_X`
|
|
11
|
+
* - Strict equality: `CONFIG_X === true`, `CONFIG_X !== false`, etc.
|
|
12
|
+
*/
|
|
13
|
+
export declare function tryEvaluateCondition(expr: ts.Expression, defineMap: DefineMap): boolean | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Check whether a node has a `@define FLAG` JSDoc tag whose flag
|
|
16
|
+
* is in the define map and resolves to falsy. When true, the
|
|
17
|
+
* entire declaration should be stripped from the output.
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldStripDefineGuard(node: ts.Node, defineMap: DefineMap): boolean;
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Attempt to statically evaluate a TS expression against the define map.
|
|
4
|
+
* Returns the resolved value, or `undefined` if the expression cannot be
|
|
5
|
+
* statically resolved (i.e. it should be left to the normal transpiler).
|
|
6
|
+
*
|
|
7
|
+
* Supported forms:
|
|
8
|
+
* - Bare identifier: `CONFIG_X` -> lookup
|
|
9
|
+
* - Negation: `!CONFIG_X`
|
|
10
|
+
* - Strict equality: `CONFIG_X === true`, `CONFIG_X !== false`, etc.
|
|
11
|
+
*/
|
|
12
|
+
export function tryEvaluateCondition(expr, defineMap) {
|
|
13
|
+
// Bare identifier: CONFIG_X
|
|
14
|
+
if (ts.isIdentifier(expr)) {
|
|
15
|
+
const value = defineMap.get(expr.text);
|
|
16
|
+
return value === undefined ? undefined : !!value;
|
|
17
|
+
}
|
|
18
|
+
// Negation: !CONFIG_X
|
|
19
|
+
if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.ExclamationToken) {
|
|
20
|
+
const inner = tryEvaluateCondition(expr.operand, defineMap);
|
|
21
|
+
return inner === undefined ? undefined : !inner;
|
|
22
|
+
}
|
|
23
|
+
// Strict equality/inequality: CONFIG_X === true, CONFIG_X !== false, etc.
|
|
24
|
+
if (ts.isBinaryExpression(expr)) {
|
|
25
|
+
const op = expr.operatorToken.kind;
|
|
26
|
+
if (op !== ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
|
27
|
+
op !== ts.SyntaxKind.ExclamationEqualsEqualsToken) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
let identValue;
|
|
31
|
+
let literalValue;
|
|
32
|
+
// Try both orientations: CONFIG_X === true or true === CONFIG_X
|
|
33
|
+
if (ts.isIdentifier(expr.left)) {
|
|
34
|
+
identValue = defineMap.get(expr.left.text);
|
|
35
|
+
literalValue = extractLiteral(expr.right);
|
|
36
|
+
}
|
|
37
|
+
else if (ts.isIdentifier(expr.right)) {
|
|
38
|
+
identValue = defineMap.get(expr.right.text);
|
|
39
|
+
literalValue = extractLiteral(expr.left);
|
|
40
|
+
}
|
|
41
|
+
if (identValue === undefined || literalValue === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const equal = identValue === literalValue;
|
|
44
|
+
return op === ts.SyntaxKind.EqualsEqualsEqualsToken ? equal : !equal;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check whether a node has a `@define FLAG` JSDoc tag whose flag
|
|
50
|
+
* is in the define map and resolves to falsy. When true, the
|
|
51
|
+
* entire declaration should be stripped from the output.
|
|
52
|
+
*/
|
|
53
|
+
export function shouldStripDefineGuard(node, defineMap) {
|
|
54
|
+
const jsDocs = node.jsDoc;
|
|
55
|
+
if (!jsDocs || jsDocs.length === 0)
|
|
56
|
+
return false;
|
|
57
|
+
for (const tag of ts.getJSDocTags(node)) {
|
|
58
|
+
if (tag.tagName.text === "define" && typeof tag.comment === "string") {
|
|
59
|
+
const flag = tag.comment.trim();
|
|
60
|
+
if (defineMap.has(flag) && !defineMap.get(flag)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
function extractLiteral(expr) {
|
|
68
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword)
|
|
69
|
+
return true;
|
|
70
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword)
|
|
71
|
+
return false;
|
|
72
|
+
if (ts.isNumericLiteral(expr))
|
|
73
|
+
return Number(expr.text);
|
|
74
|
+
if (ts.isStringLiteral(expr))
|
|
75
|
+
return expr.text;
|
|
76
|
+
// Handle negative numbers: -123
|
|
77
|
+
if (ts.isPrefixUnaryExpression(expr) &&
|
|
78
|
+
expr.operator === ts.SyntaxKind.MinusToken &&
|
|
79
|
+
ts.isNumericLiteral(expr.operand)) {
|
|
80
|
+
return -Number(expr.operand.text);
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as tstl from "typescript-to-lua";
|
|
2
2
|
import type { CallTransform } from "./transforms.js";
|
|
3
3
|
import type { OptimizeFlags } from "./optimize.js";
|
|
4
|
-
|
|
4
|
+
import type { DefineMap } from "./define.js";
|
|
5
|
+
export type { CallTransform, OptimizeFlags, DefineMap };
|
|
5
6
|
export interface SluaPluginOptions {
|
|
6
7
|
/** Enable per-transform output optimizations. Pass `true` to enable all. */
|
|
7
8
|
optimize?: boolean | OptimizeFlags;
|
|
9
|
+
/** Compile-time defines for dead code elimination. */
|
|
10
|
+
define?: Record<string, boolean | number | string>;
|
|
8
11
|
[key: string]: any;
|
|
9
12
|
}
|
|
10
13
|
declare function createPlugin(options?: SluaPluginOptions): tstl.Plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,91 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import * as tstl from "typescript-to-lua";
|
|
3
3
|
import { PASCAL_TO_LOWER, TSTL_KEYWORD_FIXUPS, BINARY_BITWISE_OPS, COMPOUND_BITWISE_OPS, } from "./constants.js";
|
|
4
|
-
import { createBit32Call, isMathFloor, isStringOrNumberLike, extractBtestPattern, extractIndexOfPresence, extractConcatSelfAssignment, extractSpreadSelfAssignment, emitChainedExtend, getLLIndexSemantics, emitLLIndexCall, isDetectedEventIndex, createNamespacedCall, createStringFindCall,
|
|
4
|
+
import { createBit32Call, isMathFloor, isStringOrNumberLike, extractBtestPattern, extractIndexOfPresence, extractConcatSelfAssignment, extractSpreadSelfAssignment, emitChainedExtend, getLLIndexSemantics, emitLLIndexCall, isDetectedEventIndex, createNamespacedCall, createStringFindCall, } from "./utils.js";
|
|
5
5
|
import { CALL_TRANSFORMS } from "./transforms.js";
|
|
6
6
|
import { createOptimizeTransforms, countFilterCalls, ALL_OPTIMIZE } from "./optimize.js";
|
|
7
|
+
import { tryEvaluateCondition, shouldStripDefineGuard } from "./define.js";
|
|
8
|
+
import { stripInternalJSDocTags, stripEmptyModuleBoilerplate, collapseDefaultParamNilChecks, shortenTempNames, collapseFieldAccesses, inlineForwardDeclarations, } from "./lua-transforms.js";
|
|
7
9
|
function createPlugin(options = {}) {
|
|
8
10
|
const opt = options.optimize === true ? ALL_OPTIMIZE : options.optimize || {};
|
|
9
11
|
const filterSkipFiles = new Set();
|
|
10
12
|
const optTransforms = opt.filter ? createOptimizeTransforms(filterSkipFiles) : [];
|
|
11
13
|
const transforms = [...CALL_TRANSFORMS, ...optTransforms];
|
|
14
|
+
const defineMap = new Map(options.define ? Object.entries(options.define) : []);
|
|
12
15
|
const plugin = {
|
|
13
16
|
visitors: {
|
|
17
|
+
[ts.SyntaxKind.Identifier]: defineMap.size > 0
|
|
18
|
+
? (node, context) => {
|
|
19
|
+
const value = defineMap.get(node.text);
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return context.superTransformExpression(node);
|
|
22
|
+
// Don't replace if this identifier is:
|
|
23
|
+
// - A property name in a property access (obj.CONFIG_X)
|
|
24
|
+
// - A declaration name (const CONFIG_X = ...)
|
|
25
|
+
// - A parameter name
|
|
26
|
+
// - A property assignment name ({ CONFIG_X: ... })
|
|
27
|
+
// - A shorthand property assignment name ({ CONFIG_X })
|
|
28
|
+
const parent = node.parent;
|
|
29
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
30
|
+
return context.superTransformExpression(node);
|
|
31
|
+
}
|
|
32
|
+
if ((ts.isVariableDeclaration(parent) ||
|
|
33
|
+
ts.isFunctionDeclaration(parent) ||
|
|
34
|
+
ts.isParameter(parent) ||
|
|
35
|
+
ts.isEnumMember(parent)) &&
|
|
36
|
+
parent.name === node) {
|
|
37
|
+
return context.superTransformExpression(node);
|
|
38
|
+
}
|
|
39
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
40
|
+
return context.superTransformExpression(node);
|
|
41
|
+
}
|
|
42
|
+
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
43
|
+
return context.superTransformExpression(node);
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "boolean") {
|
|
46
|
+
return tstl.createBooleanLiteral(value, node);
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "number") {
|
|
49
|
+
return tstl.createNumericLiteral(value, node);
|
|
50
|
+
}
|
|
51
|
+
return tstl.createStringLiteral(value, node);
|
|
52
|
+
}
|
|
53
|
+
: undefined,
|
|
54
|
+
[ts.SyntaxKind.IfStatement]: defineMap.size > 0
|
|
55
|
+
? (node, context) => {
|
|
56
|
+
const result = tryEvaluateCondition(node.expression, defineMap);
|
|
57
|
+
if (result === undefined) {
|
|
58
|
+
return context.superTransformStatements(node);
|
|
59
|
+
}
|
|
60
|
+
if (result) {
|
|
61
|
+
const stmts = ts.isBlock(node.thenStatement)
|
|
62
|
+
? [...node.thenStatement.statements]
|
|
63
|
+
: [node.thenStatement];
|
|
64
|
+
return stmts.flatMap((s) => context.transformStatements(s));
|
|
65
|
+
}
|
|
66
|
+
if (node.elseStatement) {
|
|
67
|
+
const stmts = ts.isBlock(node.elseStatement)
|
|
68
|
+
? [...node.elseStatement.statements]
|
|
69
|
+
: [node.elseStatement];
|
|
70
|
+
return stmts.flatMap((s) => context.transformStatements(s));
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
: undefined,
|
|
75
|
+
[ts.SyntaxKind.FunctionDeclaration]: defineMap.size > 0
|
|
76
|
+
? (node, context) => {
|
|
77
|
+
if (shouldStripDefineGuard(node, defineMap))
|
|
78
|
+
return [];
|
|
79
|
+
return context.superTransformStatements(node);
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
82
|
+
[ts.SyntaxKind.VariableStatement]: defineMap.size > 0
|
|
83
|
+
? (node, context) => {
|
|
84
|
+
if (shouldStripDefineGuard(node, defineMap))
|
|
85
|
+
return [];
|
|
86
|
+
return context.superTransformStatements(node);
|
|
87
|
+
}
|
|
88
|
+
: undefined,
|
|
14
89
|
[ts.SyntaxKind.PropertyAccessExpression]: (node, context) => {
|
|
15
90
|
const result = context.superTransformExpression(node);
|
|
16
91
|
// Rewrite identifiers in the Lua AST (PascalCase -> lowercase, TSTL keyword fixups).
|
|
@@ -187,6 +262,35 @@ function createPlugin(options = {}) {
|
|
|
187
262
|
}
|
|
188
263
|
return parts.reduce((prev, current) => tstl.createBinaryExpression(prev, current, tstl.SyntaxKind.ConcatOperator));
|
|
189
264
|
},
|
|
265
|
+
// Collapse `() => fn()` to just `fn` when fn has zero parameters.
|
|
266
|
+
// Extra args from the caller are harmlessly ignored by zero-param functions.
|
|
267
|
+
[ts.SyntaxKind.ArrowFunction]: (node, context) => {
|
|
268
|
+
if (node.parameters.length === 0) {
|
|
269
|
+
let callExpr;
|
|
270
|
+
if (ts.isCallExpression(node.body)) {
|
|
271
|
+
callExpr = node.body;
|
|
272
|
+
}
|
|
273
|
+
else if (ts.isBlock(node.body) && node.body.statements.length === 1) {
|
|
274
|
+
const stmt = node.body.statements[0];
|
|
275
|
+
if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
|
|
276
|
+
callExpr = stmt.expression;
|
|
277
|
+
}
|
|
278
|
+
else if (ts.isReturnStatement(stmt) &&
|
|
279
|
+
stmt.expression &&
|
|
280
|
+
ts.isCallExpression(stmt.expression)) {
|
|
281
|
+
callExpr = stmt.expression;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (callExpr && callExpr.arguments.length === 0 && ts.isIdentifier(callExpr.expression)) {
|
|
285
|
+
const type = context.checker.getTypeAtLocation(callExpr.expression);
|
|
286
|
+
const sigs = type.getCallSignatures();
|
|
287
|
+
if (sigs.length > 0 && sigs.every((s) => s.parameters.length === 0)) {
|
|
288
|
+
return context.transformExpression(callExpr.expression);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return context.superTransformExpression(node);
|
|
293
|
+
},
|
|
190
294
|
},
|
|
191
295
|
beforeTransform(program, compilerOptions) {
|
|
192
296
|
const diagnostics = [];
|
|
@@ -214,152 +318,34 @@ function createPlugin(options = {}) {
|
|
|
214
318
|
}
|
|
215
319
|
return diagnostics;
|
|
216
320
|
},
|
|
217
|
-
|
|
218
|
-
// Strip internal @indexArg / @indexReturn JSDoc tags from Lua comments.
|
|
219
|
-
// These are only consumed by the plugin at transpile time; they should
|
|
220
|
-
// not leak into the output as Lua comments.
|
|
221
|
-
for (const file of result) {
|
|
222
|
-
file.code = file.code
|
|
223
|
-
.replace(/^--\s*@index(?:Arg|Return)\b.*\n/gm, "")
|
|
224
|
-
.replace(/^(---.*)\n(?:-- *\n)+(?=local |ll\.)/gm, "$1\n");
|
|
225
|
-
}
|
|
226
|
-
// Strip empty module boilerplate from files without explicit exports.
|
|
227
|
-
// `moduleDetection: "force"` causes TSTL to wrap every file as a module;
|
|
228
|
-
// standalone SLua scripts don't need the ____exports wrapper.
|
|
321
|
+
afterPrint(program, _options, emitHost, result) {
|
|
229
322
|
for (const file of result) {
|
|
230
|
-
if (!file.
|
|
323
|
+
if (!file.luaAst)
|
|
231
324
|
continue;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// Shorten TSTL destructuring temp names: ____fn_result_N -> _rN
|
|
245
|
-
// Then collapse consecutive field accesses into multi-assignment:
|
|
246
|
-
// local a = _r0.x\nlocal b = _r0.y -> local a, b = _r0.x, _r0.y
|
|
247
|
-
if (opt.shortenTemps) {
|
|
248
|
-
for (const file of result) {
|
|
249
|
-
const seen = new Map();
|
|
250
|
-
let counter = 0;
|
|
251
|
-
// Collect all unique temp names in order of first occurrence
|
|
252
|
-
for (const match of file.code.matchAll(/____\w+_result_\d+/g)) {
|
|
253
|
-
if (!seen.has(match[0])) {
|
|
254
|
-
seen.set(match[0], `_r${counter++}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Replace all long names with short aliases in a single pass
|
|
258
|
-
if (seen.size > 0) {
|
|
259
|
-
const combined = new RegExp(`\\b(${[...seen.keys()].join("|")})\\b`, "g");
|
|
260
|
-
file.code = file.code.replace(combined, (m) => seen.get(m) ?? m);
|
|
261
|
-
}
|
|
262
|
-
// Collapse consecutive field accesses from the same temp into multi-assignment
|
|
263
|
-
const lines = file.code.split("\n");
|
|
264
|
-
const collapsed = [];
|
|
265
|
-
let li = 0;
|
|
266
|
-
while (li < lines.length) {
|
|
267
|
-
const m = lines[li].match(/^(\s*)local\s+(\w+)\s*=\s*(_r\d+)\.(\w+)\s*$/);
|
|
268
|
-
if (m) {
|
|
269
|
-
const [, indent, name, temp, field] = m;
|
|
270
|
-
const names = [name];
|
|
271
|
-
const accesses = [`${temp}.${field}`];
|
|
272
|
-
const nextRe = new RegExp(`^${escapeRegex(indent)}local\\s+(\\w+)\\s*=\\s*${temp}\\.(\\w+)\\s*$`);
|
|
273
|
-
while (li + 1 < lines.length) {
|
|
274
|
-
const next = lines[li + 1].match(nextRe);
|
|
275
|
-
if (!next)
|
|
276
|
-
break;
|
|
277
|
-
names.push(next[1]);
|
|
278
|
-
accesses.push(`${temp}.${next[2]}`);
|
|
279
|
-
li++;
|
|
280
|
-
}
|
|
281
|
-
if (names.length > 1) {
|
|
282
|
-
collapsed.push(`${indent}local ${names.join(", ")} = ${accesses.join(", ")}`);
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
collapsed.push(lines[li]);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
collapsed.push(lines[li]);
|
|
290
|
-
}
|
|
291
|
-
li++;
|
|
292
|
-
}
|
|
293
|
-
file.code = collapsed.join("\n");
|
|
325
|
+
let dirty = false;
|
|
326
|
+
// Always-on transforms
|
|
327
|
+
dirty = stripInternalJSDocTags(file.luaAst) || dirty;
|
|
328
|
+
dirty = stripEmptyModuleBoilerplate(file.luaAst, file.sourceFiles) || dirty;
|
|
329
|
+
// Opt-in transforms
|
|
330
|
+
if (opt.defaultParams)
|
|
331
|
+
dirty = collapseDefaultParamNilChecks(file.luaAst) || dirty;
|
|
332
|
+
if (opt.shortenTemps) {
|
|
333
|
+
dirty = shortenTempNames(file.luaAst) || dirty;
|
|
334
|
+
dirty = collapseFieldAccesses(file.luaAst) || dirty;
|
|
294
335
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
continue;
|
|
305
|
-
// Match forward declaration: `local var1, var2, ...` with no `=`
|
|
306
|
-
const declMatch = lines[i].match(/^(\s*)local\s+((?:\w+(?:,\s*)*)*\w+)\s*$/);
|
|
307
|
-
if (!declMatch)
|
|
308
|
-
continue;
|
|
309
|
-
const indent = declMatch[1];
|
|
310
|
-
const escapedIndent = escapeRegex(indent);
|
|
311
|
-
const vars = declMatch[2]
|
|
312
|
-
.split(/,\s*/)
|
|
313
|
-
.map((v) => v.trim())
|
|
314
|
-
.filter(Boolean);
|
|
315
|
-
const inlined = new Set();
|
|
316
|
-
for (const varName of vars) {
|
|
317
|
-
// Hoist regex compilation outside inner loop; negative lookahead
|
|
318
|
-
// rejects multi-variable assignment lines like `a, b = fn()`
|
|
319
|
-
const assignRe = new RegExp(`^${escapedIndent}${varName}(?!\\s*,)\\s*=\\s*(.+)$`);
|
|
320
|
-
const refRe = new RegExp(`\\b${varName}\\b`);
|
|
321
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
322
|
-
if (removedLines.has(j))
|
|
323
|
-
continue;
|
|
324
|
-
// Check for bare single-variable assignment at same indentation
|
|
325
|
-
const assignMatch = lines[j].match(assignRe);
|
|
326
|
-
if (assignMatch) {
|
|
327
|
-
// Don't inline if RHS references the variable (self-referencing)
|
|
328
|
-
if (refRe.test(assignMatch[1]))
|
|
329
|
-
break;
|
|
330
|
-
lines[j] = `${indent}local ${varName} = ${assignMatch[1]}`;
|
|
331
|
-
inlined.add(varName);
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
// Reference to varName prevents inlining
|
|
335
|
-
if (refRe.test(lines[j]))
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (inlined.size === 0)
|
|
340
|
-
continue;
|
|
341
|
-
const remaining = vars.filter((v) => !inlined.has(v));
|
|
342
|
-
if (remaining.length === 0) {
|
|
343
|
-
removedLines.add(i);
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
lines[i] = `${indent}local ${remaining.join(", ")}`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
file.code = lines.filter((_, idx) => !removedLines.has(idx)).join("\n");
|
|
336
|
+
if (opt.inlineLocals)
|
|
337
|
+
dirty = inlineForwardDeclarations(file.luaAst) || dirty;
|
|
338
|
+
// Re-print if AST was modified
|
|
339
|
+
if (dirty) {
|
|
340
|
+
const printer = new tstl.LuaPrinter(emitHost, program, file.fileName);
|
|
341
|
+
const printed = printer.print(file.luaAst);
|
|
342
|
+
file.code = printed.code;
|
|
343
|
+
file.sourceMap = printed.sourceMap;
|
|
344
|
+
file.sourceMapNode = printed.sourceMapNode;
|
|
350
345
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// that may also post-process the Lua source in beforeEmit.
|
|
355
|
-
// `+=` is Luau-only syntax and would be garbled by Lua 5.1 parsers.
|
|
356
|
-
afterEmit(_program, _options, emitHost, result) {
|
|
357
|
-
if (!opt.compoundAssignment)
|
|
358
|
-
return;
|
|
359
|
-
for (const file of result) {
|
|
360
|
-
file.code = file.code.replace(/^(\s*)(\w+) = \2 (\/\/|\.\.|[+\-*/%^]) (\S+)(\s*(?:--.*)?)$/gm, "$1$2 $3= $4$5");
|
|
361
|
-
if (emitHost.writeFile) {
|
|
362
|
-
emitHost.writeFile(file.outputPath, file.code, false);
|
|
346
|
+
// Compound assignment stays as regex (no Lua AST node for +=)
|
|
347
|
+
if (opt.compoundAssignment) {
|
|
348
|
+
file.code = file.code.replace(/^(\s*)(\w+) = \2 (\/\/|\.\.|[+\-*/%^]) (\S+)(\s*(?:--.*)?)$/gm, "$1$2 $3= $4$5");
|
|
363
349
|
}
|
|
364
350
|
}
|
|
365
351
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as lua from "typescript-to-lua";
|
|
2
|
+
export type BlockVisitor = (statements: lua.Statement[]) => void;
|
|
3
|
+
/** Walk all statement arrays in a File. Depth-first. */
|
|
4
|
+
export declare function walkBlocks(file: lua.File, visitor: BlockVisitor): void;
|
|
5
|
+
type IdentifierCallback = (id: lua.Identifier) => void;
|
|
6
|
+
/** Walk all Identifier nodes in a File. Callback may mutate id.text. */
|
|
7
|
+
export declare function walkIdentifiers(file: lua.File, cb: IdentifierCallback): void;
|
|
8
|
+
/** Check if an expression tree contains an Identifier with the given name. */
|
|
9
|
+
export declare function containsIdentifier(node: lua.Node, name: string): boolean;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import * as lua from "typescript-to-lua";
|
|
2
|
+
function walkExpression(expr, blockVisitor) {
|
|
3
|
+
switch (expr.kind) {
|
|
4
|
+
case lua.SyntaxKind.StringLiteral:
|
|
5
|
+
case lua.SyntaxKind.NumericLiteral:
|
|
6
|
+
case lua.SyntaxKind.NilKeyword:
|
|
7
|
+
case lua.SyntaxKind.DotsKeyword:
|
|
8
|
+
case lua.SyntaxKind.ArgKeyword:
|
|
9
|
+
case lua.SyntaxKind.TrueKeyword:
|
|
10
|
+
case lua.SyntaxKind.FalseKeyword:
|
|
11
|
+
case lua.SyntaxKind.Identifier:
|
|
12
|
+
break;
|
|
13
|
+
case lua.SyntaxKind.BinaryExpression: {
|
|
14
|
+
const bin = expr;
|
|
15
|
+
walkExpression(bin.left, blockVisitor);
|
|
16
|
+
walkExpression(bin.right, blockVisitor);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case lua.SyntaxKind.UnaryExpression:
|
|
20
|
+
walkExpression(expr.operand, blockVisitor);
|
|
21
|
+
break;
|
|
22
|
+
case lua.SyntaxKind.ParenthesizedExpression:
|
|
23
|
+
walkExpression(expr.expression, blockVisitor);
|
|
24
|
+
break;
|
|
25
|
+
case lua.SyntaxKind.ConditionalExpression: {
|
|
26
|
+
const cond = expr;
|
|
27
|
+
walkExpression(cond.condition, blockVisitor);
|
|
28
|
+
walkExpression(cond.whenTrue, blockVisitor);
|
|
29
|
+
walkExpression(cond.whenFalse, blockVisitor);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case lua.SyntaxKind.CallExpression: {
|
|
33
|
+
const call = expr;
|
|
34
|
+
walkExpression(call.expression, blockVisitor);
|
|
35
|
+
for (const p of call.params)
|
|
36
|
+
walkExpression(p, blockVisitor);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case lua.SyntaxKind.MethodCallExpression: {
|
|
40
|
+
const mc = expr;
|
|
41
|
+
walkExpression(mc.prefixExpression, blockVisitor);
|
|
42
|
+
for (const p of mc.params)
|
|
43
|
+
walkExpression(p, blockVisitor);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case lua.SyntaxKind.TableIndexExpression: {
|
|
47
|
+
const ti = expr;
|
|
48
|
+
walkExpression(ti.table, blockVisitor);
|
|
49
|
+
walkExpression(ti.index, blockVisitor);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case lua.SyntaxKind.TableExpression:
|
|
53
|
+
for (const f of expr.fields) {
|
|
54
|
+
walkExpression(f.value, blockVisitor);
|
|
55
|
+
if (f.key)
|
|
56
|
+
walkExpression(f.key, blockVisitor);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case lua.SyntaxKind.FunctionExpression: {
|
|
60
|
+
const fn = expr;
|
|
61
|
+
walkBlock(fn.body, blockVisitor);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case lua.SyntaxKind.TableFieldExpression: {
|
|
65
|
+
const tf = expr;
|
|
66
|
+
walkExpression(tf.value, blockVisitor);
|
|
67
|
+
if (tf.key)
|
|
68
|
+
walkExpression(tf.key, blockVisitor);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function walkStatement(stmt, blockVisitor) {
|
|
74
|
+
switch (stmt.kind) {
|
|
75
|
+
case lua.SyntaxKind.DoStatement:
|
|
76
|
+
walkBlock(stmt, blockVisitor);
|
|
77
|
+
break;
|
|
78
|
+
case lua.SyntaxKind.VariableDeclarationStatement: {
|
|
79
|
+
const vds = stmt;
|
|
80
|
+
if (vds.right) {
|
|
81
|
+
for (const r of vds.right)
|
|
82
|
+
walkExpression(r, blockVisitor);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case lua.SyntaxKind.AssignmentStatement: {
|
|
87
|
+
const as_ = stmt;
|
|
88
|
+
for (const l of as_.left)
|
|
89
|
+
walkExpression(l, blockVisitor);
|
|
90
|
+
for (const r of as_.right)
|
|
91
|
+
walkExpression(r, blockVisitor);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case lua.SyntaxKind.IfStatement: {
|
|
95
|
+
const is_ = stmt;
|
|
96
|
+
walkExpression(is_.condition, blockVisitor);
|
|
97
|
+
walkBlock(is_.ifBlock, blockVisitor);
|
|
98
|
+
if (is_.elseBlock) {
|
|
99
|
+
if (lua.isIfStatement(is_.elseBlock)) {
|
|
100
|
+
walkStatement(is_.elseBlock, blockVisitor);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
walkBlock(is_.elseBlock, blockVisitor);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case lua.SyntaxKind.WhileStatement: {
|
|
109
|
+
const ws = stmt;
|
|
110
|
+
walkExpression(ws.condition, blockVisitor);
|
|
111
|
+
walkBlock(ws.body, blockVisitor);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case lua.SyntaxKind.RepeatStatement: {
|
|
115
|
+
const rs = stmt;
|
|
116
|
+
walkExpression(rs.condition, blockVisitor);
|
|
117
|
+
walkBlock(rs.body, blockVisitor);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case lua.SyntaxKind.ForStatement: {
|
|
121
|
+
const fs = stmt;
|
|
122
|
+
walkExpression(fs.controlVariableInitializer, blockVisitor);
|
|
123
|
+
walkExpression(fs.limitExpression, blockVisitor);
|
|
124
|
+
if (fs.stepExpression)
|
|
125
|
+
walkExpression(fs.stepExpression, blockVisitor);
|
|
126
|
+
walkBlock(fs.body, blockVisitor);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case lua.SyntaxKind.ForInStatement: {
|
|
130
|
+
const fis = stmt;
|
|
131
|
+
for (const e of fis.expressions)
|
|
132
|
+
walkExpression(e, blockVisitor);
|
|
133
|
+
walkBlock(fis.body, blockVisitor);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case lua.SyntaxKind.ReturnStatement:
|
|
137
|
+
for (const e of stmt.expressions)
|
|
138
|
+
walkExpression(e, blockVisitor);
|
|
139
|
+
break;
|
|
140
|
+
case lua.SyntaxKind.ExpressionStatement:
|
|
141
|
+
walkExpression(stmt.expression, blockVisitor);
|
|
142
|
+
break;
|
|
143
|
+
case lua.SyntaxKind.GotoStatement:
|
|
144
|
+
case lua.SyntaxKind.LabelStatement:
|
|
145
|
+
case lua.SyntaxKind.BreakStatement:
|
|
146
|
+
case lua.SyntaxKind.ContinueStatement:
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function walkBlock(block, blockVisitor) {
|
|
151
|
+
blockVisitor(block.statements);
|
|
152
|
+
for (const stmt of block.statements) {
|
|
153
|
+
walkStatement(stmt, blockVisitor);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Walk all statement arrays in a File. Depth-first. */
|
|
157
|
+
export function walkBlocks(file, visitor) {
|
|
158
|
+
walkBlock(file, visitor);
|
|
159
|
+
}
|
|
160
|
+
function walkExpressionIds(expr, cb) {
|
|
161
|
+
switch (expr.kind) {
|
|
162
|
+
case lua.SyntaxKind.Identifier:
|
|
163
|
+
cb(expr);
|
|
164
|
+
break;
|
|
165
|
+
case lua.SyntaxKind.StringLiteral:
|
|
166
|
+
case lua.SyntaxKind.NumericLiteral:
|
|
167
|
+
case lua.SyntaxKind.NilKeyword:
|
|
168
|
+
case lua.SyntaxKind.DotsKeyword:
|
|
169
|
+
case lua.SyntaxKind.ArgKeyword:
|
|
170
|
+
case lua.SyntaxKind.TrueKeyword:
|
|
171
|
+
case lua.SyntaxKind.FalseKeyword:
|
|
172
|
+
break;
|
|
173
|
+
case lua.SyntaxKind.BinaryExpression: {
|
|
174
|
+
const bin = expr;
|
|
175
|
+
walkExpressionIds(bin.left, cb);
|
|
176
|
+
walkExpressionIds(bin.right, cb);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case lua.SyntaxKind.UnaryExpression:
|
|
180
|
+
walkExpressionIds(expr.operand, cb);
|
|
181
|
+
break;
|
|
182
|
+
case lua.SyntaxKind.ParenthesizedExpression:
|
|
183
|
+
walkExpressionIds(expr.expression, cb);
|
|
184
|
+
break;
|
|
185
|
+
case lua.SyntaxKind.ConditionalExpression: {
|
|
186
|
+
const cond = expr;
|
|
187
|
+
walkExpressionIds(cond.condition, cb);
|
|
188
|
+
walkExpressionIds(cond.whenTrue, cb);
|
|
189
|
+
walkExpressionIds(cond.whenFalse, cb);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case lua.SyntaxKind.CallExpression: {
|
|
193
|
+
const call = expr;
|
|
194
|
+
walkExpressionIds(call.expression, cb);
|
|
195
|
+
for (const p of call.params)
|
|
196
|
+
walkExpressionIds(p, cb);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case lua.SyntaxKind.MethodCallExpression: {
|
|
200
|
+
const mc = expr;
|
|
201
|
+
walkExpressionIds(mc.prefixExpression, cb);
|
|
202
|
+
walkExpressionIds(mc.name, cb);
|
|
203
|
+
for (const p of mc.params)
|
|
204
|
+
walkExpressionIds(p, cb);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case lua.SyntaxKind.TableIndexExpression: {
|
|
208
|
+
const ti = expr;
|
|
209
|
+
walkExpressionIds(ti.table, cb);
|
|
210
|
+
walkExpressionIds(ti.index, cb);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case lua.SyntaxKind.TableExpression:
|
|
214
|
+
for (const f of expr.fields) {
|
|
215
|
+
walkExpressionIds(f.value, cb);
|
|
216
|
+
if (f.key)
|
|
217
|
+
walkExpressionIds(f.key, cb);
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
case lua.SyntaxKind.FunctionExpression: {
|
|
221
|
+
const fn = expr;
|
|
222
|
+
if (fn.params) {
|
|
223
|
+
for (const p of fn.params)
|
|
224
|
+
cb(p);
|
|
225
|
+
}
|
|
226
|
+
walkBlockIds(fn.body, cb);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case lua.SyntaxKind.TableFieldExpression: {
|
|
230
|
+
const tf = expr;
|
|
231
|
+
walkExpressionIds(tf.value, cb);
|
|
232
|
+
if (tf.key)
|
|
233
|
+
walkExpressionIds(tf.key, cb);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function walkStatementIds(stmt, cb) {
|
|
239
|
+
switch (stmt.kind) {
|
|
240
|
+
case lua.SyntaxKind.DoStatement:
|
|
241
|
+
walkBlockIds(stmt, cb);
|
|
242
|
+
break;
|
|
243
|
+
case lua.SyntaxKind.VariableDeclarationStatement: {
|
|
244
|
+
const vds = stmt;
|
|
245
|
+
for (const l of vds.left)
|
|
246
|
+
cb(l);
|
|
247
|
+
if (vds.right) {
|
|
248
|
+
for (const r of vds.right)
|
|
249
|
+
walkExpressionIds(r, cb);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case lua.SyntaxKind.AssignmentStatement: {
|
|
254
|
+
const as_ = stmt;
|
|
255
|
+
for (const l of as_.left)
|
|
256
|
+
walkExpressionIds(l, cb);
|
|
257
|
+
for (const r of as_.right)
|
|
258
|
+
walkExpressionIds(r, cb);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case lua.SyntaxKind.IfStatement: {
|
|
262
|
+
const is_ = stmt;
|
|
263
|
+
walkExpressionIds(is_.condition, cb);
|
|
264
|
+
walkBlockIds(is_.ifBlock, cb);
|
|
265
|
+
if (is_.elseBlock) {
|
|
266
|
+
if (lua.isIfStatement(is_.elseBlock)) {
|
|
267
|
+
walkStatementIds(is_.elseBlock, cb);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
walkBlockIds(is_.elseBlock, cb);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case lua.SyntaxKind.WhileStatement: {
|
|
276
|
+
const ws = stmt;
|
|
277
|
+
walkExpressionIds(ws.condition, cb);
|
|
278
|
+
walkBlockIds(ws.body, cb);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case lua.SyntaxKind.RepeatStatement: {
|
|
282
|
+
const rs = stmt;
|
|
283
|
+
walkExpressionIds(rs.condition, cb);
|
|
284
|
+
walkBlockIds(rs.body, cb);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case lua.SyntaxKind.ForStatement: {
|
|
288
|
+
const fs = stmt;
|
|
289
|
+
cb(fs.controlVariable);
|
|
290
|
+
walkExpressionIds(fs.controlVariableInitializer, cb);
|
|
291
|
+
walkExpressionIds(fs.limitExpression, cb);
|
|
292
|
+
if (fs.stepExpression)
|
|
293
|
+
walkExpressionIds(fs.stepExpression, cb);
|
|
294
|
+
walkBlockIds(fs.body, cb);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case lua.SyntaxKind.ForInStatement: {
|
|
298
|
+
const fis = stmt;
|
|
299
|
+
for (const n of fis.names)
|
|
300
|
+
cb(n);
|
|
301
|
+
for (const e of fis.expressions)
|
|
302
|
+
walkExpressionIds(e, cb);
|
|
303
|
+
walkBlockIds(fis.body, cb);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case lua.SyntaxKind.ReturnStatement:
|
|
307
|
+
for (const e of stmt.expressions)
|
|
308
|
+
walkExpressionIds(e, cb);
|
|
309
|
+
break;
|
|
310
|
+
case lua.SyntaxKind.ExpressionStatement:
|
|
311
|
+
walkExpressionIds(stmt.expression, cb);
|
|
312
|
+
break;
|
|
313
|
+
case lua.SyntaxKind.GotoStatement:
|
|
314
|
+
case lua.SyntaxKind.LabelStatement:
|
|
315
|
+
case lua.SyntaxKind.BreakStatement:
|
|
316
|
+
case lua.SyntaxKind.ContinueStatement:
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function walkBlockIds(block, cb) {
|
|
321
|
+
for (const stmt of block.statements) {
|
|
322
|
+
walkStatementIds(stmt, cb);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Walk all Identifier nodes in a File. Callback may mutate id.text. */
|
|
326
|
+
export function walkIdentifiers(file, cb) {
|
|
327
|
+
walkBlockIds(file, cb);
|
|
328
|
+
}
|
|
329
|
+
/** Check if an expression tree contains an Identifier with the given name. */
|
|
330
|
+
export function containsIdentifier(node, name) {
|
|
331
|
+
if (lua.isIdentifier(node)) {
|
|
332
|
+
return node.text === name;
|
|
333
|
+
}
|
|
334
|
+
// Walk expressions
|
|
335
|
+
if (lua.isBinaryExpression(node)) {
|
|
336
|
+
return containsIdentifier(node.left, name) || containsIdentifier(node.right, name);
|
|
337
|
+
}
|
|
338
|
+
if (lua.isUnaryExpression(node)) {
|
|
339
|
+
return containsIdentifier(node.operand, name);
|
|
340
|
+
}
|
|
341
|
+
if (lua.isParenthesizedExpression(node)) {
|
|
342
|
+
return containsIdentifier(node.expression, name);
|
|
343
|
+
}
|
|
344
|
+
if (lua.isConditionalExpression(node)) {
|
|
345
|
+
return (containsIdentifier(node.condition, name) ||
|
|
346
|
+
containsIdentifier(node.whenTrue, name) ||
|
|
347
|
+
containsIdentifier(node.whenFalse, name));
|
|
348
|
+
}
|
|
349
|
+
if (lua.isCallExpression(node)) {
|
|
350
|
+
return (containsIdentifier(node.expression, name) ||
|
|
351
|
+
node.params.some((p) => containsIdentifier(p, name)));
|
|
352
|
+
}
|
|
353
|
+
if (lua.isMethodCallExpression(node)) {
|
|
354
|
+
return (containsIdentifier(node.prefixExpression, name) ||
|
|
355
|
+
containsIdentifier(node.name, name) ||
|
|
356
|
+
node.params.some((p) => containsIdentifier(p, name)));
|
|
357
|
+
}
|
|
358
|
+
if (lua.isTableIndexExpression(node)) {
|
|
359
|
+
return containsIdentifier(node.table, name) || containsIdentifier(node.index, name);
|
|
360
|
+
}
|
|
361
|
+
if (lua.isTableExpression(node)) {
|
|
362
|
+
return node.fields.some((f) => containsIdentifier(f.value, name) || (f.key ? containsIdentifier(f.key, name) : false));
|
|
363
|
+
}
|
|
364
|
+
if (lua.isTableFieldExpression(node)) {
|
|
365
|
+
return (containsIdentifier(node.value, name) ||
|
|
366
|
+
(node.key ? containsIdentifier(node.key, name) : false));
|
|
367
|
+
}
|
|
368
|
+
if (lua.isFunctionExpression(node)) {
|
|
369
|
+
return node.body.statements.some((s) => containsIdentifierInStatement(s, name));
|
|
370
|
+
}
|
|
371
|
+
// Walk statements
|
|
372
|
+
if (lua.isDoStatement(node)) {
|
|
373
|
+
return node.statements.some((s) => containsIdentifierInStatement(s, name));
|
|
374
|
+
}
|
|
375
|
+
if (lua.isVariableDeclarationStatement(node)) {
|
|
376
|
+
return (node.left.some((l) => l.text === name) ||
|
|
377
|
+
(node.right?.some((r) => containsIdentifier(r, name)) ?? false));
|
|
378
|
+
}
|
|
379
|
+
if (lua.isAssignmentStatement(node)) {
|
|
380
|
+
return (node.left.some((l) => containsIdentifier(l, name)) ||
|
|
381
|
+
node.right.some((r) => containsIdentifier(r, name)));
|
|
382
|
+
}
|
|
383
|
+
if (lua.isIfStatement(node)) {
|
|
384
|
+
if (containsIdentifier(node.condition, name))
|
|
385
|
+
return true;
|
|
386
|
+
if (node.ifBlock.statements.some((s) => containsIdentifierInStatement(s, name)))
|
|
387
|
+
return true;
|
|
388
|
+
if (node.elseBlock) {
|
|
389
|
+
if (lua.isIfStatement(node.elseBlock))
|
|
390
|
+
return containsIdentifier(node.elseBlock, name);
|
|
391
|
+
return node.elseBlock.statements.some((s) => containsIdentifierInStatement(s, name));
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
if (lua.isReturnStatement(node)) {
|
|
396
|
+
return node.expressions.some((e) => containsIdentifier(e, name));
|
|
397
|
+
}
|
|
398
|
+
if (lua.isExpressionStatement(node)) {
|
|
399
|
+
return containsIdentifier(node.expression, name);
|
|
400
|
+
}
|
|
401
|
+
if (lua.isIterationStatement(node)) {
|
|
402
|
+
if (lua.isForStatement(node)) {
|
|
403
|
+
return (node.controlVariable.text === name ||
|
|
404
|
+
containsIdentifier(node.controlVariableInitializer, name) ||
|
|
405
|
+
containsIdentifier(node.limitExpression, name) ||
|
|
406
|
+
(node.stepExpression ? containsIdentifier(node.stepExpression, name) : false) ||
|
|
407
|
+
node.body.statements.some((s) => containsIdentifierInStatement(s, name)));
|
|
408
|
+
}
|
|
409
|
+
if (lua.isForInStatement(node)) {
|
|
410
|
+
return (node.names.some((n) => n.text === name) ||
|
|
411
|
+
node.expressions.some((e) => containsIdentifier(e, name)) ||
|
|
412
|
+
node.body.statements.some((s) => containsIdentifierInStatement(s, name)));
|
|
413
|
+
}
|
|
414
|
+
// While / Repeat
|
|
415
|
+
return (containsIdentifier(node.condition, name) ||
|
|
416
|
+
node.body.statements.some((s) => containsIdentifierInStatement(s, name)));
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
function containsIdentifierInStatement(stmt, name) {
|
|
421
|
+
return containsIdentifier(stmt, name);
|
|
422
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as lua from "typescript-to-lua";
|
|
3
|
+
/**
|
|
4
|
+
* Strip internal JSDoc tags from Lua comments (@indexArg, @indexReturn, @define).
|
|
5
|
+
* These are only consumed by the plugin at transpile time.
|
|
6
|
+
*
|
|
7
|
+
* TSTL generates JSDoc as flat string arrays in leadingComments:
|
|
8
|
+
* ["-", " @define FLAG", " continuation line", " @indexArg 0"]
|
|
9
|
+
* Each string becomes a `-- <text>` line. When we find a tag line, we also
|
|
10
|
+
* remove its continuation lines (those starting with ` ` but not ` @`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripInternalJSDocTags(file: lua.File): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Strip empty module boilerplate from files without explicit exports.
|
|
15
|
+
* `moduleDetection: "force"` causes TSTL to wrap every file as a module;
|
|
16
|
+
* standalone SLua scripts don't need the ____exports wrapper.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stripEmptyModuleBoilerplate(file: lua.File, sourceFiles: readonly ts.SourceFile[] | undefined): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Collapse default-parameter nil-checks into `x = x or <literal>`.
|
|
21
|
+
* Matches: if x == nil then x = <literal> end
|
|
22
|
+
* Safe for strings and numbers (both truthy in Lua); the TS source only
|
|
23
|
+
* generates these for string/number defaults.
|
|
24
|
+
*/
|
|
25
|
+
export declare function collapseDefaultParamNilChecks(file: lua.File): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Shorten TSTL destructuring temp names: ____*_result_* -> _rN.
|
|
28
|
+
* Two-pass: collect unique names in order, then rename all identifiers.
|
|
29
|
+
*/
|
|
30
|
+
export declare function shortenTempNames(file: lua.File): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Collapse consecutive field accesses from the same shortened temp into
|
|
33
|
+
* multi-assignment: `local a = _r0.x; local b = _r0.y` -> `local a, b = _r0.x, _r0.y`.
|
|
34
|
+
* Only collapses when the base matches `_r\d+` (shortened temps).
|
|
35
|
+
*/
|
|
36
|
+
export declare function collapseFieldAccesses(file: lua.File): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Merge forward-declared `local x` with its first `x = value` assignment.
|
|
39
|
+
* Only inlines when there are no references to x between declaration and assignment.
|
|
40
|
+
*/
|
|
41
|
+
export declare function inlineForwardDeclarations(file: lua.File): boolean;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as lua from "typescript-to-lua";
|
|
3
|
+
import { walkBlocks, walkIdentifiers, containsIdentifier } from "./lua-ast-walk.js";
|
|
4
|
+
const JSDOC_TAG_RE = /@(?:index(?:Arg|Return)|define)\b/;
|
|
5
|
+
/**
|
|
6
|
+
* Strip internal JSDoc tags from Lua comments (@indexArg, @indexReturn, @define).
|
|
7
|
+
* These are only consumed by the plugin at transpile time.
|
|
8
|
+
*
|
|
9
|
+
* TSTL generates JSDoc as flat string arrays in leadingComments:
|
|
10
|
+
* ["-", " @define FLAG", " continuation line", " @indexArg 0"]
|
|
11
|
+
* Each string becomes a `-- <text>` line. When we find a tag line, we also
|
|
12
|
+
* remove its continuation lines (those starting with ` ` but not ` @`).
|
|
13
|
+
*/
|
|
14
|
+
export function stripInternalJSDocTags(file) {
|
|
15
|
+
let changed = false;
|
|
16
|
+
walkBlocks(file, (statements) => {
|
|
17
|
+
for (const stmt of statements) {
|
|
18
|
+
if (!stmt.leadingComments || stmt.leadingComments.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
const filtered = [];
|
|
21
|
+
let skip = false;
|
|
22
|
+
for (const comment of stmt.leadingComments) {
|
|
23
|
+
if (typeof comment === "string") {
|
|
24
|
+
if (JSDOC_TAG_RE.test(comment)) {
|
|
25
|
+
// Tag line, skip it and subsequent continuation lines
|
|
26
|
+
skip = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (skip) {
|
|
30
|
+
// Continuation line: starts with space but not with ` @tag`
|
|
31
|
+
if (comment.startsWith(" ") && !comment.match(/^ @\w/)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Not a continuation, stop skipping
|
|
35
|
+
skip = false;
|
|
36
|
+
}
|
|
37
|
+
filtered.push(comment);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Block comment (string[]), strip if any line contains the tag
|
|
41
|
+
skip = false;
|
|
42
|
+
if (comment.some((line) => JSDOC_TAG_RE.test(line))) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
filtered.push(comment);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// If only the JSDoc opener "-" remains, remove it too
|
|
49
|
+
if (filtered.length === 1 && filtered[0] === "-") {
|
|
50
|
+
filtered.length = 0;
|
|
51
|
+
}
|
|
52
|
+
if (filtered.length !== stmt.leadingComments.length) {
|
|
53
|
+
stmt.leadingComments = filtered.length > 0 ? filtered : undefined;
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Strip empty module boilerplate from files without explicit exports.
|
|
62
|
+
* `moduleDetection: "force"` causes TSTL to wrap every file as a module;
|
|
63
|
+
* standalone SLua scripts don't need the ____exports wrapper.
|
|
64
|
+
*/
|
|
65
|
+
export function stripEmptyModuleBoilerplate(file, sourceFiles) {
|
|
66
|
+
const stmts = file.statements;
|
|
67
|
+
if (stmts.length < 2)
|
|
68
|
+
return false;
|
|
69
|
+
// Find `local ____exports = {}` at top level
|
|
70
|
+
const declIdx = stmts.findIndex((s) => lua.isVariableDeclarationStatement(s) &&
|
|
71
|
+
s.left.length === 1 &&
|
|
72
|
+
s.left[0].text === "____exports" &&
|
|
73
|
+
s.right &&
|
|
74
|
+
s.right.length === 1 &&
|
|
75
|
+
lua.isTableExpression(s.right[0]) &&
|
|
76
|
+
s.right[0].fields.length === 0);
|
|
77
|
+
if (declIdx === -1)
|
|
78
|
+
return false;
|
|
79
|
+
// Find `return ____exports` at end
|
|
80
|
+
const last = stmts[stmts.length - 1];
|
|
81
|
+
if (!lua.isReturnStatement(last) ||
|
|
82
|
+
last.expressions.length !== 1 ||
|
|
83
|
+
!lua.isIdentifier(last.expressions[0]) ||
|
|
84
|
+
last.expressions[0].text !== "____exports") {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// Check TS source files for explicit exports
|
|
88
|
+
const hasExplicitExports = sourceFiles?.some((sf) => sf.statements.some((s) => ts.isExportDeclaration(s) ||
|
|
89
|
+
ts.isExportAssignment(s) ||
|
|
90
|
+
(ts.canHaveModifiers(s) &&
|
|
91
|
+
ts.getModifiers(s)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))));
|
|
92
|
+
if (hasExplicitExports)
|
|
93
|
+
return false;
|
|
94
|
+
// Remove both: the decl and the return
|
|
95
|
+
stmts.splice(stmts.length - 1, 1); // remove return first (higher index)
|
|
96
|
+
stmts.splice(declIdx, 1); // then remove decl
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Collapse default-parameter nil-checks into `x = x or <literal>`.
|
|
101
|
+
* Matches: if x == nil then x = <literal> end
|
|
102
|
+
* Safe for strings and numbers (both truthy in Lua); the TS source only
|
|
103
|
+
* generates these for string/number defaults.
|
|
104
|
+
*/
|
|
105
|
+
export function collapseDefaultParamNilChecks(file) {
|
|
106
|
+
let changed = false;
|
|
107
|
+
walkBlocks(file, (statements) => {
|
|
108
|
+
for (let i = 0; i < statements.length; i++) {
|
|
109
|
+
const stmt = statements[i];
|
|
110
|
+
if (!lua.isIfStatement(stmt))
|
|
111
|
+
continue;
|
|
112
|
+
if (stmt.elseBlock)
|
|
113
|
+
continue;
|
|
114
|
+
// condition: x == nil
|
|
115
|
+
if (!lua.isBinaryExpression(stmt.condition))
|
|
116
|
+
continue;
|
|
117
|
+
if (stmt.condition.operator !== lua.SyntaxKind.EqualityOperator)
|
|
118
|
+
continue;
|
|
119
|
+
if (!lua.isIdentifier(stmt.condition.left))
|
|
120
|
+
continue;
|
|
121
|
+
if (!lua.isNilLiteral(stmt.condition.right))
|
|
122
|
+
continue;
|
|
123
|
+
const paramName = stmt.condition.left;
|
|
124
|
+
// ifBlock has exactly 1 statement: x = <literal>
|
|
125
|
+
if (stmt.ifBlock.statements.length !== 1)
|
|
126
|
+
continue;
|
|
127
|
+
const inner = stmt.ifBlock.statements[0];
|
|
128
|
+
if (!lua.isAssignmentStatement(inner))
|
|
129
|
+
continue;
|
|
130
|
+
if (inner.left.length !== 1 || inner.right.length !== 1)
|
|
131
|
+
continue;
|
|
132
|
+
const assignTarget = inner.left[0];
|
|
133
|
+
if (!lua.isIdentifier(assignTarget))
|
|
134
|
+
continue;
|
|
135
|
+
if (assignTarget.text !== paramName.text)
|
|
136
|
+
continue;
|
|
137
|
+
const literal = inner.right[0];
|
|
138
|
+
if (!lua.isStringLiteral(literal) && !lua.isNumericLiteral(literal)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Replace with: x = x or <literal>
|
|
142
|
+
const orExpr = lua.createBinaryExpression(lua.cloneIdentifier(paramName), literal, lua.SyntaxKind.OrOperator);
|
|
143
|
+
const assignment = lua.createAssignmentStatement(lua.cloneIdentifier(paramName), orExpr);
|
|
144
|
+
// Preserve leading comments from the if statement
|
|
145
|
+
if (stmt.leadingComments) {
|
|
146
|
+
assignment.leadingComments = stmt.leadingComments;
|
|
147
|
+
}
|
|
148
|
+
statements[i] = assignment;
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return changed;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Shorten TSTL destructuring temp names: ____*_result_* -> _rN.
|
|
156
|
+
* Two-pass: collect unique names in order, then rename all identifiers.
|
|
157
|
+
*/
|
|
158
|
+
export function shortenTempNames(file) {
|
|
159
|
+
const seen = new Map();
|
|
160
|
+
let counter = 0;
|
|
161
|
+
const tempRe = /^____\w+_result_\d+$/;
|
|
162
|
+
// Pass 1: collect unique temp names in order of first occurrence
|
|
163
|
+
walkIdentifiers(file, (id) => {
|
|
164
|
+
if (tempRe.test(id.text) && !seen.has(id.text)) {
|
|
165
|
+
seen.set(id.text, `_r${counter++}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (seen.size === 0)
|
|
169
|
+
return false;
|
|
170
|
+
// Pass 2: rename all matching identifiers
|
|
171
|
+
walkIdentifiers(file, (id) => {
|
|
172
|
+
const short = seen.get(id.text);
|
|
173
|
+
if (short)
|
|
174
|
+
id.text = short;
|
|
175
|
+
});
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Collapse consecutive field accesses from the same shortened temp into
|
|
180
|
+
* multi-assignment: `local a = _r0.x; local b = _r0.y` -> `local a, b = _r0.x, _r0.y`.
|
|
181
|
+
* Only collapses when the base matches `_r\d+` (shortened temps).
|
|
182
|
+
*/
|
|
183
|
+
export function collapseFieldAccesses(file) {
|
|
184
|
+
let changed = false;
|
|
185
|
+
const tempBaseRe = /^_r\d+$/;
|
|
186
|
+
walkBlocks(file, (statements) => {
|
|
187
|
+
let i = 0;
|
|
188
|
+
while (i < statements.length) {
|
|
189
|
+
const stmt = statements[i];
|
|
190
|
+
// Match: local <name> = <temp>.<field>
|
|
191
|
+
if (!lua.isVariableDeclarationStatement(stmt) ||
|
|
192
|
+
stmt.left.length !== 1 ||
|
|
193
|
+
!stmt.right ||
|
|
194
|
+
stmt.right.length !== 1 ||
|
|
195
|
+
!lua.isTableIndexExpression(stmt.right[0]) ||
|
|
196
|
+
!lua.isIdentifier(stmt.right[0].table) ||
|
|
197
|
+
!tempBaseRe.test(stmt.right[0].table.text)) {
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const baseName = stmt.right[0].table.text;
|
|
202
|
+
// Look ahead for consecutive field accesses on the same base
|
|
203
|
+
const left = [...stmt.left];
|
|
204
|
+
const right = [...stmt.right];
|
|
205
|
+
let end = i + 1;
|
|
206
|
+
while (end < statements.length) {
|
|
207
|
+
const next = statements[end];
|
|
208
|
+
if (lua.isVariableDeclarationStatement(next) &&
|
|
209
|
+
next.left.length === 1 &&
|
|
210
|
+
next.right &&
|
|
211
|
+
next.right.length === 1 &&
|
|
212
|
+
lua.isTableIndexExpression(next.right[0]) &&
|
|
213
|
+
lua.isIdentifier(next.right[0].table) &&
|
|
214
|
+
next.right[0].table.text === baseName) {
|
|
215
|
+
left.push(...next.left);
|
|
216
|
+
right.push(...next.right);
|
|
217
|
+
end++;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (end - i > 1) {
|
|
224
|
+
// Create merged multi-assignment
|
|
225
|
+
const merged = lua.createVariableDeclarationStatement(left, right);
|
|
226
|
+
if (stmt.leadingComments) {
|
|
227
|
+
merged.leadingComments = stmt.leadingComments;
|
|
228
|
+
}
|
|
229
|
+
statements.splice(i, end - i, merged);
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return changed;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Merge forward-declared `local x` with its first `x = value` assignment.
|
|
239
|
+
* Only inlines when there are no references to x between declaration and assignment.
|
|
240
|
+
*/
|
|
241
|
+
export function inlineForwardDeclarations(file) {
|
|
242
|
+
let changed = false;
|
|
243
|
+
walkBlocks(file, (statements) => {
|
|
244
|
+
for (let i = 0; i < statements.length; i++) {
|
|
245
|
+
const stmt = statements[i];
|
|
246
|
+
// Match forward declaration: `local var1, var2, ...` with no initializer
|
|
247
|
+
if (!lua.isVariableDeclarationStatement(stmt) || (stmt.right && stmt.right.length > 0)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const vars = stmt.left;
|
|
251
|
+
const inlined = new Set();
|
|
252
|
+
for (const varId of vars) {
|
|
253
|
+
const varName = varId.text;
|
|
254
|
+
// Scan forward for assignment to same identifier
|
|
255
|
+
for (let j = i + 1; j < statements.length; j++) {
|
|
256
|
+
const candidate = statements[j];
|
|
257
|
+
if (lua.isAssignmentStatement(candidate) &&
|
|
258
|
+
candidate.left.length === 1 &&
|
|
259
|
+
lua.isIdentifier(candidate.left[0]) &&
|
|
260
|
+
candidate.left[0].text === varName &&
|
|
261
|
+
candidate.right.length === 1) {
|
|
262
|
+
// Don't inline if RHS references the variable (self-referencing)
|
|
263
|
+
if (containsIdentifier(candidate.right[0], varName))
|
|
264
|
+
break;
|
|
265
|
+
// Don't inline multi-variable assignments like `a, b = fn()`
|
|
266
|
+
// (already handled by the length check above)
|
|
267
|
+
// Replace assignment with variable declaration
|
|
268
|
+
const newDecl = lua.createVariableDeclarationStatement(lua.cloneIdentifier(candidate.left[0]), candidate.right[0]);
|
|
269
|
+
if (candidate.leadingComments) {
|
|
270
|
+
newDecl.leadingComments = candidate.leadingComments;
|
|
271
|
+
}
|
|
272
|
+
statements[j] = newDecl;
|
|
273
|
+
inlined.add(varName);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
// Reference to varName prevents inlining
|
|
277
|
+
if (containsIdentifier(candidate, varName))
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (inlined.size === 0)
|
|
282
|
+
continue;
|
|
283
|
+
changed = true;
|
|
284
|
+
const remaining = vars.filter((v) => !inlined.has(v.text));
|
|
285
|
+
if (remaining.length === 0) {
|
|
286
|
+
// Remove the forward declaration entirely
|
|
287
|
+
statements.splice(i, 1);
|
|
288
|
+
i--; // re-check this index
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Keep remaining vars in the forward declaration
|
|
292
|
+
statements[i] = lua.createVariableDeclarationStatement(remaining);
|
|
293
|
+
if (stmt.leadingComments) {
|
|
294
|
+
;
|
|
295
|
+
statements[i].leadingComments =
|
|
296
|
+
stmt.leadingComments;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return changed;
|
|
302
|
+
}
|
package/dist/optimize.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface OptimizeFlags {
|
|
|
15
15
|
inlineLocals?: boolean;
|
|
16
16
|
/** Strip `tostring()` from number-typed template literal interpolations. */
|
|
17
17
|
numericConcat?: boolean;
|
|
18
|
+
/** Collapse `if x == nil then x = <literal> end` to `x = x or <literal>`. */
|
|
19
|
+
defaultParams?: boolean;
|
|
18
20
|
}
|
|
19
21
|
export declare const ALL_OPTIMIZE: Required<OptimizeFlags>;
|
|
20
22
|
/**
|
package/dist/optimize.js
CHANGED