@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.
- package/README.md +234 -16
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +50 -0
- package/dist/define.d.ts +19 -0
- package/dist/define.js +83 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +341 -662
- package/dist/lua-ast-walk.d.ts +10 -0
- package/dist/lua-ast-walk.js +422 -0
- package/dist/lua-transforms.d.ts +41 -0
- package/dist/lua-transforms.js +302 -0
- package/dist/optimize.d.ts +31 -0
- package/dist/optimize.js +106 -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,355 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import * as tstl from "typescript-to-lua";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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, } from "./utils.js";
|
|
5
|
+
import { CALL_TRANSFORMS } from "./transforms.js";
|
|
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";
|
|
9
|
+
function createPlugin(options = {}) {
|
|
10
|
+
const opt = options.optimize === true ? ALL_OPTIMIZE : options.optimize || {};
|
|
11
|
+
const filterSkipFiles = new Set();
|
|
12
|
+
const optTransforms = opt.filter ? createOptimizeTransforms(filterSkipFiles) : [];
|
|
13
|
+
const transforms = [...CALL_TRANSFORMS, ...optTransforms];
|
|
14
|
+
const defineMap = new Map(options.define ? Object.entries(options.define) : []);
|
|
15
|
+
const plugin = {
|
|
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);
|
|
532
52
|
}
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
context.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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,
|
|
89
|
+
[ts.SyntaxKind.PropertyAccessExpression]: (node, context) => {
|
|
90
|
+
const result = context.superTransformExpression(node);
|
|
91
|
+
// Rewrite identifiers in the Lua AST (PascalCase -> lowercase, TSTL keyword fixups).
|
|
92
|
+
if (tstl.isTableIndexExpression(result) && tstl.isIdentifier(result.table)) {
|
|
93
|
+
const replacement = PASCAL_TO_LOWER[result.table.text] ?? TSTL_KEYWORD_FIXUPS[result.table.text];
|
|
94
|
+
if (replacement) {
|
|
95
|
+
result.table.text = replacement;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// DetectedEvent.index is 1-based at runtime in SLua; emit `obj.index - 1`
|
|
99
|
+
// so TypeScript sees a 0-based value. This composes correctly with
|
|
100
|
+
// @indexArg functions (which add +1, cancelling out the -1).
|
|
101
|
+
if (isDetectedEventIndex(node, context.checker)) {
|
|
102
|
+
return tstl.createBinaryExpression(result, tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
[ts.SyntaxKind.BinaryExpression]: (node, context) => {
|
|
107
|
+
// Check for btest pattern: (a & b) !== 0, 0 === (a & b), etc.
|
|
108
|
+
const btest = extractBtestPattern(node);
|
|
109
|
+
if (btest) {
|
|
110
|
+
const left = context.transformExpression(btest.band.left);
|
|
111
|
+
const right = context.transformExpression(btest.band.right);
|
|
112
|
+
const call = createBit32Call("btest", [left, right], node);
|
|
113
|
+
return btest.negate
|
|
114
|
+
? tstl.createUnaryExpression(call, tstl.SyntaxKind.NotOperator, node)
|
|
115
|
+
: call;
|
|
116
|
+
}
|
|
117
|
+
// indexOf presence check: s.indexOf(x) >= 0 -> string.find(s, x, 1, true)
|
|
118
|
+
if (opt.indexOf) {
|
|
119
|
+
const presence = extractIndexOfPresence(node, context.checker);
|
|
120
|
+
if (presence) {
|
|
121
|
+
const callExpr = presence.call.expression;
|
|
122
|
+
const receiver = context.transformExpression(callExpr.expression);
|
|
123
|
+
const arg = context.transformExpression(presence.call.arguments[0]);
|
|
124
|
+
const findCall = presence.isString
|
|
125
|
+
? createStringFindCall(receiver, arg, node)
|
|
126
|
+
: createNamespacedCall("table", "find", [receiver, arg], node);
|
|
127
|
+
return presence.negate
|
|
128
|
+
? tstl.createUnaryExpression(findCall, tstl.SyntaxKind.NotOperator, node)
|
|
129
|
+
: findCall;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const op = node.operatorToken.kind;
|
|
133
|
+
const fn = BINARY_BITWISE_OPS[op];
|
|
134
|
+
if (fn) {
|
|
135
|
+
const left = context.transformExpression(node.left);
|
|
136
|
+
const right = context.transformExpression(node.right);
|
|
137
|
+
return createBit32Call(fn, [left, right], node);
|
|
138
|
+
}
|
|
139
|
+
// Compound bitwise assignments (`&=`, `|=`, `^=`, `<<=`, `>>=`, `>>>=`).
|
|
140
|
+
// Manually desugar to `lhs = bit32.<fn>(lhs, rhs)` so we preserve the
|
|
141
|
+
// correct function (especially arshift vs rshift). This path is only
|
|
142
|
+
// reached when a compound assignment is used as an *expression*; the
|
|
143
|
+
// statement case is handled by the ExpressionStatement visitor below.
|
|
144
|
+
const compoundFn = COMPOUND_BITWISE_OPS[op];
|
|
583
145
|
if (compoundFn) {
|
|
584
|
-
const left = context.transformExpression(node.
|
|
585
|
-
const right = context.transformExpression(node.
|
|
146
|
+
const left = context.transformExpression(node.left);
|
|
147
|
+
const right = context.transformExpression(node.right);
|
|
586
148
|
const call = createBit32Call(compoundFn, [left, right], node);
|
|
587
|
-
|
|
149
|
+
context.addPrecedingStatements(tstl.createAssignmentStatement(left, call, node));
|
|
150
|
+
return left;
|
|
588
151
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
152
|
+
return context.superTransformExpression(node);
|
|
153
|
+
},
|
|
154
|
+
// Compound bitwise assignments used as statements (`a &= 3;`) are handled
|
|
155
|
+
// by TSTL's ExpressionStatement -> transformBinaryExpressionStatement path,
|
|
156
|
+
// which never calls the BinaryExpression visitor. We intercept here and
|
|
157
|
+
// manually desugar to `lhs = bit32.<fn>(lhs, rhs)` to preserve the correct
|
|
158
|
+
// function name (especially arshift vs rshift which TSTL conflates).
|
|
159
|
+
[ts.SyntaxKind.ExpressionStatement]: (node, context) => {
|
|
160
|
+
if (ts.isBinaryExpression(node.expression)) {
|
|
161
|
+
const compoundFn = COMPOUND_BITWISE_OPS[node.expression.operatorToken.kind];
|
|
162
|
+
if (compoundFn) {
|
|
163
|
+
const left = context.transformExpression(node.expression.left);
|
|
164
|
+
const right = context.transformExpression(node.expression.right);
|
|
165
|
+
const call = createBit32Call(compoundFn, [left, right], node);
|
|
166
|
+
return [tstl.createAssignmentStatement(left, call, node)];
|
|
167
|
+
}
|
|
168
|
+
// Self-reassignment concat/spread -> table.extend (in-place, no assignment needed)
|
|
169
|
+
const concatMatch = extractConcatSelfAssignment(node.expression, context.checker) ??
|
|
170
|
+
extractSpreadSelfAssignment(node.expression, context.checker);
|
|
171
|
+
if (concatMatch) {
|
|
172
|
+
const target = context.transformExpression(concatMatch.name);
|
|
173
|
+
const args = concatMatch.args.map((a) => context.transformExpression(a));
|
|
174
|
+
const call = emitChainedExtend(target, args, node);
|
|
175
|
+
return [tstl.createExpressionStatement(call, node)];
|
|
176
|
+
}
|
|
597
177
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
178
|
+
return context.superTransformStatements(node);
|
|
179
|
+
},
|
|
180
|
+
[ts.SyntaxKind.CallExpression]: (node, context) => {
|
|
181
|
+
// Catalog-driven transforms
|
|
182
|
+
for (const transform of transforms) {
|
|
183
|
+
if (transform.match(node, context.checker)) {
|
|
184
|
+
return transform.emit(node, context);
|
|
185
|
+
}
|
|
606
186
|
}
|
|
187
|
+
// ll.* index-semantics: automatic 0->1 index adjustment
|
|
188
|
+
const indexSemantics = getLLIndexSemantics(node, context.checker);
|
|
189
|
+
if (indexSemantics) {
|
|
190
|
+
return emitLLIndexCall(node, context, indexSemantics);
|
|
191
|
+
}
|
|
192
|
+
// `Math.floor(a / b)` -> `a // b` (native Luau floor division operator)
|
|
193
|
+
if (isMathFloor(node)) {
|
|
194
|
+
const arg = node.arguments[0];
|
|
195
|
+
if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.SlashToken) {
|
|
196
|
+
const left = context.transformExpression(arg.left);
|
|
197
|
+
const right = context.transformExpression(arg.right);
|
|
198
|
+
return tstl.createBinaryExpression(left, right, tstl.SyntaxKind.FloorDivisionOperator, node);
|
|
199
|
+
}
|
|
200
|
+
// `Math.floor((a / b) * c)` -> `a * c // b`
|
|
201
|
+
// Reorders multiplication around a division to use floor division.
|
|
202
|
+
// Mathematically equivalent for reals; may differ by ±1 at exact
|
|
203
|
+
// integer boundaries due to floating-point rounding reorder.
|
|
204
|
+
if (opt.floorMultiply &&
|
|
205
|
+
ts.isBinaryExpression(arg) &&
|
|
206
|
+
arg.operatorToken.kind === ts.SyntaxKind.AsteriskToken) {
|
|
207
|
+
const leftUn = ts.isParenthesizedExpression(arg.left) ? arg.left.expression : arg.left;
|
|
208
|
+
const rightUn = ts.isParenthesizedExpression(arg.right)
|
|
209
|
+
? arg.right.expression
|
|
210
|
+
: arg.right;
|
|
211
|
+
let div = null;
|
|
212
|
+
let mult = null;
|
|
213
|
+
if (ts.isBinaryExpression(leftUn) &&
|
|
214
|
+
leftUn.operatorToken.kind === ts.SyntaxKind.SlashToken) {
|
|
215
|
+
div = leftUn;
|
|
216
|
+
mult = arg.right;
|
|
217
|
+
}
|
|
218
|
+
else if (ts.isBinaryExpression(rightUn) &&
|
|
219
|
+
rightUn.operatorToken.kind === ts.SyntaxKind.SlashToken) {
|
|
220
|
+
div = rightUn;
|
|
221
|
+
mult = arg.left;
|
|
222
|
+
}
|
|
223
|
+
if (div && mult) {
|
|
224
|
+
const dividend = context.transformExpression(div.left);
|
|
225
|
+
const divisor = context.transformExpression(div.right);
|
|
226
|
+
const multiplier = context.transformExpression(mult);
|
|
227
|
+
// a * c // b (same precedence, left-associative -> (a * c) // b)
|
|
228
|
+
return tstl.createBinaryExpression(tstl.createBinaryExpression(dividend, multiplier, tstl.SyntaxKind.MultiplicationOperator, node), divisor, tstl.SyntaxKind.FloorDivisionOperator, node);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return context.superTransformExpression(node);
|
|
233
|
+
},
|
|
234
|
+
[ts.SyntaxKind.PrefixUnaryExpression]: (node, context) => {
|
|
235
|
+
if (node.operator === ts.SyntaxKind.TildeToken) {
|
|
236
|
+
const operand = context.transformExpression(node.operand);
|
|
237
|
+
return createBit32Call("bnot", [operand], node);
|
|
238
|
+
}
|
|
239
|
+
return context.superTransformExpression(node);
|
|
240
|
+
},
|
|
241
|
+
[ts.SyntaxKind.TemplateExpression]: (node, context) => {
|
|
242
|
+
if (!opt.numericConcat) {
|
|
243
|
+
return context.superTransformExpression(node);
|
|
244
|
+
}
|
|
245
|
+
const parts = [];
|
|
246
|
+
const head = node.head.text;
|
|
247
|
+
if (head.length > 0) {
|
|
248
|
+
parts.push(tstl.createStringLiteral(head, node.head));
|
|
249
|
+
}
|
|
250
|
+
for (const span of node.templateSpans) {
|
|
251
|
+
const expr = context.transformExpression(span.expression);
|
|
252
|
+
if (isStringOrNumberLike(context.checker, span.expression)) {
|
|
253
|
+
parts.push(expr);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
parts.push(tstl.createCallExpression(tstl.createIdentifier("tostring"), [expr], span.expression));
|
|
257
|
+
}
|
|
258
|
+
const text = span.literal.text;
|
|
259
|
+
if (text.length > 0) {
|
|
260
|
+
parts.push(tstl.createStringLiteral(text, span.literal));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return parts.reduce((prev, current) => tstl.createBinaryExpression(prev, current, tstl.SyntaxKind.ConcatOperator));
|
|
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
|
+
},
|
|
294
|
+
},
|
|
295
|
+
beforeTransform(program, compilerOptions) {
|
|
296
|
+
const diagnostics = [];
|
|
297
|
+
if (compilerOptions.luaTarget !== tstl.LuaTarget.Luau) {
|
|
298
|
+
diagnostics.push({
|
|
299
|
+
file: undefined,
|
|
300
|
+
start: undefined,
|
|
301
|
+
length: undefined,
|
|
302
|
+
messageText: '@gwigz/slua-tstl-plugin requires luaTarget to be "Luau", set "luaTarget": "Luau" in tsconfig.json',
|
|
303
|
+
category: ts.DiagnosticCategory.Error,
|
|
304
|
+
code: 90000,
|
|
305
|
+
source: "@gwigz/slua-tstl-plugin",
|
|
306
|
+
});
|
|
607
307
|
}
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
const left = context.transformExpression(arg.left);
|
|
618
|
-
const right = context.transformExpression(arg.right);
|
|
619
|
-
return tstl.createBinaryExpression(left, right, tstl.SyntaxKind.FloorDivisionOperator, node);
|
|
308
|
+
// Pre-scan: skip filter inlining for files with multiple .filter() calls
|
|
309
|
+
// (the shared __TS__ArrayFilter helper is smaller than 2+ inlined loops).
|
|
310
|
+
// In luaBundle mode, all files end up in one output so we count globally.
|
|
311
|
+
if (opt.filter) {
|
|
312
|
+
const bundle = !!options.luaBundle;
|
|
313
|
+
const skip = countFilterCalls(program, bundle);
|
|
314
|
+
filterSkipFiles.clear();
|
|
315
|
+
for (const f of skip) {
|
|
316
|
+
filterSkipFiles.add(f);
|
|
620
317
|
}
|
|
621
318
|
}
|
|
622
|
-
return
|
|
319
|
+
return diagnostics;
|
|
623
320
|
},
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
321
|
+
afterPrint(program, _options, emitHost, result) {
|
|
322
|
+
for (const file of result) {
|
|
323
|
+
if (!file.luaAst)
|
|
324
|
+
continue;
|
|
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;
|
|
335
|
+
}
|
|
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;
|
|
345
|
+
}
|
|
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");
|
|
349
|
+
}
|
|
628
350
|
}
|
|
629
|
-
return context.superTransformExpression(node);
|
|
630
351
|
},
|
|
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;
|
|
352
|
+
};
|
|
353
|
+
return plugin;
|
|
354
|
+
}
|
|
355
|
+
export default createPlugin;
|