@gwigz/slua-tstl-plugin 1.0.0 → 1.1.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 +188 -16
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +50 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +352 -659
- package/dist/optimize.d.ts +29 -0
- package/dist/optimize.js +105 -0
- package/dist/transforms.d.ts +7 -0
- package/dist/transforms.js +195 -0
- package/dist/utils.d.ts +130 -0
- package/dist/utils.js +412 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,676 +1,369 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import * as tstl from "typescript-to-lua";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Creates a `bit32.<fn>(...args)` Lua call expression.
|
|
25
|
-
* The optional `node` attaches TypeScript source-map information; when
|
|
26
|
-
* patching already-lowered Lua AST nodes (e.g. from compound-assignment
|
|
27
|
-
* desugaring) there is no originating TS node, so it may be omitted.
|
|
28
|
-
*/
|
|
29
|
-
function createBit32Call(fn, args, node) {
|
|
30
|
-
return tstl.createCallExpression(tstl.createTableIndexExpression(tstl.createIdentifier("bit32"), tstl.createStringLiteral(fn)), args, node);
|
|
31
|
-
}
|
|
32
|
-
const BINARY_BITWISE_OPS = {
|
|
33
|
-
[ts.SyntaxKind.AmpersandToken]: "band",
|
|
34
|
-
[ts.SyntaxKind.BarToken]: "bor",
|
|
35
|
-
[ts.SyntaxKind.CaretToken]: "bxor",
|
|
36
|
-
[ts.SyntaxKind.LessThanLessThanToken]: "lshift",
|
|
37
|
-
[ts.SyntaxKind.GreaterThanGreaterThanToken]: "arshift",
|
|
38
|
-
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: "rshift",
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Compound bitwise assignment tokens (`&=`, `|=`, etc.) map to the same
|
|
42
|
-
* `bit32.*` functions as their non-compound counterparts. We handle
|
|
43
|
-
* these at the TypeScript AST level rather than patching the Lua AST,
|
|
44
|
-
* because TSTL's desugaring loses the distinction between `>>=`
|
|
45
|
-
* (arshift) and `>>>=` (rshift) -- both lower to the same Lua operator.
|
|
46
|
-
*/
|
|
47
|
-
const COMPOUND_BITWISE_OPS = {
|
|
48
|
-
[ts.SyntaxKind.AmpersandEqualsToken]: "band",
|
|
49
|
-
[ts.SyntaxKind.BarEqualsToken]: "bor",
|
|
50
|
-
[ts.SyntaxKind.CaretEqualsToken]: "bxor",
|
|
51
|
-
[ts.SyntaxKind.LessThanLessThanEqualsToken]: "lshift",
|
|
52
|
-
[ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: "arshift",
|
|
53
|
-
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: "rshift",
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Returns true when `node` is `Math.floor(<single-arg>)`.
|
|
57
|
-
*/
|
|
58
|
-
function isMathFloor(node) {
|
|
59
|
-
return (node.arguments.length === 1 &&
|
|
60
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
61
|
-
ts.isIdentifier(node.expression.expression) &&
|
|
62
|
-
node.expression.expression.text === "Math" &&
|
|
63
|
-
node.expression.name.text === "floor");
|
|
64
|
-
}
|
|
65
|
-
const EQUALITY_OPS = new Set([
|
|
66
|
-
ts.SyntaxKind.EqualsEqualsToken,
|
|
67
|
-
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
68
|
-
ts.SyntaxKind.ExclamationEqualsToken,
|
|
69
|
-
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
70
|
-
]);
|
|
71
|
-
function isZeroLiteral(node) {
|
|
72
|
-
return ts.isNumericLiteral(node) && node.text === "0";
|
|
73
|
-
}
|
|
74
|
-
function isNegatedEquality(op) {
|
|
75
|
-
return (op === ts.SyntaxKind.ExclamationEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Detect `(a & b) !== 0`, `0 === (a & b)`, etc. and return the `&` expression
|
|
79
|
-
* plus whether to negate the btest result.
|
|
80
|
-
*/
|
|
81
|
-
function extractBtestPattern(node) {
|
|
82
|
-
const op = node.operatorToken.kind;
|
|
83
|
-
if (!EQUALITY_OPS.has(op))
|
|
84
|
-
return null;
|
|
85
|
-
let bandExpr;
|
|
86
|
-
if (isZeroLiteral(node.right)) {
|
|
87
|
-
bandExpr = ts.isParenthesizedExpression(node.left) ? node.left.expression : node.left;
|
|
88
|
-
}
|
|
89
|
-
else if (isZeroLiteral(node.left)) {
|
|
90
|
-
bandExpr = ts.isParenthesizedExpression(node.right) ? node.right.expression : node.right;
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
if (!ts.isBinaryExpression(bandExpr) ||
|
|
96
|
-
bandExpr.operatorToken.kind !== ts.SyntaxKind.AmpersandToken) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
// `== 0` / `=== 0` mean "no bits in common", so we negate btest (negate = true).
|
|
100
|
-
// `!= 0` / `!== 0` mean "some bits in common", which is what btest returns
|
|
101
|
-
// directly, so no negation is needed. The `!isNegatedEquality` double-negative
|
|
102
|
-
// captures this: negated equality (!=) -> false -> don't negate btest.
|
|
103
|
-
return { band: bandExpr, negate: !isNegatedEquality(op) };
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Type-checking helpers for catalog transforms.
|
|
107
|
-
*/
|
|
108
|
-
function isStringType(expr, checker) {
|
|
109
|
-
const type = checker.getTypeAtLocation(expr);
|
|
110
|
-
return !!(type.flags & ts.TypeFlags.StringLike);
|
|
111
|
-
}
|
|
112
|
-
function isArrayType(expr, checker) {
|
|
113
|
-
const type = checker.getTypeAtLocation(expr);
|
|
114
|
-
return checker.isArrayLikeType(type);
|
|
115
|
-
}
|
|
116
|
-
function isDetectedEventType(expr, checker) {
|
|
117
|
-
const type = checker.getTypeAtLocation(expr);
|
|
118
|
-
return type.symbol?.name === "DetectedEvent";
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Returns true when `node` is `detectedEvent.index`, a property access
|
|
122
|
-
* on a `DetectedEvent` reading the `.index` field.
|
|
123
|
-
*/
|
|
124
|
-
function isDetectedEventIndex(node, checker) {
|
|
125
|
-
return node.name.text === "index" && isDetectedEventType(node.expression, checker);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Checks whether `node` is `obj.method(args)` where `obj` matches the
|
|
129
|
-
* given type predicate and the method name matches.
|
|
130
|
-
*/
|
|
131
|
-
function isMethodCall(node, checker, typeGuard, method, argCount) {
|
|
132
|
-
if (!ts.isPropertyAccessExpression(node.expression))
|
|
133
|
-
return false;
|
|
134
|
-
if (node.expression.name.text !== method)
|
|
135
|
-
return false;
|
|
136
|
-
if (argCount !== undefined && node.arguments.length !== argCount)
|
|
137
|
-
return false;
|
|
138
|
-
return typeGuard(node.expression.expression, checker);
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Checks whether `node` is `Namespace.method(args)` using syntactic
|
|
142
|
-
* identifier matching (no TypeChecker needed).
|
|
143
|
-
*/
|
|
144
|
-
function isNamespaceCall(node, namespace, method) {
|
|
145
|
-
if (!ts.isPropertyAccessExpression(node.expression))
|
|
146
|
-
return false;
|
|
147
|
-
if (node.expression.name.text !== method)
|
|
148
|
-
return false;
|
|
149
|
-
return (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === namespace);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Checks whether `node` is a call to a global function by name.
|
|
153
|
-
*/
|
|
154
|
-
function isGlobalCall(node, name) {
|
|
155
|
-
return ts.isIdentifier(node.expression) && node.expression.text === name;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Creates a `ns.fn(...args)` Lua call expression.
|
|
159
|
-
*/
|
|
160
|
-
function createNamespacedCall(ns, fn, args, node) {
|
|
161
|
-
return tstl.createCallExpression(tstl.createTableIndexExpression(tstl.createIdentifier(ns), tstl.createStringLiteral(fn)), args, node);
|
|
162
|
-
}
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// Self-reassignment array concat -> table.extend optimization
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
/**
|
|
167
|
-
* Detects `arr = arr.concat(b, c, ...)` where LHS is a simple identifier,
|
|
168
|
-
* the receiver matches LHS, and all concat arguments are array-typed.
|
|
169
|
-
*/
|
|
170
|
-
function extractConcatSelfAssignment(expr, checker) {
|
|
171
|
-
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
172
|
-
return null;
|
|
173
|
-
if (!ts.isIdentifier(expr.left))
|
|
174
|
-
return null;
|
|
175
|
-
if (!ts.isCallExpression(expr.right))
|
|
176
|
-
return null;
|
|
177
|
-
if (!ts.isPropertyAccessExpression(expr.right.expression))
|
|
178
|
-
return null;
|
|
179
|
-
if (expr.right.expression.name.text !== "concat")
|
|
180
|
-
return null;
|
|
181
|
-
if (!ts.isIdentifier(expr.right.expression.expression))
|
|
182
|
-
return null;
|
|
183
|
-
if (expr.right.expression.expression.text !== expr.left.text)
|
|
184
|
-
return null;
|
|
185
|
-
if (expr.right.arguments.length === 0)
|
|
186
|
-
return null;
|
|
187
|
-
for (const arg of expr.right.arguments) {
|
|
188
|
-
if (!isArrayType(arg, checker))
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
return { name: expr.left, args: expr.right.arguments };
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Detects `arr = [...arr, ...b, ...c]` where LHS is a simple identifier,
|
|
195
|
-
* all elements are spreads, the first spread matches LHS, and all tail
|
|
196
|
-
* spread expressions are array-typed.
|
|
197
|
-
*/
|
|
198
|
-
function extractSpreadSelfAssignment(expr, checker) {
|
|
199
|
-
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
200
|
-
return null;
|
|
201
|
-
if (!ts.isIdentifier(expr.left))
|
|
202
|
-
return null;
|
|
203
|
-
if (!ts.isArrayLiteralExpression(expr.right))
|
|
204
|
-
return null;
|
|
205
|
-
const elements = expr.right.elements;
|
|
206
|
-
if (elements.length < 2)
|
|
207
|
-
return null;
|
|
208
|
-
for (const el of elements) {
|
|
209
|
-
if (!ts.isSpreadElement(el))
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
const first = elements[0];
|
|
213
|
-
if (!ts.isIdentifier(first.expression))
|
|
214
|
-
return null;
|
|
215
|
-
if (first.expression.text !== expr.left.text)
|
|
216
|
-
return null;
|
|
217
|
-
const tailArgs = [];
|
|
218
|
-
for (let i = 1; i < elements.length; i++) {
|
|
219
|
-
const spread = elements[i];
|
|
220
|
-
if (!isArrayType(spread.expression, checker))
|
|
221
|
-
return null;
|
|
222
|
-
tailArgs.push(spread.expression);
|
|
223
|
-
}
|
|
224
|
-
return { name: expr.left, args: tailArgs };
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Builds nested `table.extend` calls:
|
|
228
|
-
* - Single arg: `table.extend(arr, b)`
|
|
229
|
-
* - Multiple: `table.extend(table.extend(arr, b), c)`
|
|
230
|
-
*/
|
|
231
|
-
function emitChainedExtend(target, args, node) {
|
|
232
|
-
let result = createNamespacedCall("table", "extend", [target, args[0]], node);
|
|
233
|
-
for (let i = 1; i < args.length; i++) {
|
|
234
|
-
result = createNamespacedCall("table", "extend", [result, args[i]], node);
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* For an `ll.Foo(...)` call, inspect the resolved signature's JSDoc tags
|
|
240
|
-
* to determine which arguments have `@indexArg` semantics and whether
|
|
241
|
-
* the return value has `@indexReturn` semantics.
|
|
242
|
-
*/
|
|
243
|
-
function getLLIndexSemantics(node, checker) {
|
|
244
|
-
// Must be ll.Something(...)
|
|
245
|
-
if (!ts.isPropertyAccessExpression(node.expression))
|
|
246
|
-
return null;
|
|
247
|
-
if (!ts.isIdentifier(node.expression.expression))
|
|
248
|
-
return null;
|
|
249
|
-
if (node.expression.expression.text !== "ll")
|
|
250
|
-
return null;
|
|
251
|
-
const sig = checker.getResolvedSignature(node);
|
|
252
|
-
const decl = sig?.declaration;
|
|
253
|
-
if (!decl || !ts.isFunctionDeclaration(decl))
|
|
254
|
-
return null;
|
|
255
|
-
const tags = ts.getJSDocTags(decl);
|
|
256
|
-
const indexArgs = new Set();
|
|
257
|
-
let indexReturn = false;
|
|
258
|
-
for (const tag of tags) {
|
|
259
|
-
const tagName = tag.tagName.text;
|
|
260
|
-
if (tagName === "indexArg") {
|
|
261
|
-
const text = typeof tag.comment === "string"
|
|
262
|
-
? tag.comment
|
|
263
|
-
: Array.isArray(tag.comment)
|
|
264
|
-
? tag.comment.map((c) => c.text).join("")
|
|
265
|
-
: undefined;
|
|
266
|
-
if (text) {
|
|
267
|
-
indexArgs.add(text.trim());
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
else if (tagName === "indexReturn") {
|
|
271
|
-
indexReturn = true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (indexArgs.size === 0 && !indexReturn)
|
|
275
|
-
return null;
|
|
276
|
-
return { indexArgs, indexReturn };
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Adjusts a 0-based index argument to 1-based for Lua.
|
|
280
|
-
* Constant-folds numeric literals; otherwise emits `expr + 1`.
|
|
281
|
-
*/
|
|
282
|
-
function adjustIndexArg(arg, context) {
|
|
283
|
-
if (ts.isNumericLiteral(arg)) {
|
|
284
|
-
return tstl.createNumericLiteral(Number(arg.text) + 1);
|
|
285
|
-
}
|
|
286
|
-
// DetectedEvent.index is already 1-based at runtime. The PropertyAccess
|
|
287
|
-
// visitor emits `- 1` to make it 0-based for TS, and @indexArg adds `+ 1`.
|
|
288
|
-
// These cancel out, so emit the raw property access directly.
|
|
289
|
-
if (ts.isPropertyAccessExpression(arg) && isDetectedEventIndex(arg, context.checker)) {
|
|
290
|
-
const obj = context.transformExpression(arg.expression);
|
|
291
|
-
return tstl.createTableIndexExpression(obj, tstl.createStringLiteral("index"));
|
|
292
|
-
}
|
|
293
|
-
return tstl.createBinaryExpression(context.transformExpression(arg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, arg);
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Emit an `ll.Foo(...)` call with automatic index adjustments:
|
|
297
|
-
* - `@indexArg` parameters get `+1`
|
|
298
|
-
* - `@indexReturn` wraps the result in a nil-safe `__tmp and (__tmp - 1)`
|
|
299
|
-
*/
|
|
300
|
-
function emitLLIndexCall(node, context, semantics) {
|
|
301
|
-
const expr = node.expression;
|
|
302
|
-
const fnName = expr.name.text;
|
|
303
|
-
// Resolve parameter names from the declaration
|
|
304
|
-
const sig = context.checker.getResolvedSignature(node);
|
|
305
|
-
const params = sig?.declaration && ts.isFunctionDeclaration(sig.declaration)
|
|
306
|
-
? sig.declaration.parameters
|
|
307
|
-
: undefined;
|
|
308
|
-
const args = node.arguments.map((arg, i) => {
|
|
309
|
-
const paramName = params?.[i]?.name;
|
|
310
|
-
const name = paramName && ts.isIdentifier(paramName) ? paramName.text : undefined;
|
|
311
|
-
if (name && semantics.indexArgs.has(name)) {
|
|
312
|
-
return adjustIndexArg(arg, context);
|
|
313
|
-
}
|
|
314
|
-
return context.transformExpression(arg);
|
|
315
|
-
});
|
|
316
|
-
const call = createNamespacedCall("ll", fnName, args, node);
|
|
317
|
-
if (!semantics.indexReturn) {
|
|
318
|
-
return call;
|
|
319
|
-
}
|
|
320
|
-
// Check if return type is number-like (skip for list/string returns like llFindNotecardTextSync)
|
|
321
|
-
const retType = context.checker.getTypeAtLocation(node);
|
|
322
|
-
const isNumberReturn = !!(retType.flags & ts.TypeFlags.NumberLike) ||
|
|
323
|
-
(retType.isUnion() && retType.types.some((t) => !!(t.flags & ts.TypeFlags.NumberLike)));
|
|
324
|
-
if (!isNumberReturn) {
|
|
325
|
-
return call;
|
|
326
|
-
}
|
|
327
|
-
// Nil-safe return adjustment: `local __tmp = ll.Fn(...); __tmp and (__tmp - 1)`
|
|
328
|
-
const tempId = tstl.createIdentifier("____tmp");
|
|
329
|
-
context.addPrecedingStatements(tstl.createVariableDeclarationStatement(tempId, call, node));
|
|
330
|
-
return tstl.createBinaryExpression(tstl.cloneIdentifier(tempId), tstl.createParenthesizedExpression(tstl.createBinaryExpression(tstl.cloneIdentifier(tempId), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator)), tstl.SyntaxKind.AndOperator, node);
|
|
331
|
-
}
|
|
332
|
-
const CALL_TRANSFORMS = [
|
|
333
|
-
// JSON.stringify(val) -> lljson.encode(val)
|
|
334
|
-
{
|
|
335
|
-
match: (node) => isNamespaceCall(node, "JSON", "stringify"),
|
|
336
|
-
emit: (node, context) => {
|
|
337
|
-
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
338
|
-
return createNamespacedCall("lljson", "encode", args, node);
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
// JSON.parse(str) -> lljson.decode(str)
|
|
342
|
-
{
|
|
343
|
-
match: (node) => isNamespaceCall(node, "JSON", "parse"),
|
|
344
|
-
emit: (node, context) => {
|
|
345
|
-
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
346
|
-
return createNamespacedCall("lljson", "decode", args, node);
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
|
-
// btoa(str) -> llbase64.encode(str)
|
|
350
|
-
{
|
|
351
|
-
match: (node) => isGlobalCall(node, "btoa") && node.arguments.length === 1,
|
|
352
|
-
emit: (node, context) => {
|
|
353
|
-
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
354
|
-
return createNamespacedCall("llbase64", "encode", args, node);
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
// atob(str) -> llbase64.decode(str)
|
|
358
|
-
{
|
|
359
|
-
match: (node) => isGlobalCall(node, "atob") && node.arguments.length === 1,
|
|
360
|
-
emit: (node, context) => {
|
|
361
|
-
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
362
|
-
return createNamespacedCall("llbase64", "decode", args, node);
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
// str.toUpperCase() -> ll.ToUpper(str)
|
|
366
|
-
{
|
|
367
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "toUpperCase", 0),
|
|
368
|
-
emit: (node, context) => {
|
|
369
|
-
const str = context.transformExpression(node.expression.expression);
|
|
370
|
-
return createNamespacedCall("ll", "ToUpper", [str], node);
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
// str.toLowerCase() -> ll.ToLower(str)
|
|
374
|
-
{
|
|
375
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "toLowerCase", 0),
|
|
376
|
-
emit: (node, context) => {
|
|
377
|
-
const str = context.transformExpression(node.expression.expression);
|
|
378
|
-
return createNamespacedCall("ll", "ToLower", [str], node);
|
|
379
|
-
},
|
|
380
|
-
},
|
|
381
|
-
// str.trim() -> ll.StringTrim(str, STRING_TRIM)
|
|
382
|
-
{
|
|
383
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "trim", 0),
|
|
384
|
-
emit: (node, context) => {
|
|
385
|
-
const str = context.transformExpression(node.expression.expression);
|
|
386
|
-
return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM")], node);
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
// str.trimStart() -> ll.StringTrim(str, STRING_TRIM_HEAD)
|
|
390
|
-
{
|
|
391
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "trimStart", 0),
|
|
392
|
-
emit: (node, context) => {
|
|
393
|
-
const str = context.transformExpression(node.expression.expression);
|
|
394
|
-
return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM_HEAD")], node);
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
// str.trimEnd() -> ll.StringTrim(str, STRING_TRIM_TAIL)
|
|
398
|
-
{
|
|
399
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "trimEnd", 0),
|
|
400
|
-
emit: (node, context) => {
|
|
401
|
-
const str = context.transformExpression(node.expression.expression);
|
|
402
|
-
return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM_TAIL")], node);
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
// str.indexOf(x) -> (string.find(str, x, 1, true) or 0) - 1
|
|
406
|
-
{
|
|
407
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "indexOf", 1),
|
|
408
|
-
emit: (node, context) => {
|
|
409
|
-
const str = context.transformExpression(node.expression.expression);
|
|
410
|
-
const search = context.transformExpression(node.arguments[0]);
|
|
411
|
-
const findCall = createNamespacedCall("string", "find", [str, search, tstl.createNumericLiteral(1), tstl.createBooleanLiteral(true)], node);
|
|
412
|
-
const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
|
|
413
|
-
return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
// str.indexOf(x, fromIndex) -> (string.find(str, x, fromIndex + 1, true) or 0) - 1
|
|
417
|
-
{
|
|
418
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "indexOf", 2),
|
|
419
|
-
emit: (node, context) => {
|
|
420
|
-
const str = context.transformExpression(node.expression.expression);
|
|
421
|
-
const search = context.transformExpression(node.arguments[0]);
|
|
422
|
-
const fromArg = node.arguments[1];
|
|
423
|
-
const init = ts.isNumericLiteral(fromArg)
|
|
424
|
-
? tstl.createNumericLiteral(Number(fromArg.text) + 1)
|
|
425
|
-
: tstl.createBinaryExpression(context.transformExpression(fromArg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, node);
|
|
426
|
-
const findCall = createNamespacedCall("string", "find", [str, search, init, tstl.createBooleanLiteral(true)], node);
|
|
427
|
-
const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
|
|
428
|
-
return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
// str.includes(x) -> string.find(str, x, 1, true) ~= nil
|
|
432
|
-
{
|
|
433
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "includes", 1),
|
|
434
|
-
emit: (node, context) => {
|
|
435
|
-
const str = context.transformExpression(node.expression.expression);
|
|
436
|
-
const search = context.transformExpression(node.arguments[0]);
|
|
437
|
-
const findCall = createNamespacedCall("string", "find", [str, search, tstl.createNumericLiteral(1), tstl.createBooleanLiteral(true)], node);
|
|
438
|
-
return tstl.createBinaryExpression(findCall, tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node);
|
|
439
|
-
},
|
|
440
|
-
},
|
|
441
|
-
// str.split(sep) -> string.split(str, sep) (1-arg only)
|
|
442
|
-
{
|
|
443
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "split", 1),
|
|
444
|
-
emit: (node, context) => {
|
|
445
|
-
const str = context.transformExpression(node.expression.expression);
|
|
446
|
-
const sep = context.transformExpression(node.arguments[0]);
|
|
447
|
-
return createNamespacedCall("string", "split", [str, sep], node);
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
// str.repeat(n) -> string.rep(str, n)
|
|
451
|
-
{
|
|
452
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "repeat", 1),
|
|
453
|
-
emit: (node, context) => {
|
|
454
|
-
const str = context.transformExpression(node.expression.expression);
|
|
455
|
-
const n = context.transformExpression(node.arguments[0]);
|
|
456
|
-
return createNamespacedCall("string", "rep", [str, n], node);
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
// str.startsWith(search) -> string.find(str, search, 1, true) == 1 (1-arg only)
|
|
460
|
-
{
|
|
461
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "startsWith", 1),
|
|
462
|
-
emit: (node, context) => {
|
|
463
|
-
const str = context.transformExpression(node.expression.expression);
|
|
464
|
-
const search = context.transformExpression(node.arguments[0]);
|
|
465
|
-
const findCall = createNamespacedCall("string", "find", [str, search, tstl.createNumericLiteral(1), tstl.createBooleanLiteral(true)], node);
|
|
466
|
-
return tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(1), tstl.SyntaxKind.EqualityOperator, node);
|
|
467
|
-
},
|
|
468
|
-
},
|
|
469
|
-
// str.substring(start) -> string.sub(str, start + 1)
|
|
470
|
-
// str.substring(start, end) -> string.sub(str, start + 1, end)
|
|
471
|
-
{
|
|
472
|
-
match: (node, checker) => {
|
|
473
|
-
if (!isMethodCall(node, checker, isStringType, "substring")) {
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
|
-
return node.arguments.length === 1 || node.arguments.length === 2;
|
|
477
|
-
},
|
|
478
|
-
emit: (node, context) => {
|
|
479
|
-
const str = context.transformExpression(node.expression.expression);
|
|
480
|
-
const startArg = node.arguments[0];
|
|
481
|
-
const start = ts.isNumericLiteral(startArg)
|
|
482
|
-
? tstl.createNumericLiteral(Number(startArg.text) + 1)
|
|
483
|
-
: tstl.createBinaryExpression(context.transformExpression(startArg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, node);
|
|
484
|
-
const args = [str, start];
|
|
485
|
-
if (node.arguments.length === 2) {
|
|
486
|
-
args.push(context.transformExpression(node.arguments[1]));
|
|
487
|
-
}
|
|
488
|
-
return createNamespacedCall("string", "sub", args, node);
|
|
489
|
-
},
|
|
490
|
-
},
|
|
491
|
-
// str.replaceAll(search, replacement) -> ll.ReplaceSubString(str, search, replacement, 0)
|
|
492
|
-
{
|
|
493
|
-
match: (node, checker) => isMethodCall(node, checker, isStringType, "replaceAll", 2),
|
|
494
|
-
emit: (node, context) => {
|
|
495
|
-
const str = context.transformExpression(node.expression.expression);
|
|
496
|
-
const search = context.transformExpression(node.arguments[0]);
|
|
497
|
-
const replacement = context.transformExpression(node.arguments[1]);
|
|
498
|
-
return createNamespacedCall("ll", "ReplaceSubString", [str, search, replacement, tstl.createNumericLiteral(0)], node);
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
// arr.includes(val) -> table.find(arr, val) ~= nil
|
|
502
|
-
{
|
|
503
|
-
match: (node, checker) => isMethodCall(node, checker, isArrayType, "includes", 1),
|
|
504
|
-
emit: (node, context) => {
|
|
505
|
-
const arr = context.transformExpression(node.expression.expression);
|
|
506
|
-
const val = context.transformExpression(node.arguments[0]);
|
|
507
|
-
const findCall = createNamespacedCall("table", "find", [arr, val], node);
|
|
508
|
-
return tstl.createBinaryExpression(findCall, tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node);
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
// arr.indexOf(val) -> (table.find(arr, val) or 0) - 1 (1-arg only)
|
|
512
|
-
{
|
|
513
|
-
match: (node, checker) => isMethodCall(node, checker, isArrayType, "indexOf", 1),
|
|
514
|
-
emit: (node, context) => {
|
|
515
|
-
const arr = context.transformExpression(node.expression.expression);
|
|
516
|
-
const val = context.transformExpression(node.arguments[0]);
|
|
517
|
-
const findCall = createNamespacedCall("table", "find", [arr, val], node);
|
|
518
|
-
const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
|
|
519
|
-
return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
520
|
-
},
|
|
521
|
-
},
|
|
522
|
-
];
|
|
523
|
-
const plugin = {
|
|
524
|
-
visitors: {
|
|
525
|
-
[ts.SyntaxKind.PropertyAccessExpression]: (node, context) => {
|
|
526
|
-
const result = context.superTransformExpression(node);
|
|
527
|
-
// Rewrite identifiers in the Lua AST (PascalCase -> lowercase, TSTL keyword fixups).
|
|
528
|
-
if (tstl.isTableIndexExpression(result) && tstl.isIdentifier(result.table)) {
|
|
529
|
-
const replacement = PASCAL_TO_LOWER[result.table.text] ?? TSTL_KEYWORD_FIXUPS[result.table.text];
|
|
530
|
-
if (replacement) {
|
|
531
|
-
result.table.text = replacement;
|
|
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";
|
|
5
|
+
import { CALL_TRANSFORMS } from "./transforms.js";
|
|
6
|
+
import { createOptimizeTransforms, countFilterCalls, ALL_OPTIMIZE } from "./optimize.js";
|
|
7
|
+
function createPlugin(options = {}) {
|
|
8
|
+
const opt = options.optimize === true ? ALL_OPTIMIZE : options.optimize || {};
|
|
9
|
+
const filterSkipFiles = new Set();
|
|
10
|
+
const optTransforms = opt.filter ? createOptimizeTransforms(filterSkipFiles) : [];
|
|
11
|
+
const transforms = [...CALL_TRANSFORMS, ...optTransforms];
|
|
12
|
+
const plugin = {
|
|
13
|
+
visitors: {
|
|
14
|
+
[ts.SyntaxKind.PropertyAccessExpression]: (node, context) => {
|
|
15
|
+
const result = context.superTransformExpression(node);
|
|
16
|
+
// Rewrite identifiers in the Lua AST (PascalCase -> lowercase, TSTL keyword fixups).
|
|
17
|
+
if (tstl.isTableIndexExpression(result) && tstl.isIdentifier(result.table)) {
|
|
18
|
+
const replacement = PASCAL_TO_LOWER[result.table.text] ?? TSTL_KEYWORD_FIXUPS[result.table.text];
|
|
19
|
+
if (replacement) {
|
|
20
|
+
result.table.text = replacement;
|
|
21
|
+
}
|
|
532
22
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
[ts.SyntaxKind.ExpressionStatement]: (node, context) => {
|
|
581
|
-
if (ts.isBinaryExpression(node.expression)) {
|
|
582
|
-
const compoundFn = COMPOUND_BITWISE_OPS[node.expression.operatorToken.kind];
|
|
23
|
+
// DetectedEvent.index is 1-based at runtime in SLua; emit `obj.index - 1`
|
|
24
|
+
// so TypeScript sees a 0-based value. This composes correctly with
|
|
25
|
+
// @indexArg functions (which add +1, cancelling out the -1).
|
|
26
|
+
if (isDetectedEventIndex(node, context.checker)) {
|
|
27
|
+
return tstl.createBinaryExpression(result, tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
},
|
|
31
|
+
[ts.SyntaxKind.BinaryExpression]: (node, context) => {
|
|
32
|
+
// Check for btest pattern: (a & b) !== 0, 0 === (a & b), etc.
|
|
33
|
+
const btest = extractBtestPattern(node);
|
|
34
|
+
if (btest) {
|
|
35
|
+
const left = context.transformExpression(btest.band.left);
|
|
36
|
+
const right = context.transformExpression(btest.band.right);
|
|
37
|
+
const call = createBit32Call("btest", [left, right], node);
|
|
38
|
+
return btest.negate
|
|
39
|
+
? tstl.createUnaryExpression(call, tstl.SyntaxKind.NotOperator, node)
|
|
40
|
+
: call;
|
|
41
|
+
}
|
|
42
|
+
// indexOf presence check: s.indexOf(x) >= 0 -> string.find(s, x, 1, true)
|
|
43
|
+
if (opt.indexOf) {
|
|
44
|
+
const presence = extractIndexOfPresence(node, context.checker);
|
|
45
|
+
if (presence) {
|
|
46
|
+
const callExpr = presence.call.expression;
|
|
47
|
+
const receiver = context.transformExpression(callExpr.expression);
|
|
48
|
+
const arg = context.transformExpression(presence.call.arguments[0]);
|
|
49
|
+
const findCall = presence.isString
|
|
50
|
+
? createStringFindCall(receiver, arg, node)
|
|
51
|
+
: createNamespacedCall("table", "find", [receiver, arg], node);
|
|
52
|
+
return presence.negate
|
|
53
|
+
? tstl.createUnaryExpression(findCall, tstl.SyntaxKind.NotOperator, node)
|
|
54
|
+
: findCall;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const op = node.operatorToken.kind;
|
|
58
|
+
const fn = BINARY_BITWISE_OPS[op];
|
|
59
|
+
if (fn) {
|
|
60
|
+
const left = context.transformExpression(node.left);
|
|
61
|
+
const right = context.transformExpression(node.right);
|
|
62
|
+
return createBit32Call(fn, [left, right], node);
|
|
63
|
+
}
|
|
64
|
+
// Compound bitwise assignments (`&=`, `|=`, `^=`, `<<=`, `>>=`, `>>>=`).
|
|
65
|
+
// Manually desugar to `lhs = bit32.<fn>(lhs, rhs)` so we preserve the
|
|
66
|
+
// correct function (especially arshift vs rshift). This path is only
|
|
67
|
+
// reached when a compound assignment is used as an *expression*; the
|
|
68
|
+
// statement case is handled by the ExpressionStatement visitor below.
|
|
69
|
+
const compoundFn = COMPOUND_BITWISE_OPS[op];
|
|
583
70
|
if (compoundFn) {
|
|
584
|
-
const left = context.transformExpression(node.
|
|
585
|
-
const right = context.transformExpression(node.
|
|
71
|
+
const left = context.transformExpression(node.left);
|
|
72
|
+
const right = context.transformExpression(node.right);
|
|
586
73
|
const call = createBit32Call(compoundFn, [left, right], node);
|
|
587
|
-
|
|
74
|
+
context.addPrecedingStatements(tstl.createAssignmentStatement(left, call, node));
|
|
75
|
+
return left;
|
|
588
76
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
77
|
+
return context.superTransformExpression(node);
|
|
78
|
+
},
|
|
79
|
+
// Compound bitwise assignments used as statements (`a &= 3;`) are handled
|
|
80
|
+
// by TSTL's ExpressionStatement -> transformBinaryExpressionStatement path,
|
|
81
|
+
// which never calls the BinaryExpression visitor. We intercept here and
|
|
82
|
+
// manually desugar to `lhs = bit32.<fn>(lhs, rhs)` to preserve the correct
|
|
83
|
+
// function name (especially arshift vs rshift which TSTL conflates).
|
|
84
|
+
[ts.SyntaxKind.ExpressionStatement]: (node, context) => {
|
|
85
|
+
if (ts.isBinaryExpression(node.expression)) {
|
|
86
|
+
const compoundFn = COMPOUND_BITWISE_OPS[node.expression.operatorToken.kind];
|
|
87
|
+
if (compoundFn) {
|
|
88
|
+
const left = context.transformExpression(node.expression.left);
|
|
89
|
+
const right = context.transformExpression(node.expression.right);
|
|
90
|
+
const call = createBit32Call(compoundFn, [left, right], node);
|
|
91
|
+
return [tstl.createAssignmentStatement(left, call, node)];
|
|
92
|
+
}
|
|
93
|
+
// Self-reassignment concat/spread -> table.extend (in-place, no assignment needed)
|
|
94
|
+
const concatMatch = extractConcatSelfAssignment(node.expression, context.checker) ??
|
|
95
|
+
extractSpreadSelfAssignment(node.expression, context.checker);
|
|
96
|
+
if (concatMatch) {
|
|
97
|
+
const target = context.transformExpression(concatMatch.name);
|
|
98
|
+
const args = concatMatch.args.map((a) => context.transformExpression(a));
|
|
99
|
+
const call = emitChainedExtend(target, args, node);
|
|
100
|
+
return [tstl.createExpressionStatement(call, node)];
|
|
101
|
+
}
|
|
597
102
|
}
|
|
103
|
+
return context.superTransformStatements(node);
|
|
104
|
+
},
|
|
105
|
+
[ts.SyntaxKind.CallExpression]: (node, context) => {
|
|
106
|
+
// Catalog-driven transforms
|
|
107
|
+
for (const transform of transforms) {
|
|
108
|
+
if (transform.match(node, context.checker)) {
|
|
109
|
+
return transform.emit(node, context);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ll.* index-semantics: automatic 0->1 index adjustment
|
|
113
|
+
const indexSemantics = getLLIndexSemantics(node, context.checker);
|
|
114
|
+
if (indexSemantics) {
|
|
115
|
+
return emitLLIndexCall(node, context, indexSemantics);
|
|
116
|
+
}
|
|
117
|
+
// `Math.floor(a / b)` -> `a // b` (native Luau floor division operator)
|
|
118
|
+
if (isMathFloor(node)) {
|
|
119
|
+
const arg = node.arguments[0];
|
|
120
|
+
if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.SlashToken) {
|
|
121
|
+
const left = context.transformExpression(arg.left);
|
|
122
|
+
const right = context.transformExpression(arg.right);
|
|
123
|
+
return tstl.createBinaryExpression(left, right, tstl.SyntaxKind.FloorDivisionOperator, node);
|
|
124
|
+
}
|
|
125
|
+
// `Math.floor((a / b) * c)` -> `a * c // b`
|
|
126
|
+
// Reorders multiplication around a division to use floor division.
|
|
127
|
+
// Mathematically equivalent for reals; may differ by ±1 at exact
|
|
128
|
+
// integer boundaries due to floating-point rounding reorder.
|
|
129
|
+
if (opt.floorMultiply &&
|
|
130
|
+
ts.isBinaryExpression(arg) &&
|
|
131
|
+
arg.operatorToken.kind === ts.SyntaxKind.AsteriskToken) {
|
|
132
|
+
const leftUn = ts.isParenthesizedExpression(arg.left) ? arg.left.expression : arg.left;
|
|
133
|
+
const rightUn = ts.isParenthesizedExpression(arg.right)
|
|
134
|
+
? arg.right.expression
|
|
135
|
+
: arg.right;
|
|
136
|
+
let div = null;
|
|
137
|
+
let mult = null;
|
|
138
|
+
if (ts.isBinaryExpression(leftUn) &&
|
|
139
|
+
leftUn.operatorToken.kind === ts.SyntaxKind.SlashToken) {
|
|
140
|
+
div = leftUn;
|
|
141
|
+
mult = arg.right;
|
|
142
|
+
}
|
|
143
|
+
else if (ts.isBinaryExpression(rightUn) &&
|
|
144
|
+
rightUn.operatorToken.kind === ts.SyntaxKind.SlashToken) {
|
|
145
|
+
div = rightUn;
|
|
146
|
+
mult = arg.left;
|
|
147
|
+
}
|
|
148
|
+
if (div && mult) {
|
|
149
|
+
const dividend = context.transformExpression(div.left);
|
|
150
|
+
const divisor = context.transformExpression(div.right);
|
|
151
|
+
const multiplier = context.transformExpression(mult);
|
|
152
|
+
// a * c // b (same precedence, left-associative -> (a * c) // b)
|
|
153
|
+
return tstl.createBinaryExpression(tstl.createBinaryExpression(dividend, multiplier, tstl.SyntaxKind.MultiplicationOperator, node), divisor, tstl.SyntaxKind.FloorDivisionOperator, node);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return context.superTransformExpression(node);
|
|
158
|
+
},
|
|
159
|
+
[ts.SyntaxKind.PrefixUnaryExpression]: (node, context) => {
|
|
160
|
+
if (node.operator === ts.SyntaxKind.TildeToken) {
|
|
161
|
+
const operand = context.transformExpression(node.operand);
|
|
162
|
+
return createBit32Call("bnot", [operand], node);
|
|
163
|
+
}
|
|
164
|
+
return context.superTransformExpression(node);
|
|
165
|
+
},
|
|
166
|
+
[ts.SyntaxKind.TemplateExpression]: (node, context) => {
|
|
167
|
+
if (!opt.numericConcat) {
|
|
168
|
+
return context.superTransformExpression(node);
|
|
169
|
+
}
|
|
170
|
+
const parts = [];
|
|
171
|
+
const head = node.head.text;
|
|
172
|
+
if (head.length > 0) {
|
|
173
|
+
parts.push(tstl.createStringLiteral(head, node.head));
|
|
174
|
+
}
|
|
175
|
+
for (const span of node.templateSpans) {
|
|
176
|
+
const expr = context.transformExpression(span.expression);
|
|
177
|
+
if (isStringOrNumberLike(context.checker, span.expression)) {
|
|
178
|
+
parts.push(expr);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
parts.push(tstl.createCallExpression(tstl.createIdentifier("tostring"), [expr], span.expression));
|
|
182
|
+
}
|
|
183
|
+
const text = span.literal.text;
|
|
184
|
+
if (text.length > 0) {
|
|
185
|
+
parts.push(tstl.createStringLiteral(text, span.literal));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return parts.reduce((prev, current) => tstl.createBinaryExpression(prev, current, tstl.SyntaxKind.ConcatOperator));
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
beforeTransform(program, compilerOptions) {
|
|
192
|
+
const diagnostics = [];
|
|
193
|
+
if (compilerOptions.luaTarget !== tstl.LuaTarget.Luau) {
|
|
194
|
+
diagnostics.push({
|
|
195
|
+
file: undefined,
|
|
196
|
+
start: undefined,
|
|
197
|
+
length: undefined,
|
|
198
|
+
messageText: '@gwigz/slua-tstl-plugin requires luaTarget to be "Luau", set "luaTarget": "Luau" in tsconfig.json',
|
|
199
|
+
category: ts.DiagnosticCategory.Error,
|
|
200
|
+
code: 90000,
|
|
201
|
+
source: "@gwigz/slua-tstl-plugin",
|
|
202
|
+
});
|
|
598
203
|
}
|
|
599
|
-
|
|
204
|
+
// Pre-scan: skip filter inlining for files with multiple .filter() calls
|
|
205
|
+
// (the shared __TS__ArrayFilter helper is smaller than 2+ inlined loops).
|
|
206
|
+
// In luaBundle mode, all files end up in one output so we count globally.
|
|
207
|
+
if (opt.filter) {
|
|
208
|
+
const bundle = !!options.luaBundle;
|
|
209
|
+
const skip = countFilterCalls(program, bundle);
|
|
210
|
+
filterSkipFiles.clear();
|
|
211
|
+
for (const f of skip) {
|
|
212
|
+
filterSkipFiles.add(f);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return diagnostics;
|
|
600
216
|
},
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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.
|
|
229
|
+
for (const file of result) {
|
|
230
|
+
if (!file.code.includes("local ____exports = {}\n"))
|
|
231
|
+
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");
|
|
606
242
|
}
|
|
607
243
|
}
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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");
|
|
294
|
+
}
|
|
612
295
|
}
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
const
|
|
619
|
-
|
|
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");
|
|
620
350
|
}
|
|
621
351
|
}
|
|
622
|
-
return context.superTransformExpression(node);
|
|
623
352
|
},
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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);
|
|
363
|
+
}
|
|
628
364
|
}
|
|
629
|
-
return context.superTransformExpression(node);
|
|
630
365
|
},
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
diagnostics.push({
|
|
636
|
-
file: undefined,
|
|
637
|
-
start: undefined,
|
|
638
|
-
length: undefined,
|
|
639
|
-
messageText: '@gwigz/slua-tstl-plugin requires luaTarget to be "Luau", set "luaTarget": "Luau" in tsconfig.json',
|
|
640
|
-
category: ts.DiagnosticCategory.Error,
|
|
641
|
-
code: 90000,
|
|
642
|
-
source: "@gwigz/slua-tstl-plugin",
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
return diagnostics;
|
|
646
|
-
},
|
|
647
|
-
beforeEmit(program, _options, _emitHost, result) {
|
|
648
|
-
// Strip internal @indexArg / @indexReturn JSDoc tags from Lua comments.
|
|
649
|
-
// These are only consumed by the plugin at transpile time; they should
|
|
650
|
-
// not leak into the output as Lua comments.
|
|
651
|
-
for (const file of result) {
|
|
652
|
-
file.code = file.code
|
|
653
|
-
.replace(/^--\s*@index(?:Arg|Return)\b.*\n/gm, "")
|
|
654
|
-
.replace(/^(---.*)\n(?:-- *\n)+(?=local |ll\.)/gm, "$1\n");
|
|
655
|
-
}
|
|
656
|
-
// Strip empty module boilerplate from files without explicit exports.
|
|
657
|
-
// `moduleDetection: "force"` causes TSTL to wrap every file as a module;
|
|
658
|
-
// standalone SLua scripts don't need the ____exports wrapper.
|
|
659
|
-
for (const file of result) {
|
|
660
|
-
if (!file.code.includes("local ____exports = {}\n"))
|
|
661
|
-
continue;
|
|
662
|
-
if (!file.code.trimEnd().endsWith("return ____exports"))
|
|
663
|
-
continue;
|
|
664
|
-
const hasExplicitExports = file.sourceFiles?.some((sf) => sf.statements.some((s) => ts.isExportDeclaration(s) ||
|
|
665
|
-
ts.isExportAssignment(s) ||
|
|
666
|
-
(ts.canHaveModifiers(s) &&
|
|
667
|
-
ts.getModifiers(s)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))));
|
|
668
|
-
if (!hasExplicitExports) {
|
|
669
|
-
file.code = file.code
|
|
670
|
-
.replace(/local ____exports = \{\}\n/, "")
|
|
671
|
-
.replace(/\nreturn ____exports\n?$/, "\n");
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
},
|
|
675
|
-
};
|
|
676
|
-
export default plugin;
|
|
366
|
+
};
|
|
367
|
+
return plugin;
|
|
368
|
+
}
|
|
369
|
+
export default createPlugin;
|