@gwigz/slua-tstl-plugin 1.1.0 → 1.3.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 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,9 +322,42 @@ 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
- Some TypeScript patterns pull in large TSTL runtime helpers. Here are recommendations for keeping output lean:
357
+ Some TypeScript patterns pull in large TSTL runtime helpers. Here are recommendations for keeping output lean.
358
+
359
+ > [!TIP]
360
+ > Install [`@gwigz/slua-oxlint-config`](../oxlint-config) to enforce these recommendations at lint time.
312
361
 
313
362
  ### Avoid `delete` on objects
314
363
 
@@ -398,3 +447,7 @@ const seen: Record<string, boolean> = {}
398
447
  ```bash
399
448
  bun run build
400
449
  ```
450
+
451
+ ## Documentation
452
+
453
+ Full API reference and usage examples are available at [slua.gwigz.link/docs/slua](https://slua.gwigz.link/docs/slua).
@@ -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
- export type { CallTransform, OptimizeFlags };
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, escapeRegex, } from "./utils.js";
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
- beforeEmit(program, _options, _emitHost, result) {
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.code.includes("local ____exports = {}\n"))
323
+ if (!file.luaAst)
231
324
  continue;
232
- if (!file.code.trimEnd().endsWith("return ____exports"))
233
- continue;
234
- const hasExplicitExports = file.sourceFiles?.some((sf) => sf.statements.some((s) => ts.isExportDeclaration(s) ||
235
- ts.isExportAssignment(s) ||
236
- (ts.canHaveModifiers(s) &&
237
- ts.getModifiers(s)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))));
238
- if (!hasExplicitExports) {
239
- file.code = file.code
240
- .replace(/local ____exports = \{\}\n/, "")
241
- .replace(/\nreturn ____exports\n?$/, "\n");
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
- // Merge forward-declared `local x` with its first `x = value` assignment.
297
- // Only inlines when there are no references to x between declaration and assignment.
298
- if (opt.inlineLocals) {
299
- for (const file of result) {
300
- const lines = file.code.split("\n");
301
- const removedLines = new Set();
302
- for (let i = 0; i < lines.length; i++) {
303
- if (removedLines.has(i))
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
- // Compound assignment runs in afterEmit so it applies after other plugins
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 {};