@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/utils.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as tstl from "typescript-to-lua";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a `bit32.<fn>(...args)` Lua call expression.
|
|
5
|
+
* The optional `node` attaches TypeScript source-map information; when
|
|
6
|
+
* patching already-lowered Lua AST nodes (e.g. from compound-assignment
|
|
7
|
+
* desugaring) there is no originating TS node, so it may be omitted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createBit32Call(fn: string, args: tstl.Expression[], node?: ts.Node): tstl.CallExpression;
|
|
10
|
+
/**
|
|
11
|
+
* Returns true when `node` is `Math.floor(<single-arg>)`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isMathFloor(node: ts.CallExpression): node is ts.CallExpression & {
|
|
14
|
+
arguments: [ts.Expression];
|
|
15
|
+
};
|
|
16
|
+
export declare function isZeroLiteral(node: ts.Expression): boolean;
|
|
17
|
+
export declare function isNegatedEquality(op: ts.SyntaxKind): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Detect `(a & b) !== 0`, `0 === (a & b)`, etc. and return the `&` expression
|
|
20
|
+
* plus whether to negate the btest result.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractBtestPattern(node: ts.BinaryExpression): {
|
|
23
|
+
band: ts.BinaryExpression;
|
|
24
|
+
negate: boolean;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* Detects `-1` as either a PrefixUnaryExpression (minus + 1) or a numeric literal with text "-1".
|
|
28
|
+
*/
|
|
29
|
+
export declare function isMinusOneLiteral(node: ts.Expression): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Detect `s.indexOf(x) >= 0`, `s.indexOf(x) !== -1`, etc. and return
|
|
32
|
+
* the indexOf call plus whether the check means "not found" (negate).
|
|
33
|
+
*
|
|
34
|
+
* Presence patterns (found -> truthy): `>= 0`, `> -1`, `!== -1`, `!= -1`
|
|
35
|
+
* Absence patterns (not found -> negate): `< 0`, `=== -1`, `== -1`
|
|
36
|
+
* Handles both operand orders (indexOf on left or right).
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractIndexOfPresence(node: ts.BinaryExpression, checker: ts.TypeChecker): {
|
|
39
|
+
call: ts.CallExpression;
|
|
40
|
+
isString: boolean;
|
|
41
|
+
negate: boolean;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* Type-checking helpers for catalog transforms.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isStringType(expr: ts.Expression, checker: ts.TypeChecker): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Checks whether `expr` resolves to a string or number type.
|
|
49
|
+
* Falls back to inspecting the symbol's declared type annotation when
|
|
50
|
+
* `getTypeAtLocation` returns an unexpected type (e.g. `void` for identifiers
|
|
51
|
+
* that shadow DOM globals like `name`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function isStringOrNumberLike(checker: ts.TypeChecker, expr: ts.Expression): boolean;
|
|
54
|
+
export declare function isArrayType(expr: ts.Expression, checker: ts.TypeChecker): boolean;
|
|
55
|
+
export declare function isDetectedEventType(expr: ts.Expression, checker: ts.TypeChecker): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Returns true when `node` is `detectedEvent.index`, a property access
|
|
58
|
+
* on a `DetectedEvent` reading the `.index` field.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isDetectedEventIndex(node: ts.PropertyAccessExpression, checker: ts.TypeChecker): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Checks whether `node` is `obj.method(args)` where `obj` matches the
|
|
63
|
+
* given type predicate and the method name matches.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isMethodCall(node: ts.CallExpression, checker: ts.TypeChecker, typeGuard: (expr: ts.Expression, checker: ts.TypeChecker) => boolean, method: string, argCount?: number): node is ts.CallExpression & {
|
|
66
|
+
expression: ts.PropertyAccessExpression;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Checks whether `node` is `Namespace.method(args)` using syntactic
|
|
70
|
+
* identifier matching (no TypeChecker needed).
|
|
71
|
+
*/
|
|
72
|
+
export declare function isNamespaceCall(node: ts.CallExpression, namespace: string, method: string): node is ts.CallExpression & {
|
|
73
|
+
expression: ts.PropertyAccessExpression;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Checks whether `node` is a call to a global function by name.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isGlobalCall(node: ts.CallExpression, name: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Creates a `ns.fn(...args)` Lua call expression.
|
|
81
|
+
*/
|
|
82
|
+
export declare function createNamespacedCall(ns: string, fn: string, args: tstl.Expression[], node?: ts.Node): tstl.CallExpression;
|
|
83
|
+
/** Creates a `string.find(str, search, 1, true)` plain-text search call. */
|
|
84
|
+
export declare function createStringFindCall(str: tstl.Expression, search: tstl.Expression, node?: ts.Node): tstl.CallExpression;
|
|
85
|
+
/** Escapes a string for literal use inside `new RegExp(...)`. */
|
|
86
|
+
export declare function escapeRegex(s: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Detects `arr = arr.concat(b, c, ...)` where LHS is a simple identifier,
|
|
89
|
+
* the receiver matches LHS, and all concat arguments are array-typed.
|
|
90
|
+
*/
|
|
91
|
+
export declare function extractConcatSelfAssignment(expr: ts.BinaryExpression, checker: ts.TypeChecker): {
|
|
92
|
+
name: ts.Identifier;
|
|
93
|
+
args: readonly ts.Expression[];
|
|
94
|
+
} | null;
|
|
95
|
+
/**
|
|
96
|
+
* Detects `arr = [...arr, ...b, ...c]` where LHS is a simple identifier,
|
|
97
|
+
* all elements are spreads, the first spread matches LHS, and all tail
|
|
98
|
+
* spread expressions are array-typed.
|
|
99
|
+
*/
|
|
100
|
+
export declare function extractSpreadSelfAssignment(expr: ts.BinaryExpression, checker: ts.TypeChecker): {
|
|
101
|
+
name: ts.Identifier;
|
|
102
|
+
args: ts.Expression[];
|
|
103
|
+
} | null;
|
|
104
|
+
/**
|
|
105
|
+
* Builds nested `table.extend` calls:
|
|
106
|
+
* - Single arg: `table.extend(arr, b)`
|
|
107
|
+
* - Multiple: `table.extend(table.extend(arr, b), c)`
|
|
108
|
+
*/
|
|
109
|
+
export declare function emitChainedExtend(target: tstl.Expression, args: [tstl.Expression, ...tstl.Expression[]], node: ts.Node): tstl.CallExpression;
|
|
110
|
+
export interface LLIndexSemantics {
|
|
111
|
+
indexArgs: Set<string>;
|
|
112
|
+
indexReturn: boolean;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* For an `ll.Foo(...)` call, inspect the resolved signature's JSDoc tags
|
|
116
|
+
* to determine which arguments have `@indexArg` semantics and whether
|
|
117
|
+
* the return value has `@indexReturn` semantics.
|
|
118
|
+
*/
|
|
119
|
+
export declare function getLLIndexSemantics(node: ts.CallExpression, checker: ts.TypeChecker): LLIndexSemantics | null;
|
|
120
|
+
/**
|
|
121
|
+
* Adjusts a 0-based index argument to 1-based for Lua.
|
|
122
|
+
* Constant-folds numeric literals; otherwise emits `expr + 1`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function adjustIndexArg(arg: ts.Expression, context: tstl.TransformationContext): tstl.Expression;
|
|
125
|
+
/**
|
|
126
|
+
* Emit an `ll.Foo(...)` call with automatic index adjustments:
|
|
127
|
+
* - `@indexArg` parameters get `+1`
|
|
128
|
+
* - `@indexReturn` wraps the result in a nil-safe `__tmp and (__tmp - 1)`
|
|
129
|
+
*/
|
|
130
|
+
export declare function emitLLIndexCall(node: ts.CallExpression, context: tstl.TransformationContext, semantics: LLIndexSemantics): tstl.Expression;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as tstl from "typescript-to-lua";
|
|
3
|
+
import { EQUALITY_OPS } from "./constants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a `bit32.<fn>(...args)` Lua call expression.
|
|
6
|
+
* The optional `node` attaches TypeScript source-map information; when
|
|
7
|
+
* patching already-lowered Lua AST nodes (e.g. from compound-assignment
|
|
8
|
+
* desugaring) there is no originating TS node, so it may be omitted.
|
|
9
|
+
*/
|
|
10
|
+
export function createBit32Call(fn, args, node) {
|
|
11
|
+
return tstl.createCallExpression(tstl.createTableIndexExpression(tstl.createIdentifier("bit32"), tstl.createStringLiteral(fn)), args, node);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns true when `node` is `Math.floor(<single-arg>)`.
|
|
15
|
+
*/
|
|
16
|
+
export function isMathFloor(node) {
|
|
17
|
+
return (node.arguments.length === 1 &&
|
|
18
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
19
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
20
|
+
node.expression.expression.text === "Math" &&
|
|
21
|
+
node.expression.name.text === "floor");
|
|
22
|
+
}
|
|
23
|
+
export function isZeroLiteral(node) {
|
|
24
|
+
return ts.isNumericLiteral(node) && node.text === "0";
|
|
25
|
+
}
|
|
26
|
+
export function isNegatedEquality(op) {
|
|
27
|
+
return (op === ts.SyntaxKind.ExclamationEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Detect `(a & b) !== 0`, `0 === (a & b)`, etc. and return the `&` expression
|
|
31
|
+
* plus whether to negate the btest result.
|
|
32
|
+
*/
|
|
33
|
+
export function extractBtestPattern(node) {
|
|
34
|
+
const op = node.operatorToken.kind;
|
|
35
|
+
if (!EQUALITY_OPS.has(op))
|
|
36
|
+
return null;
|
|
37
|
+
let bandExpr;
|
|
38
|
+
if (isZeroLiteral(node.right)) {
|
|
39
|
+
bandExpr = ts.isParenthesizedExpression(node.left) ? node.left.expression : node.left;
|
|
40
|
+
}
|
|
41
|
+
else if (isZeroLiteral(node.left)) {
|
|
42
|
+
bandExpr = ts.isParenthesizedExpression(node.right) ? node.right.expression : node.right;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (!ts.isBinaryExpression(bandExpr) ||
|
|
48
|
+
bandExpr.operatorToken.kind !== ts.SyntaxKind.AmpersandToken) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// `== 0` / `=== 0` mean "no bits in common", so we negate btest (negate = true).
|
|
52
|
+
// `!= 0` / `!== 0` mean "some bits in common", which is what btest returns
|
|
53
|
+
// directly, so no negation is needed. The `!isNegatedEquality` double-negative
|
|
54
|
+
// captures this: negated equality (!=) -> false -> don't negate btest.
|
|
55
|
+
return { band: bandExpr, negate: !isNegatedEquality(op) };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Detects `-1` as either a PrefixUnaryExpression (minus + 1) or a numeric literal with text "-1".
|
|
59
|
+
*/
|
|
60
|
+
export function isMinusOneLiteral(node) {
|
|
61
|
+
if (ts.isPrefixUnaryExpression(node) &&
|
|
62
|
+
node.operator === ts.SyntaxKind.MinusToken &&
|
|
63
|
+
ts.isNumericLiteral(node.operand) &&
|
|
64
|
+
node.operand.text === "1") {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return ts.isNumericLiteral(node) && node.text === "-1";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Detect `s.indexOf(x) >= 0`, `s.indexOf(x) !== -1`, etc. and return
|
|
71
|
+
* the indexOf call plus whether the check means "not found" (negate).
|
|
72
|
+
*
|
|
73
|
+
* Presence patterns (found -> truthy): `>= 0`, `> -1`, `!== -1`, `!= -1`
|
|
74
|
+
* Absence patterns (not found -> negate): `< 0`, `=== -1`, `== -1`
|
|
75
|
+
* Handles both operand orders (indexOf on left or right).
|
|
76
|
+
*/
|
|
77
|
+
export function extractIndexOfPresence(node, checker) {
|
|
78
|
+
const op = node.operatorToken.kind;
|
|
79
|
+
let indexOfExpr;
|
|
80
|
+
let comparand;
|
|
81
|
+
let flipped;
|
|
82
|
+
// Try indexOf on the left
|
|
83
|
+
if (ts.isCallExpression(node.left)) {
|
|
84
|
+
indexOfExpr = node.left;
|
|
85
|
+
comparand = node.right;
|
|
86
|
+
flipped = false;
|
|
87
|
+
}
|
|
88
|
+
else if (ts.isCallExpression(node.right)) {
|
|
89
|
+
indexOfExpr = node.right;
|
|
90
|
+
comparand = node.left;
|
|
91
|
+
flipped = true;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// Verify it's an indexOf call on a string or array with 1 arg
|
|
97
|
+
const call = indexOfExpr;
|
|
98
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
99
|
+
return null;
|
|
100
|
+
if (call.expression.name.text !== "indexOf")
|
|
101
|
+
return null;
|
|
102
|
+
if (call.arguments.length !== 1)
|
|
103
|
+
return null;
|
|
104
|
+
const receiver = call.expression.expression;
|
|
105
|
+
let isString;
|
|
106
|
+
if (isStringType(receiver, checker)) {
|
|
107
|
+
isString = true;
|
|
108
|
+
}
|
|
109
|
+
else if (isArrayType(receiver, checker)) {
|
|
110
|
+
isString = false;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Determine the comparison semantics
|
|
116
|
+
// For non-flipped (indexOf on left): indexOf >= 0, indexOf !== -1, indexOf < 0, indexOf === -1
|
|
117
|
+
// For flipped (indexOf on right): 0 <= indexOf, -1 !== indexOf, 0 > indexOf, -1 === indexOf
|
|
118
|
+
let negate = null;
|
|
119
|
+
if (isZeroLiteral(comparand)) {
|
|
120
|
+
if (!flipped) {
|
|
121
|
+
// indexOf >= 0 -> found; indexOf < 0 -> not found
|
|
122
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken)
|
|
123
|
+
negate = false;
|
|
124
|
+
else if (op === ts.SyntaxKind.LessThanToken)
|
|
125
|
+
negate = true;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// 0 <= indexOf -> found; 0 > indexOf -> not found
|
|
129
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken)
|
|
130
|
+
negate = false;
|
|
131
|
+
else if (op === ts.SyntaxKind.GreaterThanToken)
|
|
132
|
+
negate = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (isMinusOneLiteral(comparand)) {
|
|
136
|
+
if (!flipped) {
|
|
137
|
+
// indexOf !== -1 -> found; indexOf === -1 -> not found; indexOf > -1 -> found
|
|
138
|
+
if (isNegatedEquality(op))
|
|
139
|
+
negate = false;
|
|
140
|
+
else if (EQUALITY_OPS.has(op))
|
|
141
|
+
negate = true;
|
|
142
|
+
else if (op === ts.SyntaxKind.GreaterThanToken)
|
|
143
|
+
negate = false;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// -1 !== indexOf -> found; -1 === indexOf -> not found; -1 < indexOf -> found
|
|
147
|
+
if (isNegatedEquality(op))
|
|
148
|
+
negate = false;
|
|
149
|
+
else if (EQUALITY_OPS.has(op))
|
|
150
|
+
negate = true;
|
|
151
|
+
else if (op === ts.SyntaxKind.LessThanToken)
|
|
152
|
+
negate = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (negate === null)
|
|
156
|
+
return null;
|
|
157
|
+
return { call, isString, negate };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Type-checking helpers for catalog transforms.
|
|
161
|
+
*/
|
|
162
|
+
export function isStringType(expr, checker) {
|
|
163
|
+
const type = checker.getTypeAtLocation(expr);
|
|
164
|
+
return !!(type.flags & ts.TypeFlags.StringLike);
|
|
165
|
+
}
|
|
166
|
+
const STRING_OR_NUMBER = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike;
|
|
167
|
+
/**
|
|
168
|
+
* Checks whether `expr` resolves to a string or number type.
|
|
169
|
+
* Falls back to inspecting the symbol's declared type annotation when
|
|
170
|
+
* `getTypeAtLocation` returns an unexpected type (e.g. `void` for identifiers
|
|
171
|
+
* that shadow DOM globals like `name`).
|
|
172
|
+
*/
|
|
173
|
+
export function isStringOrNumberLike(checker, expr) {
|
|
174
|
+
const type = checker.getTypeAtLocation(expr);
|
|
175
|
+
if (type.flags & STRING_OR_NUMBER)
|
|
176
|
+
return true;
|
|
177
|
+
// Fallback: resolve the type from the variable's type annotation directly.
|
|
178
|
+
const symbol = checker.getSymbolAtLocation(expr);
|
|
179
|
+
const decl = symbol?.valueDeclaration;
|
|
180
|
+
if (decl && ts.isVariableDeclaration(decl) && decl.type) {
|
|
181
|
+
return !!(checker.getTypeFromTypeNode(decl.type).flags & STRING_OR_NUMBER);
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
export function isArrayType(expr, checker) {
|
|
186
|
+
const type = checker.getTypeAtLocation(expr);
|
|
187
|
+
return checker.isArrayLikeType(type);
|
|
188
|
+
}
|
|
189
|
+
export function isDetectedEventType(expr, checker) {
|
|
190
|
+
const type = checker.getTypeAtLocation(expr);
|
|
191
|
+
return type.symbol?.name === "DetectedEvent";
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Returns true when `node` is `detectedEvent.index`, a property access
|
|
195
|
+
* on a `DetectedEvent` reading the `.index` field.
|
|
196
|
+
*/
|
|
197
|
+
export function isDetectedEventIndex(node, checker) {
|
|
198
|
+
return node.name.text === "index" && isDetectedEventType(node.expression, checker);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Checks whether `node` is `obj.method(args)` where `obj` matches the
|
|
202
|
+
* given type predicate and the method name matches.
|
|
203
|
+
*/
|
|
204
|
+
export function isMethodCall(node, checker, typeGuard, method, argCount) {
|
|
205
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
206
|
+
return false;
|
|
207
|
+
if (node.expression.name.text !== method)
|
|
208
|
+
return false;
|
|
209
|
+
if (argCount !== undefined && node.arguments.length !== argCount)
|
|
210
|
+
return false;
|
|
211
|
+
return typeGuard(node.expression.expression, checker);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Checks whether `node` is `Namespace.method(args)` using syntactic
|
|
215
|
+
* identifier matching (no TypeChecker needed).
|
|
216
|
+
*/
|
|
217
|
+
export function isNamespaceCall(node, namespace, method) {
|
|
218
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
219
|
+
return false;
|
|
220
|
+
if (node.expression.name.text !== method)
|
|
221
|
+
return false;
|
|
222
|
+
return (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === namespace);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Checks whether `node` is a call to a global function by name.
|
|
226
|
+
*/
|
|
227
|
+
export function isGlobalCall(node, name) {
|
|
228
|
+
return ts.isIdentifier(node.expression) && node.expression.text === name;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Creates a `ns.fn(...args)` Lua call expression.
|
|
232
|
+
*/
|
|
233
|
+
export function createNamespacedCall(ns, fn, args, node) {
|
|
234
|
+
return tstl.createCallExpression(tstl.createTableIndexExpression(tstl.createIdentifier(ns), tstl.createStringLiteral(fn)), args, node);
|
|
235
|
+
}
|
|
236
|
+
/** Creates a `string.find(str, search, 1, true)` plain-text search call. */
|
|
237
|
+
export function createStringFindCall(str, search, node) {
|
|
238
|
+
return createNamespacedCall("string", "find", [str, search, tstl.createNumericLiteral(1), tstl.createBooleanLiteral(true)], node);
|
|
239
|
+
}
|
|
240
|
+
/** Escapes a string for literal use inside `new RegExp(...)`. */
|
|
241
|
+
export function escapeRegex(s) {
|
|
242
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Self-reassignment array concat -> table.extend optimization
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
/**
|
|
248
|
+
* Detects `arr = arr.concat(b, c, ...)` where LHS is a simple identifier,
|
|
249
|
+
* the receiver matches LHS, and all concat arguments are array-typed.
|
|
250
|
+
*/
|
|
251
|
+
export function extractConcatSelfAssignment(expr, checker) {
|
|
252
|
+
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
253
|
+
return null;
|
|
254
|
+
if (!ts.isIdentifier(expr.left))
|
|
255
|
+
return null;
|
|
256
|
+
if (!ts.isCallExpression(expr.right))
|
|
257
|
+
return null;
|
|
258
|
+
if (!ts.isPropertyAccessExpression(expr.right.expression))
|
|
259
|
+
return null;
|
|
260
|
+
if (expr.right.expression.name.text !== "concat")
|
|
261
|
+
return null;
|
|
262
|
+
if (!ts.isIdentifier(expr.right.expression.expression))
|
|
263
|
+
return null;
|
|
264
|
+
if (expr.right.expression.expression.text !== expr.left.text)
|
|
265
|
+
return null;
|
|
266
|
+
if (expr.right.arguments.length === 0)
|
|
267
|
+
return null;
|
|
268
|
+
for (const arg of expr.right.arguments) {
|
|
269
|
+
if (!isArrayType(arg, checker))
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return { name: expr.left, args: expr.right.arguments };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Detects `arr = [...arr, ...b, ...c]` where LHS is a simple identifier,
|
|
276
|
+
* all elements are spreads, the first spread matches LHS, and all tail
|
|
277
|
+
* spread expressions are array-typed.
|
|
278
|
+
*/
|
|
279
|
+
export function extractSpreadSelfAssignment(expr, checker) {
|
|
280
|
+
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
281
|
+
return null;
|
|
282
|
+
if (!ts.isIdentifier(expr.left))
|
|
283
|
+
return null;
|
|
284
|
+
if (!ts.isArrayLiteralExpression(expr.right))
|
|
285
|
+
return null;
|
|
286
|
+
const elements = expr.right.elements;
|
|
287
|
+
if (elements.length < 2)
|
|
288
|
+
return null;
|
|
289
|
+
for (const el of elements) {
|
|
290
|
+
if (!ts.isSpreadElement(el))
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const first = elements[0];
|
|
294
|
+
if (!ts.isIdentifier(first.expression))
|
|
295
|
+
return null;
|
|
296
|
+
if (first.expression.text !== expr.left.text)
|
|
297
|
+
return null;
|
|
298
|
+
const tailArgs = [];
|
|
299
|
+
for (let i = 1; i < elements.length; i++) {
|
|
300
|
+
const spread = elements[i];
|
|
301
|
+
if (!isArrayType(spread.expression, checker))
|
|
302
|
+
return null;
|
|
303
|
+
tailArgs.push(spread.expression);
|
|
304
|
+
}
|
|
305
|
+
return { name: expr.left, args: tailArgs };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Builds nested `table.extend` calls:
|
|
309
|
+
* - Single arg: `table.extend(arr, b)`
|
|
310
|
+
* - Multiple: `table.extend(table.extend(arr, b), c)`
|
|
311
|
+
*/
|
|
312
|
+
export function emitChainedExtend(target, args, node) {
|
|
313
|
+
let result = createNamespacedCall("table", "extend", [target, args[0]], node);
|
|
314
|
+
for (let i = 1; i < args.length; i++) {
|
|
315
|
+
result = createNamespacedCall("table", "extend", [result, args[i]], node);
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* For an `ll.Foo(...)` call, inspect the resolved signature's JSDoc tags
|
|
321
|
+
* to determine which arguments have `@indexArg` semantics and whether
|
|
322
|
+
* the return value has `@indexReturn` semantics.
|
|
323
|
+
*/
|
|
324
|
+
export function getLLIndexSemantics(node, checker) {
|
|
325
|
+
// Must be ll.Something(...)
|
|
326
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
327
|
+
return null;
|
|
328
|
+
if (!ts.isIdentifier(node.expression.expression))
|
|
329
|
+
return null;
|
|
330
|
+
if (node.expression.expression.text !== "ll")
|
|
331
|
+
return null;
|
|
332
|
+
const sig = checker.getResolvedSignature(node);
|
|
333
|
+
const decl = sig?.declaration;
|
|
334
|
+
if (!decl || !ts.isFunctionDeclaration(decl))
|
|
335
|
+
return null;
|
|
336
|
+
const tags = ts.getJSDocTags(decl);
|
|
337
|
+
const indexArgs = new Set();
|
|
338
|
+
let indexReturn = false;
|
|
339
|
+
for (const tag of tags) {
|
|
340
|
+
const tagName = tag.tagName.text;
|
|
341
|
+
if (tagName === "indexArg") {
|
|
342
|
+
const text = typeof tag.comment === "string"
|
|
343
|
+
? tag.comment
|
|
344
|
+
: Array.isArray(tag.comment)
|
|
345
|
+
? tag.comment.map((c) => c.text).join("")
|
|
346
|
+
: undefined;
|
|
347
|
+
if (text) {
|
|
348
|
+
indexArgs.add(text.trim());
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (tagName === "indexReturn") {
|
|
352
|
+
indexReturn = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (indexArgs.size === 0 && !indexReturn)
|
|
356
|
+
return null;
|
|
357
|
+
return { indexArgs, indexReturn };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Adjusts a 0-based index argument to 1-based for Lua.
|
|
361
|
+
* Constant-folds numeric literals; otherwise emits `expr + 1`.
|
|
362
|
+
*/
|
|
363
|
+
export function adjustIndexArg(arg, context) {
|
|
364
|
+
if (ts.isNumericLiteral(arg)) {
|
|
365
|
+
return tstl.createNumericLiteral(Number(arg.text) + 1);
|
|
366
|
+
}
|
|
367
|
+
// DetectedEvent.index is already 1-based at runtime. The PropertyAccess
|
|
368
|
+
// visitor emits `- 1` to make it 0-based for TS, and @indexArg adds `+ 1`.
|
|
369
|
+
// These cancel out, so emit the raw property access directly.
|
|
370
|
+
if (ts.isPropertyAccessExpression(arg) && isDetectedEventIndex(arg, context.checker)) {
|
|
371
|
+
const obj = context.transformExpression(arg.expression);
|
|
372
|
+
return tstl.createTableIndexExpression(obj, tstl.createStringLiteral("index"));
|
|
373
|
+
}
|
|
374
|
+
return tstl.createBinaryExpression(context.transformExpression(arg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, arg);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Emit an `ll.Foo(...)` call with automatic index adjustments:
|
|
378
|
+
* - `@indexArg` parameters get `+1`
|
|
379
|
+
* - `@indexReturn` wraps the result in a nil-safe `__tmp and (__tmp - 1)`
|
|
380
|
+
*/
|
|
381
|
+
export function emitLLIndexCall(node, context, semantics) {
|
|
382
|
+
const expr = node.expression;
|
|
383
|
+
const fnName = expr.name.text;
|
|
384
|
+
// Resolve parameter names from the declaration
|
|
385
|
+
const sig = context.checker.getResolvedSignature(node);
|
|
386
|
+
const params = sig?.declaration && ts.isFunctionDeclaration(sig.declaration)
|
|
387
|
+
? sig.declaration.parameters
|
|
388
|
+
: undefined;
|
|
389
|
+
const args = node.arguments.map((arg, i) => {
|
|
390
|
+
const paramName = params?.[i]?.name;
|
|
391
|
+
const name = paramName && ts.isIdentifier(paramName) ? paramName.text : undefined;
|
|
392
|
+
if (name && semantics.indexArgs.has(name)) {
|
|
393
|
+
return adjustIndexArg(arg, context);
|
|
394
|
+
}
|
|
395
|
+
return context.transformExpression(arg);
|
|
396
|
+
});
|
|
397
|
+
const call = createNamespacedCall("ll", fnName, args, node);
|
|
398
|
+
if (!semantics.indexReturn) {
|
|
399
|
+
return call;
|
|
400
|
+
}
|
|
401
|
+
// Check if return type is number-like (skip for list/string returns like llFindNotecardTextSync)
|
|
402
|
+
const retType = context.checker.getTypeAtLocation(node);
|
|
403
|
+
const isNumberReturn = !!(retType.flags & ts.TypeFlags.NumberLike) ||
|
|
404
|
+
(retType.isUnion() && retType.types.some((t) => !!(t.flags & ts.TypeFlags.NumberLike)));
|
|
405
|
+
if (!isNumberReturn) {
|
|
406
|
+
return call;
|
|
407
|
+
}
|
|
408
|
+
// Nil-safe return adjustment: `local __tmp = ll.Fn(...); __tmp and (__tmp - 1)`
|
|
409
|
+
const tempId = tstl.createIdentifier("____tmp");
|
|
410
|
+
context.addPrecedingStatements(tstl.createVariableDeclarationStatement(tempId, call, node));
|
|
411
|
+
return tstl.createBinaryExpression(tstl.cloneIdentifier(tempId), tstl.createParenthesizedExpression(tstl.createBinaryExpression(tstl.cloneIdentifier(tempId), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator)), tstl.SyntaxKind.AndOperator, node);
|
|
412
|
+
}
|