@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/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
- * PascalCase class names that map to lowercase Lua globals.
5
- * `new Vector(...)` is handled by @customConstructor, but static access
6
- * like `Vector.zero` emits `Vector.zero` in Lua -- which doesn't exist.
7
- * The PropertyAccessExpression visitor rewrites the Lua identifier to lowercase.
8
- */
9
- const PASCAL_TO_LOWER = {
10
- Vector: "vector",
11
- Quaternion: "quaternion",
12
- UUID: "uuid",
13
- };
14
- /**
15
- * TSTL treats "bit32" as a Lua keyword and renames it to "____bit32" in output.
16
- * This is incorrect for Luau where bit32 is a valid global library.
17
- * The visitor rewrites the mangled name back; the diagnostic is suppressed
18
- * separately in consumers (e.g. the playground transpiler worker).
19
- */
20
- const TSTL_KEYWORD_FIXUPS = {
21
- ____bit32: "bit32",
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, } 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
- // DetectedEvent.index is 1-based at runtime in SLua; emit `obj.index - 1`
535
- // so TypeScript sees a 0-based value. This composes correctly with
536
- // @indexArg functions (which add +1, cancelling out the -1).
537
- if (isDetectedEventIndex(node, context.checker)) {
538
- return tstl.createBinaryExpression(result, tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
539
- }
540
- return result;
541
- },
542
- [ts.SyntaxKind.BinaryExpression]: (node, context) => {
543
- // Check for btest pattern: (a & b) !== 0, 0 === (a & b), etc.
544
- const btest = extractBtestPattern(node);
545
- if (btest) {
546
- const left = context.transformExpression(btest.band.left);
547
- const right = context.transformExpression(btest.band.right);
548
- const call = createBit32Call("btest", [left, right], node);
549
- return btest.negate
550
- ? tstl.createUnaryExpression(call, tstl.SyntaxKind.NotOperator, node)
551
- : call;
552
- }
553
- const op = node.operatorToken.kind;
554
- const fn = BINARY_BITWISE_OPS[op];
555
- if (fn) {
556
- const left = context.transformExpression(node.left);
557
- const right = context.transformExpression(node.right);
558
- return createBit32Call(fn, [left, right], node);
559
- }
560
- // Compound bitwise assignments (`&=`, `|=`, `^=`, `<<=`, `>>=`, `>>>=`).
561
- // Manually desugar to `lhs = bit32.<fn>(lhs, rhs)` so we preserve the
562
- // correct function (especially arshift vs rshift). This path is only
563
- // reached when a compound assignment is used as an *expression*; the
564
- // statement case is handled by the ExpressionStatement visitor below.
565
- const compoundFn = COMPOUND_BITWISE_OPS[op];
566
- if (compoundFn) {
567
- const left = context.transformExpression(node.left);
568
- const right = context.transformExpression(node.right);
569
- const call = createBit32Call(compoundFn, [left, right], node);
570
- context.addPrecedingStatements(tstl.createAssignmentStatement(left, call, node));
571
- return left;
572
- }
573
- return context.superTransformExpression(node);
574
- },
575
- // Compound bitwise assignments used as statements (`a &= 3;`) are handled
576
- // by TSTL's ExpressionStatement -> transformBinaryExpressionStatement path,
577
- // which never calls the BinaryExpression visitor. We intercept here and
578
- // manually desugar to `lhs = bit32.<fn>(lhs, rhs)` to preserve the correct
579
- // function name (especially arshift vs rshift which TSTL conflates).
580
- [ts.SyntaxKind.ExpressionStatement]: (node, context) => {
581
- if (ts.isBinaryExpression(node.expression)) {
582
- const compoundFn = COMPOUND_BITWISE_OPS[node.expression.operatorToken.kind];
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.expression.left);
585
- const right = context.transformExpression(node.expression.right);
146
+ const left = context.transformExpression(node.left);
147
+ const right = context.transformExpression(node.right);
586
148
  const call = createBit32Call(compoundFn, [left, right], node);
587
- return [tstl.createAssignmentStatement(left, call, node)];
149
+ context.addPrecedingStatements(tstl.createAssignmentStatement(left, call, node));
150
+ return left;
588
151
  }
589
- // Self-reassignment concat/spread -> table.extend (in-place, no assignment needed)
590
- const concatMatch = extractConcatSelfAssignment(node.expression, context.checker) ??
591
- extractSpreadSelfAssignment(node.expression, context.checker);
592
- if (concatMatch) {
593
- const target = context.transformExpression(concatMatch.name);
594
- const args = concatMatch.args.map((a) => context.transformExpression(a));
595
- const call = emitChainedExtend(target, args, node);
596
- return [tstl.createExpressionStatement(call, node)];
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
- return context.superTransformStatements(node);
600
- },
601
- [ts.SyntaxKind.CallExpression]: (node, context) => {
602
- // Catalog-driven transforms
603
- for (const transform of CALL_TRANSFORMS) {
604
- if (transform.match(node, context.checker)) {
605
- return transform.emit(node, context);
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
- // ll.* index-semantics: automatic 0->1 index adjustment
609
- const indexSemantics = getLLIndexSemantics(node, context.checker);
610
- if (indexSemantics) {
611
- return emitLLIndexCall(node, context, indexSemantics);
612
- }
613
- // `Math.floor(a / b)` -> `a // b` (native Luau floor division operator)
614
- if (isMathFloor(node)) {
615
- const arg = node.arguments[0];
616
- if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.SlashToken) {
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 context.superTransformExpression(node);
319
+ return diagnostics;
623
320
  },
624
- [ts.SyntaxKind.PrefixUnaryExpression]: (node, context) => {
625
- if (node.operator === ts.SyntaxKind.TildeToken) {
626
- const operand = context.transformExpression(node.operand);
627
- return createBit32Call("bnot", [operand], node);
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
- beforeTransform(_program, options) {
633
- const diagnostics = [];
634
- if (options.luaTarget !== tstl.LuaTarget.Luau) {
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;