@gwigz/slua-tstl-plugin 1.0.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.
@@ -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
+ }
@@ -0,0 +1,31 @@
1
+ import * as ts from "typescript";
2
+ import type { CallTransform } from "./transforms.js";
3
+ export interface OptimizeFlags {
4
+ /** Inline `.filter()` calls as `for` loops with `ipairs`. Default: false */
5
+ filter?: boolean;
6
+ /** Rewrite `x = x + n` to `x += n` (Luau compound assignment). Default: false */
7
+ compoundAssignment?: boolean;
8
+ /** Reorder `Math.floor((a / b) * c)` to `a * c // b`. */
9
+ floorMultiply?: boolean;
10
+ /** Emit bare `string.find`/`table.find` for indexOf presence checks. */
11
+ indexOf?: boolean;
12
+ /** Shorten TSTL destructuring temp names (`____fn_result_N` -> `_rN`). */
13
+ shortenTemps?: boolean;
14
+ /** Merge forward-declared `local x` with its first `x = value` assignment. */
15
+ inlineLocals?: boolean;
16
+ /** Strip `tostring()` from number-typed template literal interpolations. */
17
+ numericConcat?: boolean;
18
+ /** Collapse `if x == nil then x = <literal> end` to `x = x or <literal>`. */
19
+ defaultParams?: boolean;
20
+ }
21
+ export declare const ALL_OPTIMIZE: Required<OptimizeFlags>;
22
+ /**
23
+ * Count `arr.filter(cb)` calls and return a set of file names where inlining
24
+ * should be skipped (the shared `__TS__ArrayFilter` helper is smaller).
25
+ *
26
+ * When `bundle` is true (luaBundle mode), all source files end up in a single
27
+ * output, so the total across the program is what matters. Otherwise each
28
+ * file is counted independently.
29
+ */
30
+ export declare function countFilterCalls(program: ts.Program, bundle: boolean): Set<string>;
31
+ export declare function createOptimizeTransforms(filterSkipFiles: Set<string>): CallTransform[];
@@ -0,0 +1,106 @@
1
+ import * as ts from "typescript";
2
+ import * as tstl from "typescript-to-lua";
3
+ import { isMethodCall, isArrayType } from "./utils.js";
4
+ export const ALL_OPTIMIZE = {
5
+ filter: true,
6
+ compoundAssignment: true,
7
+ floorMultiply: true,
8
+ indexOf: true,
9
+ shortenTemps: true,
10
+ inlineLocals: true,
11
+ numericConcat: true,
12
+ defaultParams: true,
13
+ };
14
+ /**
15
+ * Count `arr.filter(cb)` calls and return a set of file names where inlining
16
+ * should be skipped (the shared `__TS__ArrayFilter` helper is smaller).
17
+ *
18
+ * When `bundle` is true (luaBundle mode), all source files end up in a single
19
+ * output, so the total across the program is what matters. Otherwise each
20
+ * file is counted independently.
21
+ */
22
+ export function countFilterCalls(program, bundle) {
23
+ const skip = new Set();
24
+ const checker = program.getTypeChecker();
25
+ const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile);
26
+ if (bundle) {
27
+ let total = 0;
28
+ for (const sf of sourceFiles) {
29
+ ts.forEachChild(sf, function visit(node) {
30
+ if (isArrayFilterCall(node, checker))
31
+ total++;
32
+ ts.forEachChild(node, visit);
33
+ });
34
+ }
35
+ if (total > 1) {
36
+ for (const sf of sourceFiles) {
37
+ skip.add(sf.fileName);
38
+ }
39
+ }
40
+ }
41
+ else {
42
+ for (const sf of sourceFiles) {
43
+ let count = 0;
44
+ ts.forEachChild(sf, function visit(node) {
45
+ if (isArrayFilterCall(node, checker))
46
+ count++;
47
+ ts.forEachChild(node, visit);
48
+ });
49
+ if (count > 1) {
50
+ skip.add(sf.fileName);
51
+ }
52
+ }
53
+ }
54
+ return skip;
55
+ }
56
+ function isArrayFilterCall(node, checker) {
57
+ return (ts.isCallExpression(node) &&
58
+ ts.isPropertyAccessExpression(node.expression) &&
59
+ node.expression.name.text === "filter" &&
60
+ node.arguments.length === 1 &&
61
+ isArrayType(node.expression.expression, checker));
62
+ }
63
+ export function createOptimizeTransforms(filterSkipFiles) {
64
+ let counter = 0;
65
+ return [
66
+ // arr.filter(cb) -> inline for loop with ipairs
67
+ {
68
+ match: (node, checker) => {
69
+ if (filterSkipFiles.has(node.getSourceFile().fileName))
70
+ return false;
71
+ return isMethodCall(node, checker, isArrayType, "filter", 1);
72
+ },
73
+ emit: (node, context) => {
74
+ const n = counter++;
75
+ const resultId = tstl.createIdentifier(`____opt_${n}`);
76
+ const valueId = tstl.createIdentifier(`____opt_v_${n}`);
77
+ const cbId = tstl.createIdentifier(`____opt_fn_${n}`);
78
+ const arr = context.transformExpression(node.expression.expression);
79
+ const cb = context.transformExpression(node.arguments[0]);
80
+ // Strip TSTL's context parameter (____) from the callback if present.
81
+ // Array callbacks are always called positionally; the context param is dead.
82
+ if (tstl.isFunctionExpression(cb) &&
83
+ cb.params &&
84
+ cb.params.length > 0 &&
85
+ cb.params[0].text === "____") {
86
+ cb.params = cb.params.slice(1);
87
+ }
88
+ // local ____opt_fn_N = <callback>
89
+ context.addPrecedingStatements(tstl.createVariableDeclarationStatement(cbId, cb, node));
90
+ // local ____opt_N = {}
91
+ context.addPrecedingStatements(tstl.createVariableDeclarationStatement(tstl.cloneIdentifier(resultId), tstl.createTableExpression(), node));
92
+ // ____opt_fn_N(____opt_v_N)
93
+ const filterCall = tstl.createCallExpression(tstl.cloneIdentifier(cbId), [
94
+ tstl.cloneIdentifier(valueId),
95
+ ]);
96
+ // ____opt_N[#____opt_N + 1] = ____opt_v_N
97
+ const appendStmt = tstl.createAssignmentStatement(tstl.createTableIndexExpression(tstl.cloneIdentifier(resultId), tstl.createBinaryExpression(tstl.createUnaryExpression(tstl.cloneIdentifier(resultId), tstl.SyntaxKind.LengthOperator), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator)), tstl.cloneIdentifier(valueId));
98
+ // if ____opt_fn_N(____opt_v_N) then ... end
99
+ const ifStmt = tstl.createIfStatement(filterCall, tstl.createBlock([appendStmt]));
100
+ // for _, ____opt_v_N in ipairs(arr) do ... end
101
+ context.addPrecedingStatements(tstl.createForInStatement(tstl.createBlock([ifStmt]), [tstl.createIdentifier("_"), valueId], [tstl.createCallExpression(tstl.createIdentifier("ipairs"), [arr])], node));
102
+ return tstl.cloneIdentifier(resultId);
103
+ },
104
+ },
105
+ ];
106
+ }
@@ -0,0 +1,7 @@
1
+ import * as ts from "typescript";
2
+ import * as tstl from "typescript-to-lua";
3
+ export type CallTransform = {
4
+ match: (node: ts.CallExpression, checker: ts.TypeChecker) => boolean;
5
+ emit: (node: ts.CallExpression, context: tstl.TransformationContext) => tstl.Expression;
6
+ };
7
+ export declare const CALL_TRANSFORMS: CallTransform[];
@@ -0,0 +1,195 @@
1
+ import * as ts from "typescript";
2
+ import * as tstl from "typescript-to-lua";
3
+ import { isMethodCall, isNamespaceCall, isGlobalCall, isStringType, isArrayType, createNamespacedCall, createStringFindCall, } from "./utils.js";
4
+ export const CALL_TRANSFORMS = [
5
+ // JSON.stringify(val) -> lljson.encode(val)
6
+ {
7
+ match: (node) => isNamespaceCall(node, "JSON", "stringify"),
8
+ emit: (node, context) => {
9
+ const args = node.arguments.map((a) => context.transformExpression(a));
10
+ return createNamespacedCall("lljson", "encode", args, node);
11
+ },
12
+ },
13
+ // JSON.parse(str) -> lljson.decode(str)
14
+ {
15
+ match: (node) => isNamespaceCall(node, "JSON", "parse"),
16
+ emit: (node, context) => {
17
+ const args = node.arguments.map((a) => context.transformExpression(a));
18
+ return createNamespacedCall("lljson", "decode", args, node);
19
+ },
20
+ },
21
+ // btoa(str) -> llbase64.encode(str)
22
+ {
23
+ match: (node) => isGlobalCall(node, "btoa") && node.arguments.length === 1,
24
+ emit: (node, context) => {
25
+ const args = node.arguments.map((a) => context.transformExpression(a));
26
+ return createNamespacedCall("llbase64", "encode", args, node);
27
+ },
28
+ },
29
+ // atob(str) -> llbase64.decode(str)
30
+ {
31
+ match: (node) => isGlobalCall(node, "atob") && node.arguments.length === 1,
32
+ emit: (node, context) => {
33
+ const args = node.arguments.map((a) => context.transformExpression(a));
34
+ return createNamespacedCall("llbase64", "decode", args, node);
35
+ },
36
+ },
37
+ // str.toUpperCase() -> ll.ToUpper(str)
38
+ {
39
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "toUpperCase", 0),
40
+ emit: (node, context) => {
41
+ const str = context.transformExpression(node.expression.expression);
42
+ return createNamespacedCall("ll", "ToUpper", [str], node);
43
+ },
44
+ },
45
+ // str.toLowerCase() -> ll.ToLower(str)
46
+ {
47
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "toLowerCase", 0),
48
+ emit: (node, context) => {
49
+ const str = context.transformExpression(node.expression.expression);
50
+ return createNamespacedCall("ll", "ToLower", [str], node);
51
+ },
52
+ },
53
+ // str.trim() -> ll.StringTrim(str, STRING_TRIM)
54
+ {
55
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "trim", 0),
56
+ emit: (node, context) => {
57
+ const str = context.transformExpression(node.expression.expression);
58
+ return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM")], node);
59
+ },
60
+ },
61
+ // str.trimStart() -> ll.StringTrim(str, STRING_TRIM_HEAD)
62
+ {
63
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "trimStart", 0),
64
+ emit: (node, context) => {
65
+ const str = context.transformExpression(node.expression.expression);
66
+ return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM_HEAD")], node);
67
+ },
68
+ },
69
+ // str.trimEnd() -> ll.StringTrim(str, STRING_TRIM_TAIL)
70
+ {
71
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "trimEnd", 0),
72
+ emit: (node, context) => {
73
+ const str = context.transformExpression(node.expression.expression);
74
+ return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM_TAIL")], node);
75
+ },
76
+ },
77
+ // str.indexOf(x) -> (string.find(str, x, 1, true) or 0) - 1
78
+ {
79
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "indexOf", 1),
80
+ emit: (node, context) => {
81
+ const str = context.transformExpression(node.expression.expression);
82
+ const search = context.transformExpression(node.arguments[0]);
83
+ const findOrZero = tstl.createBinaryExpression(createStringFindCall(str, search, node), tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
84
+ return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
85
+ },
86
+ },
87
+ // str.indexOf(x, fromIndex) -> (string.find(str, x, fromIndex + 1, true) or 0) - 1
88
+ {
89
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "indexOf", 2),
90
+ emit: (node, context) => {
91
+ const str = context.transformExpression(node.expression.expression);
92
+ const search = context.transformExpression(node.arguments[0]);
93
+ const fromArg = node.arguments[1];
94
+ const init = ts.isNumericLiteral(fromArg)
95
+ ? tstl.createNumericLiteral(Number(fromArg.text) + 1)
96
+ : tstl.createBinaryExpression(context.transformExpression(fromArg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, node);
97
+ const findCall = createNamespacedCall("string", "find", [str, search, init, tstl.createBooleanLiteral(true)], node);
98
+ const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
99
+ return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
100
+ },
101
+ },
102
+ // str.includes(x) -> string.find(str, x, 1, true) ~= nil
103
+ {
104
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "includes", 1),
105
+ emit: (node, context) => {
106
+ const str = context.transformExpression(node.expression.expression);
107
+ const search = context.transformExpression(node.arguments[0]);
108
+ return tstl.createBinaryExpression(createStringFindCall(str, search, node), tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node);
109
+ },
110
+ },
111
+ // str.split(sep) -> string.split(str, sep) (1-arg only)
112
+ {
113
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "split", 1),
114
+ emit: (node, context) => {
115
+ const str = context.transformExpression(node.expression.expression);
116
+ const sep = context.transformExpression(node.arguments[0]);
117
+ return createNamespacedCall("string", "split", [str, sep], node);
118
+ },
119
+ },
120
+ // str.repeat(n) -> string.rep(str, n)
121
+ {
122
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "repeat", 1),
123
+ emit: (node, context) => {
124
+ const str = context.transformExpression(node.expression.expression);
125
+ const n = context.transformExpression(node.arguments[0]);
126
+ return createNamespacedCall("string", "rep", [str, n], node);
127
+ },
128
+ },
129
+ // str.startsWith(search) -> string.find(str, search, 1, true) == 1 (1-arg only)
130
+ {
131
+ match: (node, checker) => isMethodCall(node, checker, isStringType, "startsWith", 1),
132
+ emit: (node, context) => {
133
+ const str = context.transformExpression(node.expression.expression);
134
+ const search = context.transformExpression(node.arguments[0]);
135
+ return tstl.createBinaryExpression(createStringFindCall(str, search, node), tstl.createNumericLiteral(1), tstl.SyntaxKind.EqualityOperator, node);
136
+ },
137
+ },
138
+ // str.substring(start) -> string.sub(str, start + 1)
139
+ // str.substring(start, end) -> string.sub(str, start + 1, end)
140
+ {
141
+ match: (node, checker) => {
142
+ if (!isMethodCall(node, checker, isStringType, "substring")) {
143
+ return false;
144
+ }
145
+ return node.arguments.length === 1 || node.arguments.length === 2;
146
+ },
147
+ emit: (node, context) => {
148
+ const str = context.transformExpression(node.expression.expression);
149
+ const startArg = node.arguments[0];
150
+ const start = ts.isNumericLiteral(startArg)
151
+ ? tstl.createNumericLiteral(Number(startArg.text) + 1)
152
+ : tstl.createBinaryExpression(context.transformExpression(startArg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, node);
153
+ const args = [str, start];
154
+ if (node.arguments.length === 2) {
155
+ args.push(context.transformExpression(node.arguments[1]));
156
+ }
157
+ return createNamespacedCall("string", "sub", args, node);
158
+ },
159
+ },
160
+ // str.replace / str.replaceAll -> ll.ReplaceSubString(str, search, replacement, count)
161
+ // count=1 for replace (first match only), count=0 for replaceAll
162
+ ...[
163
+ ["replace", 1],
164
+ ["replaceAll", 0],
165
+ ].map(([method, count]) => ({
166
+ match: (node, checker) => isMethodCall(node, checker, isStringType, method, 2),
167
+ emit: (node, context) => {
168
+ const str = context.transformExpression(node.expression.expression);
169
+ const search = context.transformExpression(node.arguments[0]);
170
+ const replacement = context.transformExpression(node.arguments[1]);
171
+ return createNamespacedCall("ll", "ReplaceSubString", [str, search, replacement, tstl.createNumericLiteral(count)], node);
172
+ },
173
+ })),
174
+ // arr.includes(val) -> table.find(arr, val) ~= nil
175
+ {
176
+ match: (node, checker) => isMethodCall(node, checker, isArrayType, "includes", 1),
177
+ emit: (node, context) => {
178
+ const arr = context.transformExpression(node.expression.expression);
179
+ const val = context.transformExpression(node.arguments[0]);
180
+ const findCall = createNamespacedCall("table", "find", [arr, val], node);
181
+ return tstl.createBinaryExpression(findCall, tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node);
182
+ },
183
+ },
184
+ // arr.indexOf(val) -> (table.find(arr, val) or 0) - 1 (1-arg only)
185
+ {
186
+ match: (node, checker) => isMethodCall(node, checker, isArrayType, "indexOf", 1),
187
+ emit: (node, context) => {
188
+ const arr = context.transformExpression(node.expression.expression);
189
+ const val = context.transformExpression(node.arguments[0]);
190
+ const findCall = createNamespacedCall("table", "find", [arr, val], node);
191
+ const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
192
+ return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
193
+ },
194
+ },
195
+ ];