rbxts-transform-boost 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts DELETED
@@ -1,14 +0,0 @@
1
- export interface PluginConfig {
2
- // Prepend --!optimize 2 to every file that doesn't already have it.
3
- // Default: true
4
- optimize?: boolean;
5
-
6
- // Prepend --!strict to every file that doesn't already have it.
7
- // Default: true
8
- strict?: boolean;
9
-
10
- // Hoist repeated game.GetService() calls to module-level locals,
11
- // and hoist repeated property access chains within functions to locals.
12
- // Default: true
13
- hoist?: boolean;
14
- }
package/src/index.ts DELETED
@@ -1,25 +0,0 @@
1
- import type ts from "typescript";
2
- import type { PluginConfig } from "./config";
3
- import { nativePass } from "./passes/native";
4
- import { cachePass } from "./passes/cache";
5
- import { loopsPass } from "./passes/loops";
6
- import { annotatePass } from "./passes/annotate";
7
-
8
- export type { PluginConfig };
9
-
10
- export default function(
11
- program: ts.Program,
12
- config: PluginConfig = {},
13
- { ts }: { ts: typeof import("typescript") },
14
- ): ts.TransformerFactory<ts.SourceFile> {
15
- const { optimize = true, strict = true, hoist = true } = config;
16
-
17
- return (ctx: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
18
- annotatePass(ts, program, sourceFile);
19
- let result = sourceFile;
20
- if (hoist) result = cachePass(ts, program, ctx, result);
21
- result = loopsPass(ts, program, ctx, result);
22
- if (optimize || strict) result = nativePass(ts, ctx, result, optimize, strict);
23
- return result;
24
- };
25
- }
@@ -1,400 +0,0 @@
1
- import type ts from "typescript";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
-
5
- const LUAU_TYPE: Record<string, string> = {
6
- number: "number",
7
- string: "string",
8
- boolean: "boolean",
9
- Vector3: "Vector3",
10
- Vector2: "Vector2",
11
- Vector2int16: "Vector2int16",
12
- Vector3int16: "Vector3int16",
13
- CFrame: "CFrame",
14
- UDim: "UDim",
15
- UDim2: "UDim2",
16
- Color3: "Color3",
17
- BrickColor: "BrickColor",
18
- TweenInfo: "TweenInfo",
19
- NumberRange: "NumberRange",
20
- NumberSequence: "NumberSequence",
21
- ColorSequence: "ColorSequence",
22
- Rect: "Rect",
23
- Region3: "Region3",
24
- Ray: "Ray",
25
- buffer: "buffer",
26
- Instance: "Instance",
27
- BasePart: "BasePart",
28
- Part: "Part",
29
- Model: "Model",
30
- Player: "Player",
31
- Camera: "Camera",
32
- Workspace: "Workspace",
33
- RunService: "RunService",
34
- Players: "Players",
35
- };
36
-
37
- type FnAnnotation = {
38
- params: Array<string | null>;
39
- ret: string | null;
40
- };
41
-
42
- type FileSidecar = {
43
- fns: Map<string, FnAnnotation>;
44
- consts: Set<string>;
45
- };
46
-
47
- const sidecar = new Map<string, FileSidecar>();
48
- let hooked = false;
49
-
50
- function mapTypeNode(ts: typeof import("typescript"), typeNode: ts.TypeNode): string | null {
51
- if (ts.isTypeReferenceNode(typeNode)) {
52
- const name = ts.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : null;
53
- if (!name) return null;
54
- if (LUAU_TYPE[name]) return LUAU_TYPE[name];
55
- if ((name === "Array" || name === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
56
- const inner = mapTypeNode(ts, typeNode.typeArguments[0]);
57
- return inner ? `{${inner}}` : "{any}";
58
- }
59
- return null;
60
- }
61
- if (ts.isArrayTypeNode(typeNode)) {
62
- const inner = mapTypeNode(ts, typeNode.elementType);
63
- return inner ? `{${inner}}` : "{any}";
64
- }
65
- const kw: Partial<Record<number, string>> = {
66
- [ts.SyntaxKind.NumberKeyword]: "number",
67
- [ts.SyntaxKind.StringKeyword]: "string",
68
- [ts.SyntaxKind.BooleanKeyword]: "boolean",
69
- };
70
- if (typeNode.kind in kw) return kw[typeNode.kind]!;
71
- return null;
72
- }
73
-
74
- function luauTypeForParam(
75
- ts: typeof import("typescript"),
76
- checker: ts.TypeChecker,
77
- node: ts.ParameterDeclaration,
78
- ): string | null {
79
- if (node.type) {
80
- const mapped = mapTypeNode(ts, node.type);
81
- if (mapped) return mapped;
82
- }
83
- const name = checker.typeToString(checker.getTypeAtLocation(node));
84
- return LUAU_TYPE[name] ?? null;
85
- }
86
-
87
- function luauTypeForReturn(
88
- ts: typeof import("typescript"),
89
- checker: ts.TypeChecker,
90
- node: ts.FunctionDeclaration,
91
- ): string | null {
92
- if (node.type) {
93
- const mapped = mapTypeNode(ts, node.type);
94
- if (mapped) return mapped;
95
- }
96
- const sig = checker.getSignatureFromDeclaration(node);
97
- if (!sig) return null;
98
- const ret = checker.getReturnTypeOfSignature(sig);
99
- const name = checker.typeToString(ret);
100
- return LUAU_TYPE[name] ?? null;
101
- }
102
-
103
- function outPathForSource(sourceFile: ts.SourceFile, program: ts.Program): string | null {
104
- const options = program.getCompilerOptions();
105
- const outDir = options.outDir;
106
- if (!outDir) return null;
107
- const rootDir = options.rootDir ?? commonRoot(program.getRootFileNames());
108
- if (!rootDir) return null;
109
- const rel = path.relative(rootDir, sourceFile.fileName);
110
- if (rel.startsWith("..")) return null;
111
- return path.join(outDir, rel.replace(/\.tsx?$/, ".luau"));
112
- }
113
-
114
- function commonRoot(files: readonly string[]): string | undefined {
115
- if (files.length === 0) return undefined;
116
- const parts = files[0].split(path.sep);
117
- let root = parts.slice(0, parts.length - 1);
118
- for (const f of files.slice(1)) {
119
- const fp = f.split(path.sep);
120
- let i = 0;
121
- while (i < root.length && i < fp.length - 1 && root[i] === fp[i]) i++;
122
- root = root.slice(0, i);
123
- }
124
- return root.join(path.sep) || undefined;
125
- }
126
-
127
- function collectAnnotations(
128
- ts: typeof import("typescript"),
129
- checker: ts.TypeChecker,
130
- sourceFile: ts.SourceFile,
131
- outPath: string,
132
- ): void {
133
- const entry = sidecar.get(outPath) ?? { fns: new Map<string, FnAnnotation>(), consts: new Set<string>() };
134
- sidecar.set(outPath, entry);
135
-
136
- function visit(node: ts.Node): void {
137
- if (ts.isFunctionDeclaration(node) && node.name) {
138
- const params = node.parameters.map(p => luauTypeForParam(ts, checker, p));
139
- const ret = luauTypeForReturn(ts, checker, node);
140
- if (params.some(p => p !== null) || ret !== null) {
141
- entry.fns.set(node.name.text, { params, ret });
142
- }
143
- }
144
- if (ts.isVariableStatement(node)) {
145
- const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
146
- if (isConst) {
147
- for (const decl of node.declarationList.declarations) {
148
- if (ts.isIdentifier(decl.name)) {
149
- entry.consts.add(decl.name.text);
150
- }
151
- }
152
- }
153
- }
154
- ts.forEachChild(node, visit);
155
- }
156
- visit(sourceFile);
157
- }
158
-
159
- function byLengthDesc(a: string, b: string): number {
160
- return b.length - a.length;
161
- }
162
-
163
- type ImportGroup = { label: string; lines: string[] };
164
-
165
- function organizePreamble(src: string): string {
166
- const lines = src.split("\n");
167
- let i = 0;
168
-
169
- // Collect --! directives separately from other leading comments
170
- const shebang: string[] = [];
171
- const header: string[] = [];
172
- while (i < lines.length && lines[i].startsWith("--")) {
173
- if (lines[i].startsWith("--!")) {
174
- shebang.push(lines[i++]);
175
- } else {
176
- header.push(lines[i++]);
177
- }
178
- }
179
- shebang.sort(byLengthDesc);
180
-
181
- const services: string[] = [];
182
- const runtime: string[] = [];
183
- const importGroups: ImportGroup[] = [];
184
- const bindings: string[] = [];
185
-
186
- let pendingLabel: string | null = null;
187
- let currentImports: string[] = [];
188
-
189
- function flushImports(): void {
190
- if (currentImports.length > 0) {
191
- importGroups.push({ label: pendingLabel ?? "-- Imports", lines: [...currentImports] });
192
- currentImports = [];
193
- }
194
- pendingLabel = null;
195
- }
196
-
197
- while (i < lines.length) {
198
- const line = lines[i];
199
-
200
- if (line.trim() === "") {
201
- // Blank line = end of current import group
202
- flushImports();
203
- i++;
204
- continue;
205
- }
206
-
207
- if (/^--!/.test(line)) {
208
- // Rotor can emit --!native after preamble locals — hoist it up
209
- shebang.push(line); shebang.sort(byLengthDesc); i++;
210
- } else if (/^--/.test(line)) {
211
- // User comment becomes the label for the next import group
212
- flushImports();
213
- pendingLabel = line; i++;
214
- } else if (/^local \w+ = game:GetService\(/.test(line)) {
215
- flushImports();
216
- services.push(line); i++;
217
- } else if (/^local \w+ = require\(/.test(line)) {
218
- flushImports();
219
- runtime.push(line); i++;
220
- } else if (/^local \w+ = TS\.import\(/.test(line)) {
221
- currentImports.push(line); i++;
222
- } else if (/^local \w+ = \w+[\.\[]/.test(line) && !/^local function/.test(line)) {
223
- flushImports();
224
- bindings.push(line); i++;
225
- } else {
226
- break;
227
- }
228
- }
229
- flushImports();
230
-
231
- services.sort(byLengthDesc);
232
- bindings.sort(byLengthDesc);
233
-
234
- const out: string[] = [...shebang];
235
- if (header.length > 0) out.push("", ...header);
236
- if (runtime.length > 0) out.push("", "-- Runtime", ...runtime);
237
- if (services.length > 0) out.push("", "-- Services", ...services);
238
- for (const group of importGroups) {
239
- group.lines.sort(byLengthDesc);
240
- out.push("", group.label, ...group.lines);
241
- }
242
- if (bindings.length > 0) out.push("", "-- Bindings", ...bindings);
243
- if (i < lines.length) out.push("", ...lines.slice(i));
244
-
245
- return out.join("\n");
246
- }
247
-
248
- function hoistGetService(src: string): string {
249
- // Count occurrences of each game:GetService("X") call
250
- const re = /game:GetService\("([^"]+)"\)/g;
251
- const counts = new Map<string, number>();
252
- for (const m of src.matchAll(re)) {
253
- counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
254
- }
255
-
256
- const toHoist = [...counts.entries()].filter(([, n]) => n >= 2).map(([svc]) => svc);
257
- if (toHoist.length === 0) return src;
258
-
259
- // Build locals and replace
260
- const decls = toHoist
261
- .map(svc => `local _${svc} = game:GetService("${svc}")`)
262
- .join("\n");
263
-
264
- for (const svc of toHoist) {
265
- src = src.split(`game:GetService("${svc}")`).join(`_${svc}`);
266
- }
267
-
268
- // Insert after any leading --! directives and the rotor header comment
269
- const insertAt = src.search(/^(?!--[!\s]|--\s*Compiled)/m);
270
- if (insertAt === -1) return decls + "\n" + src;
271
- return src.slice(0, insertAt) + decls + "\n" + src.slice(insertAt);
272
- }
273
-
274
- function addSpacing(src: string): string {
275
- const lines = src.split("\n");
276
- const out: string[] = [];
277
-
278
- for (let i = 0; i < lines.length; i++) {
279
- const line = lines[i];
280
- const trimmed = line.trim();
281
- const prevOut = out.length > 0 ? out[out.length - 1] : "";
282
- const prevTrimmed = prevOut.trim();
283
- const alreadyBlank = prevTrimmed === "";
284
-
285
- if (!alreadyBlank) {
286
- // Blank before top-level local function
287
- if (/^local function /.test(trimmed)) {
288
- out.push("");
289
- }
290
- // Blank before return when it's not the first statement in its block
291
- else if (
292
- /^return\b/.test(trimmed) &&
293
- !/\b(then|do|repeat)$/.test(prevTrimmed) &&
294
- !/function\s*\([^)]*\)$/.test(prevTrimmed) &&
295
- !/^local function /.test(prevTrimmed)
296
- ) {
297
- out.push("");
298
- }
299
- // Blank before a block starter (do/while/for/if/repeat) when prev is local/const
300
- else if (/^(do\b|while |for |if |repeat\b)/.test(trimmed) && /^(local |const )/.test(prevTrimmed)) {
301
- out.push("");
302
- }
303
- // Blank on const → local transition
304
- else if (/^local /.test(trimmed) && /^const /.test(prevTrimmed)) {
305
- out.push("");
306
- }
307
- }
308
-
309
- out.push(line);
310
-
311
- // Blank after `end` when next non-blank line is not end/else/elseif/until
312
- if (trimmed === "end") {
313
- const next = lines[i + 1]?.trim() ?? "";
314
- if (next !== "" && !/^(end\b|else\b|elseif\b|until\b)/.test(next)) {
315
- out.push("");
316
- }
317
- }
318
- }
319
-
320
- return out.join("\n");
321
- }
322
-
323
- function injectAnnotations(luauPath: string, entry: FileSidecar): void {
324
- if (!fs.existsSync(luauPath)) return;
325
- let src = fs.readFileSync(luauPath, "utf8");
326
- let changed = false;
327
-
328
- // Inject param + return type annotations
329
- for (const [fnName, ann] of entry.fns) {
330
- if (ann.params.every(p => p === null) && ann.ret === null) continue;
331
-
332
- const re = new RegExp(
333
- `(local function ${escapeRegex(fnName)}\\()([^)]*)(\\.\\.\\.)?(\\))`,
334
- );
335
- src = src.replace(re, (_m, open: string, rawParams: string, vararg: string | undefined, close: string) => {
336
- const names = rawParams.split(",").map((s: string) => s.trim()).filter(Boolean);
337
- const annotated = names.map((name: string, i: number) => {
338
- const bare = name.split(":")[0].trim();
339
- const typ = ann.params[i];
340
- return typ ? `${bare}: ${typ}` : bare;
341
- });
342
- if (vararg) annotated.push("...");
343
- const retSuffix = ann.ret ? `: ${ann.ret}` : "";
344
- changed = true;
345
- return `${open}${annotated.join(", ")}${close}${retSuffix}`;
346
- });
347
- }
348
-
349
- // Replace local → const for TypeScript const declarations
350
- for (const name of entry.consts) {
351
- const re = new RegExp(`^(\\t*)local (${escapeRegex(name)}) =`, "m");
352
- const next = src.replace(re, `$1const $2 =`);
353
- if (next !== src) { src = next; changed = true; }
354
- }
355
-
356
- // Hoist any repeated game:GetService() calls injected by the compiler
357
- const hoisted = hoistGetService(src);
358
- if (hoisted !== src) { src = hoisted; changed = true; }
359
-
360
- // Organize preamble into labeled sections
361
- const organized = organizePreamble(src);
362
- if (organized !== src) { src = organized; changed = true; }
363
-
364
- // Add blank lines between top-level blocks for readability
365
- const spaced = addSpacing(src);
366
- if (spaced !== src) { src = spaced; changed = true; }
367
-
368
- if (changed) fs.writeFileSync(luauPath, src, "utf8");
369
- }
370
-
371
- function escapeRegex(s: string): string {
372
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
373
- }
374
-
375
- function installWatcher(outDir: string): void {
376
- if (hooked) return;
377
- hooked = true;
378
- const seen = new Set<string>();
379
- const watcher = fs.watch(outDir, { recursive: true }, (_event, filename) => {
380
- if (!filename || !filename.endsWith(".luau")) return;
381
- const full = path.join(outDir, filename);
382
- if (seen.has(full)) return;
383
- const entry = sidecar.get(full);
384
- if (!entry) return;
385
- seen.add(full);
386
- try { injectAnnotations(full, entry); } catch { /* ignore */ }
387
- });
388
- watcher.unref();
389
- }
390
-
391
- export function annotatePass(
392
- ts: typeof import("typescript"),
393
- program: ts.Program,
394
- sourceFile: ts.SourceFile,
395
- ): void {
396
- const outPath = outPathForSource(sourceFile, program);
397
- if (!outPath) return;
398
- collectAnnotations(ts, program.getTypeChecker(), sourceFile, outPath);
399
- installWatcher(program.getCompilerOptions().outDir!);
400
- }
@@ -1,163 +0,0 @@
1
- import type ts from "typescript";
2
- import { chainKey, walk, isAssignmentTarget } from "../util";
3
-
4
- function isGetServiceCall(ts: typeof import("typescript"), node: ts.Node): node is ts.CallExpression {
5
- if (!ts.isCallExpression(node)) return false;
6
- const expr = node.expression;
7
- if (!ts.isPropertyAccessExpression(expr)) return false;
8
- const obj = expr.expression;
9
- return ts.isIdentifier(obj) && obj.text === "game" && expr.name.text === "GetService";
10
- }
11
-
12
- function getServiceName(ts: typeof import("typescript"), call: ts.CallExpression): string | undefined {
13
- const args = call.arguments;
14
- if (args.length !== 1) return undefined;
15
- const arg = args[0];
16
- if (!ts.isStringLiteral(arg)) return undefined;
17
- return arg.text;
18
- }
19
-
20
- export function cachePass(
21
- ts: typeof import("typescript"),
22
- _program: ts.Program,
23
- ctx: ts.TransformationContext,
24
- sourceFile: ts.SourceFile,
25
- ): ts.SourceFile {
26
- const factory = ctx.factory;
27
-
28
- // --- GetService hoisting ---
29
- const services = new Map<string, string>();
30
- walk(ts, sourceFile, node => {
31
- if (!node || !isGetServiceCall(ts, node)) return;
32
- const name = getServiceName(ts, node as ts.CallExpression);
33
- if (name && !services.has(name)) services.set(name, `_${name}`);
34
- });
35
-
36
- const serviceVisitor = (node: ts.Node): ts.Node => {
37
- if (isGetServiceCall(ts, node)) {
38
- const name = getServiceName(ts, node as ts.CallExpression);
39
- if (name && services.has(name)) return factory.createIdentifier(services.get(name)!);
40
- }
41
- return ts.visitEachChild(node, serviceVisitor, ctx);
42
- };
43
-
44
- let result = ts.visitEachChild(sourceFile, serviceVisitor, ctx) as ts.SourceFile;
45
-
46
- if (services.size > 0) {
47
- const hoistDecls = Array.from(services.entries()).map(([name, localName]) =>
48
- factory.createVariableStatement(
49
- undefined,
50
- factory.createVariableDeclarationList(
51
- [factory.createVariableDeclaration(
52
- factory.createIdentifier(localName),
53
- undefined,
54
- undefined,
55
- factory.createCallExpression(
56
- factory.createPropertyAccessExpression(
57
- factory.createIdentifier("game"),
58
- "GetService",
59
- ),
60
- undefined,
61
- [factory.createStringLiteral(name)],
62
- ),
63
- )],
64
- ts.NodeFlags.Const,
65
- ),
66
- )
67
- );
68
- result = factory.updateSourceFile(result, [...hoistDecls, ...Array.from(result.statements)]);
69
- }
70
-
71
- // --- Property chain hoisting within functions ---
72
- result = hoistPropertyChains(ts, result, factory, ctx);
73
- return result;
74
- }
75
-
76
- function hoistPropertyChains(
77
- ts: typeof import("typescript"),
78
- sourceFile: ts.SourceFile,
79
- factory: ts.NodeFactory,
80
- ctx: ts.TransformationContext,
81
- ): ts.SourceFile {
82
- const visitor = (node: ts.Node): ts.Node => {
83
- if (
84
- ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
85
- ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
86
- ) {
87
- return hoistInFunction(ts, node as ts.FunctionLikeDeclaration, factory, ctx);
88
- }
89
- return ts.visitEachChild(node, visitor, ctx);
90
- };
91
- return ts.visitEachChild(sourceFile, visitor, ctx) as ts.SourceFile;
92
- }
93
-
94
- function hoistInFunction(
95
- ts: typeof import("typescript"),
96
- fn: ts.FunctionLikeDeclaration,
97
- factory: ts.NodeFactory,
98
- ctx: ts.TransformationContext,
99
- ): ts.FunctionLikeDeclaration {
100
- if (!fn.body || !ts.isBlock(fn.body)) return fn;
101
-
102
- const counts = new Map<string, number>();
103
- walk(ts, fn.body, node => {
104
- if (!node || !ts.isPropertyAccessExpression(node)) return;
105
- const key = chainKey(ts, node);
106
- if (!key || !key.includes(".")) return;
107
- if (isAssignmentTarget(ts, node)) return;
108
- counts.set(key, (counts.get(key) ?? 0) + 1);
109
- });
110
-
111
- const toHoist = new Map<string, string>();
112
- let counter = 0;
113
-
114
- const candidates = Array.from(counts.entries())
115
- .filter(([, count]) => count >= 2)
116
- .sort((a, b) => b[0].length - a[0].length);
117
-
118
- for (const [key] of candidates) {
119
- const alreadyCovered = Array.from(toHoist.keys()).some(h => h.startsWith(key + "."));
120
- if (alreadyCovered) continue;
121
- toHoist.set(key, `_cache${counter++}`);
122
- }
123
-
124
- if (toHoist.size === 0) return fn;
125
-
126
- const chainVisitor = (node: ts.Node): ts.Node => {
127
- if (ts.isPropertyAccessExpression(node)) {
128
- const key = chainKey(ts, node);
129
- if (key && toHoist.has(key) && !isAssignmentTarget(ts, node)) {
130
- return factory.createIdentifier(toHoist.get(key)!);
131
- }
132
- }
133
- return ts.visitEachChild(node, chainVisitor, ctx);
134
- };
135
-
136
- const newBody = ts.visitEachChild(fn.body, chainVisitor, ctx) as ts.Block;
137
-
138
- const hoistStmts = Array.from(toHoist.entries()).map(([key, localName]) => {
139
- const parts = key.split(".");
140
- let expr: ts.Expression = factory.createIdentifier(parts[0]);
141
- for (let i = 1; i < parts.length; i++) {
142
- expr = factory.createPropertyAccessExpression(expr, parts[i]);
143
- }
144
- return factory.createVariableStatement(
145
- undefined,
146
- factory.createVariableDeclarationList(
147
- [factory.createVariableDeclaration(
148
- factory.createIdentifier(localName),
149
- undefined, undefined, expr,
150
- )],
151
- ts.NodeFlags.Const,
152
- ),
153
- );
154
- });
155
-
156
- const updatedBody = factory.updateBlock(newBody, [...hoistStmts, ...Array.from(newBody.statements)]);
157
-
158
- if (ts.isFunctionDeclaration(fn)) return factory.updateFunctionDeclaration(fn, fn.modifiers, fn.asteriskToken, fn.name, fn.typeParameters, fn.parameters, fn.type, updatedBody);
159
- if (ts.isFunctionExpression(fn)) return factory.updateFunctionExpression(fn, fn.modifiers, fn.asteriskToken, fn.name, fn.typeParameters, fn.parameters, fn.type, updatedBody);
160
- if (ts.isArrowFunction(fn)) return factory.updateArrowFunction(fn, fn.modifiers, fn.typeParameters, fn.parameters, fn.type, fn.equalsGreaterThanToken, updatedBody);
161
- if (ts.isMethodDeclaration(fn)) return factory.updateMethodDeclaration(fn, fn.modifiers, fn.asteriskToken, fn.name, fn.questionToken, fn.typeParameters, fn.parameters, fn.type, updatedBody);
162
- return fn;
163
- }
@@ -1,80 +0,0 @@
1
- import type ts from "typescript";
2
-
3
- export function loopsPass(
4
- ts: typeof import("typescript"),
5
- _program: ts.Program,
6
- ctx: ts.TransformationContext,
7
- sourceFile: ts.SourceFile,
8
- ): ts.SourceFile {
9
- const factory = ctx.factory;
10
-
11
- function isArrayLengthExpr(node: ts.Expression): ts.Expression | undefined {
12
- // matches: arr.size() or arr.length (if it ever appears)
13
- if (ts.isCallExpression(node)) {
14
- const expr = node.expression;
15
- if (ts.isPropertyAccessExpression(expr) && expr.name.text === "size" && node.arguments.length === 0) {
16
- return expr.expression;
17
- }
18
- }
19
- return undefined;
20
- }
21
-
22
- function rewriteForLoop(node: ts.ForStatement): ts.Statement {
23
- // Match: for (let i = 0; i < arr.size(); i++) { ... }
24
- const { initializer, condition, incrementor, statement } = node;
25
- if (!initializer || !condition || !incrementor) return node;
26
- if (!ts.isVariableDeclarationList(initializer)) return node;
27
- const decls = initializer.declarations;
28
- if (decls.length !== 1) return node;
29
- const decl = decls[0];
30
- if (!ts.isIdentifier(decl.name)) return node;
31
- if (!decl.initializer || !ts.isNumericLiteral(decl.initializer) || decl.initializer.text !== "0") return node;
32
- if (!ts.isBinaryExpression(condition)) return node;
33
- if (condition.operatorToken.kind !== ts.SyntaxKind.LessThanToken) return node;
34
-
35
- const arr = isArrayLengthExpr(condition.right);
36
- if (!arr) return node;
37
-
38
- const loopVar = decl.name.text;
39
- const arrName = ts.isIdentifier(arr) ? arr.text : undefined;
40
- if (!arrName) return node;
41
-
42
- const lenName = `_len_${arrName}`;
43
- const lenDecl = factory.createVariableStatement(
44
- undefined,
45
- factory.createVariableDeclarationList(
46
- [factory.createVariableDeclaration(
47
- factory.createIdentifier(lenName),
48
- undefined,
49
- factory.createTypeReferenceNode("number"),
50
- factory.createCallExpression(
51
- factory.createPropertyAccessExpression(arr, "size"),
52
- undefined, [],
53
- ),
54
- )],
55
- ts.NodeFlags.Const,
56
- ),
57
- );
58
-
59
- const newCondition = factory.updateBinaryExpression(
60
- condition,
61
- condition.left,
62
- condition.operatorToken,
63
- factory.createIdentifier(lenName),
64
- );
65
- const newFor = factory.updateForStatement(
66
- node, initializer, newCondition, incrementor, statement,
67
- );
68
-
69
- return factory.createBlock([lenDecl, newFor], true) as unknown as ts.Statement;
70
- }
71
-
72
- const visitor = (node: ts.Node): ts.Node => {
73
- if (ts.isForStatement(node)) {
74
- return rewriteForLoop(node);
75
- }
76
- return ts.visitEachChild(node, visitor, ctx);
77
- };
78
-
79
- return ts.visitEachChild(sourceFile, visitor, ctx) as ts.SourceFile;
80
- }