@elliots/typical 0.1.9 → 0.2.0-beta.1

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.
Files changed (55) hide show
  1. package/README.md +232 -96
  2. package/dist/src/cli.js +14 -60
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/cli.typical.ts +136 -0
  5. package/dist/src/config.d.ts +56 -0
  6. package/dist/src/config.js +124 -32
  7. package/dist/src/config.js.map +1 -1
  8. package/dist/src/config.typical.ts +287 -0
  9. package/dist/src/esm-loader-register.js.map +1 -1
  10. package/dist/src/esm-loader.d.ts +1 -0
  11. package/dist/src/esm-loader.js +31 -8
  12. package/dist/src/esm-loader.js.map +1 -1
  13. package/dist/src/file-filter.d.ts +1 -1
  14. package/dist/src/file-filter.js.map +1 -1
  15. package/dist/src/index.d.ts +4 -1
  16. package/dist/src/index.js +2 -1
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/program-manager.d.ts +27 -0
  19. package/dist/src/program-manager.js +121 -0
  20. package/dist/src/program-manager.js.map +1 -0
  21. package/dist/src/regex-hoister.d.ts +1 -1
  22. package/dist/src/regex-hoister.js +13 -19
  23. package/dist/src/regex-hoister.js.map +1 -1
  24. package/dist/src/setup.d.ts +1 -1
  25. package/dist/src/setup.js +3 -3
  26. package/dist/src/setup.js.map +1 -1
  27. package/dist/src/source-map.d.ts +78 -0
  28. package/dist/src/source-map.js +133 -0
  29. package/dist/src/source-map.js.map +1 -0
  30. package/dist/src/source-map.typical.ts +216 -0
  31. package/dist/src/timing.d.ts +19 -0
  32. package/dist/src/timing.js +65 -0
  33. package/dist/src/timing.js.map +1 -0
  34. package/dist/src/transformer.d.ts +28 -126
  35. package/dist/src/transformer.js +44 -1477
  36. package/dist/src/transformer.js.map +1 -1
  37. package/dist/src/transformer.typical.ts +2552 -0
  38. package/dist/src/tsc-plugin.d.ts +8 -1
  39. package/dist/src/tsc-plugin.js +11 -7
  40. package/dist/src/tsc-plugin.js.map +1 -1
  41. package/package.json +54 -44
  42. package/src/cli.ts +45 -98
  43. package/src/config.ts +200 -57
  44. package/src/esm-loader-register.ts +2 -2
  45. package/src/esm-loader.ts +46 -19
  46. package/src/index.ts +5 -2
  47. package/src/patch-fs.cjs +14 -14
  48. package/src/timing.ts +74 -0
  49. package/src/transformer.ts +52 -1969
  50. package/bin/ttsc +0 -12
  51. package/src/file-filter.ts +0 -49
  52. package/src/patch-tsconfig.cjs +0 -52
  53. package/src/regex-hoister.ts +0 -203
  54. package/src/setup.ts +0 -39
  55. package/src/tsc-plugin.ts +0 -12
@@ -1,1995 +1,78 @@
1
- import ts from "typescript";
2
- import fs from "fs";
3
- import path from "path";
4
- import { loadConfig, TypicalConfig, DOM_TYPES_TO_IGNORE } from "./config.js";
5
- import { shouldTransformFile } from "./file-filter.js";
6
- import { hoistRegexConstructors } from "./regex-hoister.js";
7
-
8
- import { transform as typiaTransform } from "typia/lib/transform.js";
9
- import { setupTsProgram } from "./setup.js";
10
-
11
- // Flags for typeToTypeNode to prefer type aliases over import() syntax
12
- const TYPE_NODE_FLAGS = ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
13
-
14
- export interface TransformContext {
15
- ts: typeof ts;
16
- factory: ts.NodeFactory;
17
- context: ts.TransformationContext;
1
+ /**
2
+ * TypicalTransformer - Thin wrapper around the Go compiler.
3
+ *
4
+ * The Go compiler (compiler) handles all TypeScript analysis
5
+ * and validation code generation. This class just manages the lifecycle
6
+ * and communication with the Go process.
7
+ */
8
+
9
+ import { resolve } from 'path'
10
+ import { TypicalCompiler, type ProjectHandle, type RawSourceMap } from '@elliots/typical-compiler'
11
+ import type { TypicalConfig } from './config.js'
12
+ import { loadConfig } from './config.js'
13
+
14
+ export interface TransformResult {
15
+ code: string
16
+ map: RawSourceMap | null
18
17
  }
19
18
 
20
19
  export class TypicalTransformer {
21
- public config: TypicalConfig;
22
- private program: ts.Program;
23
- private ts: typeof ts;
24
- private typeValidators = new Map<
25
- string,
26
- { name: string; typeNode: ts.TypeNode }
27
- >(); // type -> { validator variable name, type node }
28
- private typeStringifiers = new Map<
29
- string,
30
- { name: string; typeNode: ts.TypeNode }
31
- >(); // type -> { stringifier variable name, type node }
32
- private typeParsers = new Map<
33
- string,
34
- { name: string; typeNode: ts.TypeNode }
35
- >(); // type -> { parser variable name, type node }
20
+ public config: TypicalConfig
21
+ private compiler: TypicalCompiler
22
+ private projectHandle: ProjectHandle | null = null
23
+ private initPromise: Promise<void> | null = null
24
+ private configFile: string
36
25
 
37
- constructor(
38
- config?: TypicalConfig,
39
- program?: ts.Program,
40
- tsInstance?: typeof ts
41
- ) {
42
- this.config = config ?? loadConfig();
43
- this.ts = tsInstance ?? ts;
44
- this.program = program ?? setupTsProgram(this.ts);
45
- }
46
-
47
- public createSourceFile(fileName: string, content: string): ts.SourceFile {
48
- return this.ts.createSourceFile(
49
- fileName,
50
- content,
51
- this.ts.ScriptTarget.ES2020,
52
- true
53
- );
54
- }
55
-
56
- public transform(
57
- sourceFile: ts.SourceFile | string,
58
- mode: "basic" | "typia" | "js",
59
- skippedTypes: Set<string> = new Set()
60
- ): string {
61
- if (typeof sourceFile === "string") {
62
- const file = this.program.getSourceFile(sourceFile);
63
- if (!file) {
64
- throw new Error(`Source file not found in program: ${sourceFile}`);
65
- }
66
- sourceFile = file;
67
- }
68
-
69
- const printer = this.ts.createPrinter();
70
-
71
- // Phase 1: typical's own transformations only (no typia)
72
- const typicalTransformer = this.getTypicalOnlyTransformer(skippedTypes);
73
- const phase1Result = this.ts.transform(sourceFile, [typicalTransformer]);
74
- let transformedCode = printer.printFile(phase1Result.transformed[0]);
75
- phase1Result.dispose();
76
-
77
- if (mode === "basic") {
78
- return transformedCode;
79
- }
80
-
81
- // Phase 2: if code has typia calls, run typia transformer in its own context
82
- if (transformedCode.includes("typia.")) {
83
- const result = this.applyTypiaTransform(sourceFile.fileName, transformedCode, printer);
84
- if (typeof result === 'object' && result.retry) {
85
- // Typia failed on a type - add to skipped and retry the whole transform
86
- skippedTypes.add(result.failedType);
87
- // Clear validator caches since we're retrying
88
- this.typeValidators.clear();
89
- this.typeStringifiers.clear();
90
- this.typeParsers.clear();
91
- return this.transform(sourceFile, mode, skippedTypes);
92
- }
93
- transformedCode = result as string;
94
- }
95
-
96
- if (mode === "typia") {
97
- return transformedCode;
98
- }
99
-
100
- // Mode "js" - transpile to JavaScript
101
- const compileResult = ts.transpileModule(transformedCode, {
102
- compilerOptions: this.program.getCompilerOptions(),
103
- });
104
-
105
- return compileResult.outputText;
26
+ constructor(config?: TypicalConfig, configFile: string = 'tsconfig.json') {
27
+ this.config = config ?? loadConfig()
28
+ this.configFile = configFile
29
+ this.compiler = new TypicalCompiler({ cwd: process.cwd() })
106
30
  }
107
31
 
108
32
  /**
109
- * Apply typia transformation in a separate ts.transform() context.
110
- * This avoids mixing program contexts and eliminates the need for import recreation.
111
- * Returns either the transformed code string, or a retry signal with the failed type.
33
+ * Ensure the Go compiler is started and project is loaded.
34
+ * Uses lazy initialization - only starts on first transform.
112
35
  */
113
- private applyTypiaTransform(fileName: string, code: string, printer: ts.Printer): string | { retry: true; failedType: string } {
114
- // Write intermediate file if debug option is enabled
115
- if (this.config.debug?.writeIntermediateFiles) {
116
- const compilerOptions = this.program.getCompilerOptions();
117
- const outDir = compilerOptions.outDir || ".";
118
- const rootDir = compilerOptions.rootDir || ".";
119
-
120
- const relativePath = path.relative(rootDir, fileName);
121
- const intermediateFileName = relativePath.replace(/\.(tsx?)$/, ".typical.$1");
122
- const intermediateFilePath = path.join(outDir, intermediateFileName);
123
-
124
- const dir = path.dirname(intermediateFilePath);
125
- if (!fs.existsSync(dir)) {
126
- fs.mkdirSync(dir, { recursive: true });
127
- }
128
-
129
- fs.writeFileSync(intermediateFilePath, code);
130
- console.log(`TYPICAL: Wrote intermediate file: ${intermediateFilePath}`);
131
- }
132
-
133
- if (process.env.DEBUG) {
134
- console.log("TYPICAL: Before typia transform (first 500 chars):", code.substring(0, 500));
135
- }
136
-
137
- // Create a new source file from the transformed code
138
- const newSourceFile = this.ts.createSourceFile(
139
- fileName,
140
- code,
141
- this.ts.ScriptTarget.ES2020,
142
- true
143
- );
144
-
145
- // Create a new program with the transformed source file so typia can resolve types.
146
- // Pass oldProgram to reuse parsed/bound data from unchanged dependency files.
147
- const compilerOptions = this.program.getCompilerOptions();
148
- const originalSourceFiles = new Map<string, ts.SourceFile>();
149
- for (const sf of this.program.getSourceFiles()) {
150
- originalSourceFiles.set(sf.fileName, sf);
151
- }
152
- // Replace the original source file with our transformed one
153
- originalSourceFiles.set(fileName, newSourceFile);
154
-
155
- const customHost: ts.CompilerHost = {
156
- getSourceFile: (hostFileName, languageVersion) => {
157
- if (originalSourceFiles.has(hostFileName)) {
158
- return originalSourceFiles.get(hostFileName);
159
- }
160
- return this.ts.createSourceFile(
161
- hostFileName,
162
- this.ts.sys.readFile(hostFileName) || "",
163
- languageVersion,
164
- true
165
- );
166
- },
167
- getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
168
- writeFile: () => {},
169
- getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
170
- getCanonicalFileName: (fn) =>
171
- this.ts.sys.useCaseSensitiveFileNames ? fn : fn.toLowerCase(),
172
- useCaseSensitiveFileNames: () => this.ts.sys.useCaseSensitiveFileNames,
173
- getNewLine: () => this.ts.sys.newLine,
174
- fileExists: (fn) => originalSourceFiles.has(fn) || this.ts.sys.fileExists(fn),
175
- readFile: (fn) => this.ts.sys.readFile(fn),
176
- };
177
-
178
- // Create new program, passing oldProgram to reuse dependency context
179
- const newProgram = this.ts.createProgram(
180
- Array.from(originalSourceFiles.keys()),
181
- compilerOptions,
182
- customHost,
183
- this.program // Reuse old program's structure for unchanged files
184
- );
185
-
186
- // Get the bound source file from the new program (has proper symbol tables)
187
- const boundSourceFile = newProgram.getSourceFile(fileName);
188
- if (!boundSourceFile) {
189
- throw new Error(`Failed to get bound source file: ${fileName}`);
190
- }
191
-
192
- // Collect typia diagnostics to detect transformation failures
193
- const diagnostics: ts.Diagnostic[] = [];
194
-
195
- // Create typia transformer with the new program
196
- const typiaTransformerFactory = typiaTransform(
197
- newProgram,
198
- {},
199
- {
200
- addDiagnostic(diag: ts.Diagnostic) {
201
- diagnostics.push(diag);
202
- if (process.env.DEBUG) {
203
- console.warn("Typia diagnostic:", diag);
204
- }
205
- return diagnostics.length - 1;
206
- },
207
- }
208
- );
209
-
210
- // Run typia's transformer in its own ts.transform() call
211
- const typiaResult = this.ts.transform(boundSourceFile, [typiaTransformerFactory]);
212
- let typiaTransformed = typiaResult.transformed[0];
213
- typiaResult.dispose();
214
-
215
- if (process.env.DEBUG) {
216
- const afterTypia = printer.printFile(typiaTransformed);
217
- console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
218
- }
219
-
220
- // Check for typia errors via diagnostics
221
- const errors = diagnostics.filter(d => d.category === this.ts.DiagnosticCategory.Error);
222
- if (errors.length > 0) {
223
- // Check if any error is due to Window/globalThis intersection (DOM types)
224
- for (const d of errors) {
225
- const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
226
- if (fullMessage.includes('Window & typeof globalThis') || fullMessage.includes('typeof globalThis')) {
227
- // Find the validator that failed - look for the type in the error
228
- // Error format: "Code: typia.createAssert<{ value: number; table: Table; }>()"
229
- if (d.file && d.start !== undefined && d.length !== undefined) {
230
- const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
231
- // Extract the type from typia.createAssert<TYPE>()
232
- const typeMatch = sourceSnippet.match(/typia\.\w+<([^>]+(?:<[^>]*>)*)>\(\)/);
233
- if (typeMatch) {
234
- const failedType = typeMatch[1].trim();
235
- console.warn(`TYPICAL: Skipping validation for type due to Window/globalThis (typia cannot process DOM types): ${failedType.substring(0, 100)}...`);
236
-
237
- // Add to ignored types and signal retry needed
238
- return { retry: true, failedType };
239
- }
240
- }
241
- }
242
- }
243
-
244
- // No retryable errors, throw the original error
245
- const errorMessages = errors.map(d => {
246
- const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
247
-
248
- if (d.file && d.start !== undefined && d.length !== undefined) {
249
- const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
250
- // Extract the actual source code that caused the error
251
- const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
252
- // Truncate long snippets
253
- const snippet = sourceSnippet.length > 100
254
- ? sourceSnippet.substring(0, 100) + '...'
255
- : sourceSnippet;
256
-
257
- // Format the error message - extract type issues from typia's verbose output
258
- const formattedIssues = this.formatTypiaError(fullMessage);
259
-
260
- return `${d.file.fileName}:${line + 1}:${character + 1}\n` +
261
- ` Code: ${snippet}\n` +
262
- formattedIssues;
263
- }
264
- return this.formatTypiaError(fullMessage);
265
- });
266
- throw new Error(
267
- `TYPICAL: Typia transformation failed:\n\n${errorMessages.join('\n\n')}`
268
- );
36
+ private async ensureInitialized(): Promise<void> {
37
+ if (!this.initPromise) {
38
+ this.initPromise = (async () => {
39
+ await this.compiler.start()
40
+ this.projectHandle = await this.compiler.loadProject(this.configFile)
41
+ })()
269
42
  }
270
-
271
- // Hoist RegExp constructors to top-level constants for performance
272
- if (this.config.hoistRegex !== false) {
273
- // Need to run hoisting in a transform context
274
- const hoistResult = this.ts.transform(typiaTransformed, [
275
- (context) => (sf) => hoistRegexConstructors(sf, this.ts, context.factory)
276
- ]);
277
- typiaTransformed = hoistResult.transformed[0];
278
- hoistResult.dispose();
279
- }
280
-
281
- const finalCode = printer.printFile(typiaTransformed);
282
-
283
- // Also check for untransformed typia calls as a fallback
284
- // (in case typia silently fails without reporting a diagnostic)
285
- const untransformedCalls = this.findUntransformedTypiaCalls(finalCode);
286
- if (untransformedCalls.length > 0) {
287
- const failedTypes = untransformedCalls.map(c => c.type).filter((v, i, a) => a.indexOf(v) === i);
288
- throw new Error(
289
- `TYPICAL: Failed to transform the following types (typia cannot process them):\n` +
290
- failedTypes.map(t => ` - ${t}`).join('\n') +
291
- `\n\nTo skip validation for these types, add to ignoreTypes in typical.json:\n` +
292
- ` "ignoreTypes": [${failedTypes.map(t => `"${t}"`).join(', ')}]` +
293
- `\n\nFile: ${fileName}`
294
- );
295
- }
296
-
297
- return finalCode;
43
+ await this.initPromise
298
44
  }
299
45
 
300
46
  /**
301
- * Get a transformer that only applies typical's transformations (no typia).
302
- * @param skippedTypes Set of type strings to skip validation for (used for retry after typia errors)
303
- */
304
- private getTypicalOnlyTransformer(skippedTypes: Set<string> = new Set()): ts.TransformerFactory<ts.SourceFile> {
305
- return (context: ts.TransformationContext) => {
306
- const factory = context.factory;
307
- const typeChecker = this.program.getTypeChecker();
308
- const transformContext: TransformContext = {
309
- ts: this.ts,
310
- factory,
311
- context,
312
- };
313
-
314
- return (sourceFile: ts.SourceFile) => {
315
- // Check if this file should be transformed based on include/exclude patterns
316
- if (!this.shouldTransformFile(sourceFile.fileName)) {
317
- return sourceFile;
318
- }
319
-
320
- if (process.env.DEBUG) {
321
- console.log("TYPICAL: processing ", sourceFile.fileName);
322
- }
323
-
324
- return this.transformSourceFile(sourceFile, transformContext, typeChecker, skippedTypes);
325
- };
326
- };
327
- }
328
-
329
- /**
330
- * Get a combined transformer for use with ts-patch/ttsc.
331
- * This is used by the TSC plugin where we need a single transformer factory.
47
+ * Transform a TypeScript file by adding runtime validation.
332
48
  *
333
- * Note: Even for ts-patch, we need to create a new program with the transformed
334
- * source so typia can resolve the types from our generated typia.createAssert<T>() calls.
335
- */
336
- public getTransformer(withTypia: boolean): ts.TransformerFactory<ts.SourceFile> {
337
- return (context: ts.TransformationContext) => {
338
- const factory = context.factory;
339
- const typeChecker = this.program.getTypeChecker();
340
- const transformContext: TransformContext = {
341
- ts: this.ts,
342
- factory,
343
- context,
344
- };
345
-
346
- return (sourceFile: ts.SourceFile) => {
347
- // Check if this file should be transformed based on include/exclude patterns
348
- if (!this.shouldTransformFile(sourceFile.fileName)) {
349
- return sourceFile;
350
- }
351
-
352
- if (process.env.DEBUG) {
353
- console.log("TYPICAL: processing ", sourceFile.fileName);
354
- }
355
-
356
- // Apply typical's transformations
357
- let transformedSourceFile = this.transformSourceFile(
358
- sourceFile,
359
- transformContext,
360
- typeChecker
361
- );
362
-
363
- if (!withTypia) {
364
- return transformedSourceFile;
365
- }
366
-
367
- // Print the transformed code to check for typia calls
368
- const printer = this.ts.createPrinter();
369
- const transformedCode = printer.printFile(transformedSourceFile);
370
-
371
- if (!transformedCode.includes("typia.")) {
372
- return transformedSourceFile;
373
- }
374
-
375
- // Write intermediate file if debug option is enabled
376
- if (this.config.debug?.writeIntermediateFiles) {
377
- const compilerOptions = this.program.getCompilerOptions();
378
- const outDir = compilerOptions.outDir || ".";
379
- const rootDir = compilerOptions.rootDir || ".";
380
- const relativePath = path.relative(rootDir, sourceFile.fileName);
381
- const intermediateFileName = relativePath.replace(/\.(tsx?)$/, ".typical.$1");
382
- const intermediateFilePath = path.join(outDir, intermediateFileName);
383
- const dir = path.dirname(intermediateFilePath);
384
- if (!fs.existsSync(dir)) {
385
- fs.mkdirSync(dir, { recursive: true });
386
- }
387
- fs.writeFileSync(intermediateFilePath, transformedCode);
388
- console.log(`TYPICAL: Wrote intermediate file: ${intermediateFilePath}`);
389
- }
390
-
391
- if (process.env.DEBUG) {
392
- console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
393
- }
394
-
395
- // Create a new source file from the transformed code
396
- const newSourceFile = this.ts.createSourceFile(
397
- sourceFile.fileName,
398
- transformedCode,
399
- sourceFile.languageVersion,
400
- true
401
- );
402
-
403
- // Create a new program with the transformed source file so typia can resolve types.
404
- // Pass oldProgram to reuse parsed/bound data from unchanged dependency files.
405
- const compilerOptions = this.program.getCompilerOptions();
406
- const originalSourceFiles = new Map<string, ts.SourceFile>();
407
- for (const sf of this.program.getSourceFiles()) {
408
- originalSourceFiles.set(sf.fileName, sf);
409
- }
410
- // Replace the original source file with our transformed one
411
- originalSourceFiles.set(sourceFile.fileName, newSourceFile);
412
-
413
- const customHost: ts.CompilerHost = {
414
- getSourceFile: (hostFileName, languageVersion) => {
415
- if (originalSourceFiles.has(hostFileName)) {
416
- return originalSourceFiles.get(hostFileName);
417
- }
418
- return this.ts.createSourceFile(
419
- hostFileName,
420
- this.ts.sys.readFile(hostFileName) || "",
421
- languageVersion,
422
- true
423
- );
424
- },
425
- getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
426
- writeFile: () => {},
427
- getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
428
- getCanonicalFileName: (fn) =>
429
- this.ts.sys.useCaseSensitiveFileNames ? fn : fn.toLowerCase(),
430
- useCaseSensitiveFileNames: () => this.ts.sys.useCaseSensitiveFileNames,
431
- getNewLine: () => this.ts.sys.newLine,
432
- fileExists: (fn) => originalSourceFiles.has(fn) || this.ts.sys.fileExists(fn),
433
- readFile: (fn) => this.ts.sys.readFile(fn),
434
- };
435
-
436
- // Create new program, passing oldProgram to reuse dependency context
437
- const newProgram = this.ts.createProgram(
438
- Array.from(originalSourceFiles.keys()),
439
- compilerOptions,
440
- customHost,
441
- this.program // Reuse old program's structure for unchanged files
442
- );
443
-
444
- // Get the bound source file from the new program (has proper symbol tables)
445
- const boundSourceFile = newProgram.getSourceFile(sourceFile.fileName);
446
- if (!boundSourceFile) {
447
- throw new Error(`Failed to get bound source file: ${sourceFile.fileName}`);
448
- }
449
-
450
- // Collect typia diagnostics to detect transformation failures
451
- const diagnostics: ts.Diagnostic[] = [];
452
-
453
- // Create typia transformer with the new program
454
- const typiaTransformerFactory = typiaTransform(
455
- newProgram,
456
- {},
457
- {
458
- addDiagnostic(diag: ts.Diagnostic) {
459
- diagnostics.push(diag);
460
- if (process.env.DEBUG) {
461
- console.warn("Typia diagnostic:", diag);
462
- }
463
- return diagnostics.length - 1;
464
- },
465
- }
466
- );
467
-
468
- // Apply typia's transformer to the bound source file
469
- const typiaNodeTransformer = typiaTransformerFactory(context);
470
- transformedSourceFile = typiaNodeTransformer(boundSourceFile);
471
-
472
- if (process.env.DEBUG) {
473
- const afterTypia = printer.printFile(transformedSourceFile);
474
- console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
475
- }
476
-
477
- // Check for typia errors via diagnostics
478
- const errors = diagnostics.filter(d => d.category === this.ts.DiagnosticCategory.Error);
479
- if (errors.length > 0) {
480
- const errorMessages = errors.map(d => {
481
- const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
482
-
483
- if (d.file && d.start !== undefined && d.length !== undefined) {
484
- const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
485
- // Extract the actual source code that caused the error
486
- const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
487
- // Truncate long snippets
488
- const snippet = sourceSnippet.length > 100
489
- ? sourceSnippet.substring(0, 100) + '...'
490
- : sourceSnippet;
491
-
492
- // Format the error message - extract type issues from typia's verbose output
493
- const formattedIssues = this.formatTypiaError(fullMessage);
494
-
495
- return `${d.file.fileName}:${line + 1}:${character + 1}\n` +
496
- ` Code: ${snippet}\n` +
497
- formattedIssues;
498
- }
499
- return this.formatTypiaError(fullMessage);
500
- });
501
- throw new Error(
502
- `TYPICAL: Typia transformation failed:\n\n${errorMessages.join('\n\n')}`
503
- );
504
- }
505
-
506
- // Hoist RegExp constructors to top-level constants for performance
507
- if (this.config.hoistRegex !== false) {
508
- transformedSourceFile = hoistRegexConstructors(
509
- transformedSourceFile,
510
- this.ts,
511
- factory
512
- );
513
- }
514
-
515
- // Also check for untransformed typia calls as a fallback
516
- const finalCode = printer.printFile(transformedSourceFile);
517
- const untransformedCalls = this.findUntransformedTypiaCalls(finalCode);
518
- if (untransformedCalls.length > 0) {
519
- const failedTypes = untransformedCalls.map(c => c.type).filter((v, i, a) => a.indexOf(v) === i);
520
- throw new Error(
521
- `TYPICAL: Failed to transform the following types (typia cannot process them):\n` +
522
- failedTypes.map(t => ` - ${t}`).join('\n') +
523
- `\n\nTo skip validation for these types, add to ignoreTypes in typical.json:\n` +
524
- ` "ignoreTypes": [${failedTypes.map(t => `"${t}"`).join(', ')}]` +
525
- `\n\nFile: ${sourceFile.fileName}`
526
- );
527
- }
528
-
529
- return transformedSourceFile;
530
- };
531
- };
532
- }
533
-
534
- /**
535
- * Transform a single source file with TypeScript AST
536
- */
537
- private transformSourceFile(
538
- sourceFile: ts.SourceFile,
539
- ctx: TransformContext,
540
- typeChecker: ts.TypeChecker,
541
- skippedTypes: Set<string> = new Set()
542
- ): ts.SourceFile {
543
- const { ts } = ctx;
544
-
545
- // Helper to check if a type should be skipped (failed in typia on previous attempt)
546
- const shouldSkipType = (typeText: string): boolean => {
547
- if (skippedTypes.size === 0) return false;
548
- // Normalize: remove all whitespace and semicolons for comparison
549
- const normalize = (s: string) => s.replace(/[\s;]+/g, '').toLowerCase();
550
- const normalized = normalize(typeText);
551
- for (const skipped of skippedTypes) {
552
- const skippedNormalized = normalize(skipped);
553
- if (normalized === skippedNormalized || normalized.includes(skippedNormalized) || skippedNormalized.includes(normalized)) {
554
- if (process.env.DEBUG) {
555
- console.log(`TYPICAL: Matched skipped type: "${typeText.substring(0, 50)}..." matches "${skipped.substring(0, 50)}..."`);
556
- }
557
- return true;
558
- }
559
- }
560
- return false;
561
- };
562
-
563
- if (!sourceFile.fileName.includes('transformer.test.ts')) {
564
- // Check if this file has already been transformed by us
565
- const sourceText = sourceFile.getFullText();
566
- if (sourceText.includes('__typical_' + 'assert_') || sourceText.includes('__typical_' + 'stringify_') || sourceText.includes('__typical_' + 'parse_')) {
567
- throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
568
- }
569
- }
570
-
571
- // Reset caches for each file
572
- this.typeValidators.clear();
573
- this.typeStringifiers.clear();
574
- this.typeParsers.clear();
575
-
576
- let needsTypiaImport = false;
577
-
578
- const visit = (node: ts.Node): ts.Node => {
579
- // Transform JSON calls first (before they get wrapped in functions)
580
- if (
581
- ts.isCallExpression(node) &&
582
- ts.isPropertyAccessExpression(node.expression)
583
- ) {
584
- const propertyAccess = node.expression;
585
- if (
586
- ts.isIdentifier(propertyAccess.expression) &&
587
- propertyAccess.expression.text === "JSON"
588
- ) {
589
- needsTypiaImport = true;
590
-
591
- if (propertyAccess.name.text === "stringify") {
592
- // For stringify, we need to infer the type from the argument
593
- // First check if the argument type is 'any' - if so, skip transformation
594
- if (node.arguments.length > 0) {
595
- const arg = node.arguments[0];
596
- const argType = typeChecker.getTypeAtLocation(arg);
597
- if (this.isAnyOrUnknownTypeFlags(argType)) {
598
- return node; // Don't transform JSON.stringify for any/unknown types
599
- }
600
- }
601
-
602
- if (this.config.reusableValidators) {
603
- // Infer type from argument
604
- const arg = node.arguments[0];
605
- const { typeText, typeNode } = this.inferStringifyType(arg, typeChecker, ctx);
606
-
607
- const stringifierName = this.getOrCreateStringifier(typeText, typeNode);
608
- return ctx.factory.createCallExpression(
609
- ctx.factory.createIdentifier(stringifierName),
610
- undefined,
611
- node.arguments
612
- );
613
- } else {
614
- // Use inline typia.json.stringify
615
- return ctx.factory.updateCallExpression(
616
- node,
617
- ctx.factory.createPropertyAccessExpression(
618
- ctx.factory.createPropertyAccessExpression(
619
- ctx.factory.createIdentifier("typia"),
620
- "json"
621
- ),
622
- "stringify"
623
- ),
624
- node.typeArguments,
625
- node.arguments
626
- );
627
- }
628
- } else if (propertyAccess.name.text === "parse") {
629
- // For JSON.parse, we need to infer the expected type from context
630
- // Check if this is part of a variable declaration or type assertion
631
- let targetType: ts.TypeNode | undefined;
632
-
633
- // Look for type annotations in parent nodes
634
- let parent = node.parent;
635
- while (parent) {
636
- if (ts.isVariableDeclaration(parent) && parent.type) {
637
- targetType = parent.type;
638
- break;
639
- } else if (ts.isAsExpression(parent)) {
640
- targetType = parent.type;
641
- break;
642
- } else if (ts.isReturnStatement(parent)) {
643
- // Look for function return type
644
- let funcParent = parent.parent;
645
- while (funcParent) {
646
- if (
647
- (ts.isFunctionDeclaration(funcParent) ||
648
- ts.isArrowFunction(funcParent) ||
649
- ts.isMethodDeclaration(funcParent)) &&
650
- funcParent.type
651
- ) {
652
- targetType = funcParent.type;
653
- break;
654
- }
655
- funcParent = funcParent.parent;
656
- }
657
- break;
658
- } else if (ts.isArrowFunction(parent) && parent.type) {
659
- // Arrow function with expression body (not block)
660
- // e.g., (s: string): User => JSON.parse(s)
661
- targetType = parent.type;
662
- break;
663
- }
664
- parent = parent.parent;
665
- }
666
-
667
- if (targetType && this.isAnyOrUnknownType(targetType)) {
668
- // Don't transform JSON.parse for any/unknown types
669
- return node;
670
- }
671
-
672
- // If we can't determine the target type and there's no explicit type argument,
673
- // don't transform - we can't validate against an unknown type
674
- if (!targetType && !node.typeArguments) {
675
- return node;
676
- }
677
-
678
- if (this.config.reusableValidators && targetType) {
679
- // Use reusable parser - use typeNode text to preserve local aliases
680
- const typeText = this.getTypeKey(targetType, typeChecker);
681
- const parserName = this.getOrCreateParser(typeText, targetType);
682
-
683
- const newCall = ctx.factory.createCallExpression(
684
- ctx.factory.createIdentifier(parserName),
685
- undefined,
686
- node.arguments
687
- );
688
-
689
- return newCall;
690
- } else {
691
- // Use inline typia.json.assertParse
692
- const typeArguments = targetType
693
- ? [targetType]
694
- : node.typeArguments;
695
-
696
- return ctx.factory.updateCallExpression(
697
- node,
698
- ctx.factory.createPropertyAccessExpression(
699
- ctx.factory.createPropertyAccessExpression(
700
- ctx.factory.createIdentifier("typia"),
701
- "json"
702
- ),
703
- "assertParse"
704
- ),
705
- typeArguments,
706
- node.arguments
707
- );
708
- }
709
- }
710
- }
711
- }
712
-
713
- // Transform type assertions (as expressions) when validateCasts is enabled
714
- // e.g., `obj as User` becomes `__typical_assert_N(obj)`
715
- if (this.config.validateCasts && ts.isAsExpression(node)) {
716
- const targetType = node.type;
717
-
718
- // Skip 'as any' and 'as unknown' casts - these are intentional escapes
719
- if (this.isAnyOrUnknownType(targetType)) {
720
- return ctx.ts.visitEachChild(node, visit, ctx.context);
721
- }
722
-
723
- // Skip primitive types - no runtime validation needed
724
- if (targetType.kind === ts.SyntaxKind.StringKeyword ||
725
- targetType.kind === ts.SyntaxKind.NumberKeyword ||
726
- targetType.kind === ts.SyntaxKind.BooleanKeyword) {
727
- return ctx.ts.visitEachChild(node, visit, ctx.context);
728
- }
729
-
730
- // Skip types matching ignoreTypes patterns (including classes extending DOM types)
731
- const typeText = this.getTypeKey(targetType, typeChecker);
732
- const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
733
- if (this.isIgnoredType(typeText, typeChecker, targetTypeObj)) {
734
- if (process.env.DEBUG) {
735
- console.log(`TYPICAL: Skipping ignored type for cast: ${typeText}`);
736
- }
737
- return ctx.ts.visitEachChild(node, visit, ctx.context);
738
- }
739
-
740
- // Skip types that failed in typia (retry mechanism)
741
- if (shouldSkipType(typeText)) {
742
- if (process.env.DEBUG) {
743
- console.log(`TYPICAL: Skipping previously failed type for cast: ${typeText}`);
744
- }
745
- return ctx.ts.visitEachChild(node, visit, ctx.context);
746
- }
747
-
748
- needsTypiaImport = true;
749
-
750
- // Visit the expression first to transform any nested casts
751
- const visitedExpression = ctx.ts.visitNode(node.expression, visit) as ts.Expression;
752
-
753
- if (this.config.reusableValidators) {
754
- // Use typeNode text to preserve local aliases
755
- const typeText = this.getTypeKey(targetType, typeChecker);
756
- const validatorName = this.getOrCreateValidator(typeText, targetType);
757
-
758
- // Replace `expr as Type` with `__typical_assert_N(expr)`
759
- return ctx.factory.createCallExpression(
760
- ctx.factory.createIdentifier(validatorName),
761
- undefined,
762
- [visitedExpression]
763
- );
764
- } else {
765
- // Inline validator: typia.assert<Type>(expr)
766
- return ctx.factory.createCallExpression(
767
- ctx.factory.createPropertyAccessExpression(
768
- ctx.factory.createIdentifier("typia"),
769
- "assert"
770
- ),
771
- [targetType],
772
- [visitedExpression]
773
- );
774
- }
775
- }
776
-
777
- // Transform function declarations
778
- if (ts.isFunctionDeclaration(node)) {
779
- needsTypiaImport = true;
780
- return transformFunction(node);
781
- }
782
-
783
- // Transform arrow functions
784
- if (ts.isArrowFunction(node)) {
785
- needsTypiaImport = true;
786
- return transformFunction(node);
787
- }
788
-
789
- // Transform method declarations
790
- if (ts.isMethodDeclaration(node)) {
791
- needsTypiaImport = true;
792
- return transformFunction(node);
793
- }
794
-
795
- return ctx.ts.visitEachChild(node, visit, ctx.context);
796
- };
797
-
798
- const transformFunction = (
799
- func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration
800
- ): ts.Node => {
801
- const body = func.body;
802
-
803
- // For arrow functions with expression bodies (not blocks),
804
- // still visit the expression to transform JSON calls etc.
805
- // Also handle Promise return types with .then() validation
806
- if (body && !ts.isBlock(body) && ts.isArrowFunction(func)) {
807
- let visitedBody = ctx.ts.visitNode(body, visit) as ts.Expression;
808
-
809
- // Check if this is a non-async function with Promise return type
810
- let returnType = func.type;
811
- let returnTypeForString: ts.Type | undefined;
812
-
813
- if (returnType) {
814
- returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
815
- } else {
816
- // Try to infer the return type from the signature
817
- try {
818
- const signature = typeChecker.getSignatureFromDeclaration(func);
819
- if (signature) {
820
- const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
821
- returnType = typeChecker.typeToTypeNode(
822
- inferredReturnType,
823
- func,
824
- TYPE_NODE_FLAGS
825
- );
826
- returnTypeForString = inferredReturnType;
827
- }
828
- } catch {
829
- // Skip inference
830
- }
831
- }
832
-
833
- // Check for Promise<T> return type
834
- if (returnType && returnTypeForString) {
835
- const promiseSymbol = returnTypeForString.getSymbol();
836
- if (promiseSymbol && promiseSymbol.getName() === "Promise") {
837
- const typeArgs = (returnTypeForString as ts.TypeReference).typeArguments;
838
- if (typeArgs && typeArgs.length > 0) {
839
- const innerType = typeArgs[0];
840
- let innerTypeNode: ts.TypeNode | undefined;
841
-
842
- if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
843
- innerTypeNode = returnType.typeArguments[0];
844
- } else {
845
- innerTypeNode = typeChecker.typeToTypeNode(
846
- innerType,
847
- func,
848
- TYPE_NODE_FLAGS
849
- );
850
- }
851
-
852
- // Only add validation if validateFunctions is enabled
853
- if (this.config.validateFunctions !== false && innerTypeNode && !this.isAnyOrUnknownType(innerTypeNode)) {
854
- const innerTypeText = this.getTypeKey(innerTypeNode, typeChecker, innerType);
855
- if (!this.isIgnoredType(innerTypeText, typeChecker, innerType) && !shouldSkipType(innerTypeText)) {
856
- // Wrap expression with .then(validator)
857
- const validatorName = this.config.reusableValidators
858
- ? this.getOrCreateValidator(innerTypeText, innerTypeNode)
859
- : null;
860
-
861
- if (validatorName) {
862
- needsTypiaImport = true;
863
- visitedBody = ctx.factory.createCallExpression(
864
- ctx.factory.createPropertyAccessExpression(
865
- visitedBody,
866
- ctx.factory.createIdentifier("then")
867
- ),
868
- undefined,
869
- [ctx.factory.createIdentifier(validatorName)]
870
- );
871
- }
872
- }
873
- }
874
- }
875
- }
876
- }
877
-
878
- if (visitedBody !== body) {
879
- return ctx.factory.updateArrowFunction(
880
- func,
881
- func.modifiers,
882
- func.typeParameters,
883
- func.parameters,
884
- func.type,
885
- func.equalsGreaterThanToken,
886
- visitedBody
887
- );
888
- }
889
- return func;
890
- }
891
-
892
- if (!body || !ts.isBlock(body)) return func;
893
-
894
- // Track validated variables (params and consts with type annotations)
895
- const validatedVariables = new Map<string, ts.Type>();
896
-
897
- // Add parameter validation (only if validateFunctions is enabled)
898
- const validationStatements: ts.Statement[] = [];
899
-
900
- // Skip parameter validation if validateFunctions is disabled
901
- const shouldValidateFunctions = this.config.validateFunctions !== false;
902
-
903
- func.parameters.forEach((param) => {
904
- if (shouldValidateFunctions && param.type) {
905
- // Skip 'any' and 'unknown' types - no point validating them
906
- if (this.isAnyOrUnknownType(param.type)) {
907
- return;
908
- }
909
-
910
- // Skip types matching ignoreTypes patterns (including classes extending DOM types)
911
- const typeText = this.getTypeKey(param.type, typeChecker);
912
- const paramType = typeChecker.getTypeFromTypeNode(param.type);
913
-
914
- // Skip type parameters (generics) - can't be validated at runtime
915
- if (this.isAnyOrUnknownTypeFlags(paramType)) {
916
- if (process.env.DEBUG) {
917
- console.log(`TYPICAL: Skipping type parameter/any for parameter: ${typeText}`);
918
- }
919
- return;
920
- }
921
-
922
- if (process.env.DEBUG) {
923
- console.log(`TYPICAL: Processing parameter type: ${typeText}`);
924
- }
925
- if (this.isIgnoredType(typeText, typeChecker, paramType)) {
926
- if (process.env.DEBUG) {
927
- console.log(`TYPICAL: Skipping ignored type for parameter: ${typeText}`);
928
- }
929
- return;
930
- }
931
-
932
- // Skip types that failed in typia (retry mechanism)
933
- if (shouldSkipType(typeText)) {
934
- if (process.env.DEBUG) {
935
- console.log(`TYPICAL: Skipping previously failed type for parameter: ${typeText}`);
936
- }
937
- return;
938
- }
939
-
940
- const paramName = ts.isIdentifier(param.name)
941
- ? param.name.text
942
- : "param";
943
- const paramIdentifier = ctx.factory.createIdentifier(paramName);
944
-
945
- // Track this parameter as validated for flow analysis
946
- validatedVariables.set(paramName, paramType);
947
-
948
- if (this.config.reusableValidators) {
949
- // Use reusable validators - use typeNode text to preserve local aliases
950
- const validatorName = this.getOrCreateValidator(
951
- typeText,
952
- param.type
953
- );
954
-
955
- const validatorCall = ctx.factory.createCallExpression(
956
- ctx.factory.createIdentifier(validatorName),
957
- undefined,
958
- [paramIdentifier]
959
- );
960
- const assertCall =
961
- ctx.factory.createExpressionStatement(validatorCall);
962
-
963
- validationStatements.push(assertCall);
964
- } else {
965
- // Use inline typia.assert calls
966
- const typiaIdentifier = ctx.factory.createIdentifier("typia");
967
- const assertIdentifier = ctx.factory.createIdentifier("assert");
968
- const propertyAccess = ctx.factory.createPropertyAccessExpression(
969
- typiaIdentifier,
970
- assertIdentifier
971
- );
972
- const callExpression = ctx.factory.createCallExpression(
973
- propertyAccess,
974
- [param.type],
975
- [paramIdentifier]
976
- );
977
- const assertCall =
978
- ctx.factory.createExpressionStatement(callExpression);
979
-
980
- validationStatements.push(assertCall);
981
- }
982
- }
983
- });
984
-
985
- // First visit all child nodes (including JSON calls) before adding validation
986
- const visitedBody = ctx.ts.visitNode(body, visit) as ts.Block;
987
-
988
- // Also track const declarations with type annotations as validated
989
- // (the assignment will be validated, and const can't be reassigned)
990
- const collectConstDeclarations = (node: ts.Node): void => {
991
- if (ts.isVariableStatement(node)) {
992
- const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
993
- if (isConst) {
994
- for (const decl of node.declarationList.declarations) {
995
- if (decl.type && ts.isIdentifier(decl.name)) {
996
- // Skip any/unknown types
997
- if (!this.isAnyOrUnknownType(decl.type)) {
998
- const constType = typeChecker.getTypeFromTypeNode(decl.type);
999
- validatedVariables.set(decl.name.text, constType);
1000
- }
1001
- }
1002
- }
1003
- }
1004
- }
1005
- ts.forEachChild(node, collectConstDeclarations);
1006
- };
1007
- collectConstDeclarations(visitedBody);
1008
-
1009
- // Transform return statements - use explicit type or infer from type checker
1010
- let transformedStatements = visitedBody.statements;
1011
- let returnType = func.type;
1012
-
1013
- // Check if this is an async function
1014
- const isAsync = func.modifiers?.some(
1015
- (mod) => mod.kind === ts.SyntaxKind.AsyncKeyword
1016
- );
1017
-
1018
- // If no explicit return type, try to infer it from the type checker
1019
- let returnTypeForString: ts.Type | undefined;
1020
- if (!returnType) {
1021
- try {
1022
- const signature = typeChecker.getSignatureFromDeclaration(func);
1023
- if (signature) {
1024
- const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
1025
- returnType = typeChecker.typeToTypeNode(
1026
- inferredReturnType,
1027
- func,
1028
- TYPE_NODE_FLAGS
1029
- );
1030
- returnTypeForString = inferredReturnType;
1031
- }
1032
- } catch {
1033
- // Could not infer signature (e.g., untyped arrow function callback)
1034
- // Skip return type validation for this function
1035
- }
1036
- } else {
1037
- // For explicit return types, get the Type from the TypeNode
1038
- returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
1039
- }
1040
-
1041
- // Handle Promise return types
1042
- // Track whether this is a non-async function returning Promise (needs .then() wrapper)
1043
- let isNonAsyncPromiseReturn = false;
1044
-
1045
- if (returnType && returnTypeForString) {
1046
- const promiseSymbol = returnTypeForString.getSymbol();
1047
- if (promiseSymbol && promiseSymbol.getName() === "Promise") {
1048
- // Unwrap Promise<T> to get T for validation
1049
- const typeArgs = (returnTypeForString as ts.TypeReference).typeArguments;
1050
- if (typeArgs && typeArgs.length > 0) {
1051
- returnTypeForString = typeArgs[0];
1052
- // Also update the TypeNode to match
1053
- if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
1054
- returnType = returnType.typeArguments[0];
1055
- } else {
1056
- // Create a new type node from the unwrapped type
1057
- returnType = typeChecker.typeToTypeNode(
1058
- returnTypeForString,
1059
- func,
1060
- TYPE_NODE_FLAGS
1061
- );
1062
- }
1063
-
1064
- if (!isAsync) {
1065
- // For non-async functions returning Promise, we'll use .then(validator)
1066
- isNonAsyncPromiseReturn = true;
1067
- if (process.env.DEBUG) {
1068
- console.log(`TYPICAL: Non-async Promise return type - will use .then() for validation`);
1069
- }
1070
- }
1071
- }
1072
- }
1073
- }
1074
-
1075
- // Skip 'any' and 'unknown' return types - no point validating them
1076
- // Also skip types matching ignoreTypes patterns (including classes extending DOM types)
1077
- // Also skip types containing type parameters or constructor types
1078
- const returnTypeText = returnType && returnTypeForString
1079
- ? this.getTypeKey(returnType, typeChecker, returnTypeForString)
1080
- : null;
1081
- if (process.env.DEBUG && returnTypeText) {
1082
- console.log(`TYPICAL: Checking return type: "${returnTypeText}" (isAsync: ${isAsync})`);
1083
- }
1084
-
1085
- // Skip if return type contains type parameters, constructor types, or is otherwise unvalidatable
1086
- const shouldSkipReturnType = returnTypeForString && this.containsUnvalidatableType(returnTypeForString);
1087
- if (shouldSkipReturnType && process.env.DEBUG) {
1088
- console.log(`TYPICAL: Skipping unvalidatable return type: ${returnTypeText}`);
1089
- }
1090
-
1091
- const isIgnoredReturnType = returnTypeText && this.isIgnoredType(returnTypeText, typeChecker, returnTypeForString);
1092
- if (isIgnoredReturnType && process.env.DEBUG) {
1093
- console.log(`TYPICAL: Skipping ignored type for return: ${returnTypeText}`);
1094
- }
1095
- const isSkippedReturnType = returnTypeText && shouldSkipType(returnTypeText);
1096
- if (isSkippedReturnType && process.env.DEBUG) {
1097
- console.log(`TYPICAL: Skipping previously failed type for return: ${returnTypeText}`);
1098
- }
1099
- if (shouldValidateFunctions && returnType && returnTypeForString && !this.isAnyOrUnknownType(returnType) && !isIgnoredReturnType && !shouldSkipReturnType && !isSkippedReturnType) {
1100
- const returnTransformer = (node: ts.Node): ts.Node => {
1101
- // Don't recurse into nested functions - they have their own return types
1102
- if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) ||
1103
- ts.isFunctionExpression(node) || ts.isMethodDeclaration(node)) {
1104
- return node;
1105
- }
1106
-
1107
- if (ts.isReturnStatement(node) && node.expression) {
1108
- // Skip return validation if the expression already contains a __typical _parse_* call
1109
- // since typia.assertParse already validates the parsed data
1110
- const containsTypicalParse = (n: ts.Node): boolean => {
1111
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
1112
- const name = n.expression.text;
1113
- if (name.startsWith("__typical" + "_parse_")) {
1114
- return true;
1115
- }
1116
- }
1117
- return ts.forEachChild(n, containsTypicalParse) || false;
1118
- };
1119
- if (containsTypicalParse(node.expression)) {
1120
- return node; // Already validated by parse, skip return validation
1121
- }
1122
-
1123
- // Flow analysis: Skip return validation if returning a validated variable
1124
- // (or property of one) that hasn't been tainted
1125
- const rootVar = this.getRootIdentifier(node.expression);
1126
- if (rootVar && validatedVariables.has(rootVar)) {
1127
- // Check if the variable has been tainted (mutated, passed to function, etc.)
1128
- if (!this.isTainted(rootVar, visitedBody)) {
1129
- // Return expression is rooted at a validated, untainted variable
1130
- // For direct returns (identifier) or property access, we can skip validation
1131
- if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
1132
- return node; // Skip validation - already validated and untainted
1133
- }
1134
- }
1135
- }
1136
-
1137
- // For non-async functions returning Promise, use .then(validator)
1138
- // For async functions, await the expression before validating
1139
- if (isNonAsyncPromiseReturn) {
1140
- // return expr.then(validator)
1141
- const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
1142
- const validatorName = this.config.reusableValidators
1143
- ? this.getOrCreateValidator(returnTypeText, returnType)
1144
- : null;
1145
-
1146
- // Create the validator reference (either reusable or inline)
1147
- let validatorExpr: ts.Expression;
1148
- if (validatorName) {
1149
- validatorExpr = ctx.factory.createIdentifier(validatorName);
1150
- } else {
1151
- // Inline: typia.assert<T>
1152
- validatorExpr = ctx.factory.createPropertyAccessExpression(
1153
- ctx.factory.createIdentifier("typia"),
1154
- ctx.factory.createIdentifier("assert")
1155
- );
1156
- // Note: For inline mode, we'd need to create a wrapper arrow function
1157
- // to pass the type argument. For simplicity, just use the property access
1158
- // and let typia handle it (though this won't work without type args)
1159
- // In practice, reusableValidators should be true for this to work well
1160
- }
1161
-
1162
- // expr.then(validator)
1163
- const thenCall = ctx.factory.createCallExpression(
1164
- ctx.factory.createPropertyAccessExpression(
1165
- node.expression,
1166
- ctx.factory.createIdentifier("then")
1167
- ),
1168
- undefined,
1169
- [validatorExpr]
1170
- );
1171
-
1172
- return ctx.factory.updateReturnStatement(node, thenCall);
1173
- }
1174
-
1175
- // For async functions, we need to await the expression before validating
1176
- // because the return expression might be a Promise
1177
- let expressionToValidate = node.expression;
1178
-
1179
- if (isAsync) {
1180
- // Check if the expression is already an await expression
1181
- const isAlreadyAwaited = ts.isAwaitExpression(node.expression);
1182
-
1183
- if (!isAlreadyAwaited) {
1184
- // Wrap in await: return validate(await expr)
1185
- expressionToValidate = ctx.factory.createAwaitExpression(node.expression);
1186
- }
1187
- }
1188
-
1189
- if (this.config.reusableValidators) {
1190
- // Use reusable validators - use typeNode text to preserve local aliases
1191
- // Pass returnTypeForString for synthesized nodes (inferred return types)
1192
- const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
1193
- const validatorName = this.getOrCreateValidator(
1194
- returnTypeText,
1195
- returnType
1196
- );
1197
-
1198
- const validatorCall = ctx.factory.createCallExpression(
1199
- ctx.factory.createIdentifier(validatorName),
1200
- undefined,
1201
- [expressionToValidate]
1202
- );
1203
-
1204
- return ctx.factory.updateReturnStatement(node, validatorCall);
1205
- } else {
1206
- // Use inline typia.assert calls
1207
- const typiaIdentifier = ctx.factory.createIdentifier("typia");
1208
- const assertIdentifier = ctx.factory.createIdentifier("assert");
1209
- const propertyAccess = ctx.factory.createPropertyAccessExpression(
1210
- typiaIdentifier,
1211
- assertIdentifier
1212
- );
1213
- const callExpression = ctx.factory.createCallExpression(
1214
- propertyAccess,
1215
- [returnType],
1216
- [expressionToValidate]
1217
- );
1218
-
1219
- return ctx.factory.updateReturnStatement(node, callExpression);
1220
- }
1221
- }
1222
- return ctx.ts.visitEachChild(node, returnTransformer, ctx.context);
1223
- };
1224
-
1225
- transformedStatements = ctx.ts.visitNodes(
1226
- visitedBody.statements,
1227
- returnTransformer
1228
- ) as ts.NodeArray<ts.Statement>;
1229
- }
1230
-
1231
- // Insert validation statements at the beginning
1232
- const newStatements = ctx.factory.createNodeArray([
1233
- ...validationStatements,
1234
- ...transformedStatements,
1235
- ]);
1236
- const newBody = ctx.factory.updateBlock(visitedBody, newStatements);
1237
-
1238
- if (ts.isFunctionDeclaration(func)) {
1239
- return ctx.factory.updateFunctionDeclaration(
1240
- func,
1241
- func.modifiers,
1242
- func.asteriskToken,
1243
- func.name,
1244
- func.typeParameters,
1245
- func.parameters,
1246
- func.type,
1247
- newBody
1248
- );
1249
- } else if (ts.isArrowFunction(func)) {
1250
- return ctx.factory.updateArrowFunction(
1251
- func,
1252
- func.modifiers,
1253
- func.typeParameters,
1254
- func.parameters,
1255
- func.type,
1256
- func.equalsGreaterThanToken,
1257
- newBody
1258
- );
1259
- } else if (ts.isMethodDeclaration(func)) {
1260
- return ctx.factory.updateMethodDeclaration(
1261
- func,
1262
- func.modifiers,
1263
- func.asteriskToken,
1264
- func.name,
1265
- func.questionToken,
1266
- func.typeParameters,
1267
- func.parameters,
1268
- func.type,
1269
- newBody
1270
- );
1271
- }
1272
-
1273
- return func;
1274
- };
1275
-
1276
- let transformedSourceFile = ctx.ts.visitNode(
1277
- sourceFile,
1278
- visit
1279
- ) as ts.SourceFile;
1280
-
1281
- // Add typia import and validator statements if needed
1282
- if (needsTypiaImport) {
1283
- transformedSourceFile = this.addTypiaImport(transformedSourceFile, ctx);
1284
-
1285
- // Add validator statements after imports (only if using reusable validators)
1286
- if (this.config.reusableValidators) {
1287
- const validatorStmts = this.createValidatorStatements(ctx);
1288
-
1289
- if (validatorStmts.length > 0) {
1290
- const importStatements = transformedSourceFile.statements.filter(
1291
- ctx.ts.isImportDeclaration
1292
- );
1293
- const otherStatements = transformedSourceFile.statements.filter(
1294
- (stmt) => !ctx.ts.isImportDeclaration(stmt)
1295
- );
1296
-
1297
- const newStatements = ctx.factory.createNodeArray([
1298
- ...importStatements,
1299
- ...validatorStmts,
1300
- ...otherStatements,
1301
- ]);
1302
-
1303
- transformedSourceFile = ctx.factory.updateSourceFile(
1304
- transformedSourceFile,
1305
- newStatements
1306
- );
1307
- }
1308
- }
1309
- }
1310
-
1311
- return transformedSourceFile;
1312
- }
1313
-
1314
- public shouldTransformFile(fileName: string): boolean {
1315
- return shouldTransformFile(fileName, this.config);
1316
- }
1317
-
1318
- /**
1319
- * Check if a TypeNode represents a type that shouldn't be validated.
1320
- * This includes:
1321
- * - any/unknown (intentional escape hatches)
1322
- * - Type parameters (generics like T)
1323
- * - Constructor types (new (...args: any[]) => T)
1324
- * - Function types ((...args) => T)
1325
- */
1326
- private isAnyOrUnknownType(typeNode: ts.TypeNode): boolean {
1327
- // any/unknown are escape hatches
1328
- if (typeNode.kind === this.ts.SyntaxKind.AnyKeyword ||
1329
- typeNode.kind === this.ts.SyntaxKind.UnknownKeyword) {
1330
- return true;
1331
- }
1332
- // Type parameters (generics) can't be validated at runtime
1333
- if (typeNode.kind === this.ts.SyntaxKind.TypeReference) {
1334
- const typeRef = typeNode as ts.TypeReferenceNode;
1335
- // Single identifier that's a type parameter
1336
- if (ts.isIdentifier(typeRef.typeName)) {
1337
- // Check if it's a type parameter by looking for it in enclosing type parameter lists
1338
- // For now, we'll check if it's a single uppercase letter or common generic names
1339
- const name = typeRef.typeName.text;
1340
- // Common type parameter names - single letters or common conventions
1341
- if (/^[A-Z]$/.test(name) || /^T[A-Z]?[a-z]*$/.test(name)) {
1342
- return true;
1343
- }
1344
- }
1345
- }
1346
- // Constructor types can't be validated by typia
1347
- if (typeNode.kind === this.ts.SyntaxKind.ConstructorType) {
1348
- return true;
1349
- }
1350
- // Function types generally shouldn't be validated
1351
- if (typeNode.kind === this.ts.SyntaxKind.FunctionType) {
1352
- return true;
1353
- }
1354
- return false;
1355
- }
1356
-
1357
- /**
1358
- * Check if a type contains any unvalidatable parts (type parameters, constructor types, etc.)
1359
- * This recursively checks intersection and union types.
1360
- */
1361
- private containsUnvalidatableType(type: ts.Type): boolean {
1362
- // Type parameters can't be validated at runtime
1363
- if ((type.flags & this.ts.TypeFlags.TypeParameter) !== 0) {
1364
- return true;
1365
- }
1366
-
1367
- // Check intersection types - if any part is unvalidatable, the whole thing is
1368
- if (type.isIntersection()) {
1369
- return type.types.some(t => this.containsUnvalidatableType(t));
1370
- }
1371
-
1372
- // Check union types
1373
- if (type.isUnion()) {
1374
- return type.types.some(t => this.containsUnvalidatableType(t));
1375
- }
1376
-
1377
- // Check for constructor signatures (like `new (...args) => T`)
1378
- const callSignatures = type.getConstructSignatures?.() ?? [];
1379
- if (callSignatures.length > 0) {
1380
- return true;
1381
- }
1382
-
1383
- return false;
1384
- }
1385
-
1386
- /**
1387
- * Check if a Type has any or unknown flags, or is a type parameter or function/constructor.
1388
- */
1389
- private isAnyOrUnknownTypeFlags(type: ts.Type): boolean {
1390
- // any/unknown
1391
- if ((type.flags & this.ts.TypeFlags.Any) !== 0 ||
1392
- (type.flags & this.ts.TypeFlags.Unknown) !== 0) {
1393
- return true;
1394
- }
1395
- // Type parameters (generics) - can't be validated at runtime
1396
- if ((type.flags & this.ts.TypeFlags.TypeParameter) !== 0) {
1397
- return true;
1398
- }
1399
- return false;
1400
- }
1401
-
1402
- /**
1403
- * Check if a type name matches any of the ignoreTypes patterns.
1404
- * Supports wildcards: "React.*" matches "React.FormEvent", "React.ChangeEvent", etc.
1405
- * Also handles union types: "Document | Element" is ignored if "Document" or "Element" is in ignoreTypes.
1406
- */
1407
- private isIgnoredType(typeName: string, typeChecker?: ts.TypeChecker, type?: ts.Type): boolean {
1408
- // Combine user patterns with DOM types if ignoreDOMTypes is enabled (default: true)
1409
- const userPatterns = this.config.ignoreTypes ?? [];
1410
- const domPatterns = this.config.ignoreDOMTypes !== false ? DOM_TYPES_TO_IGNORE : [];
1411
- const patterns = [...userPatterns, ...domPatterns];
1412
-
1413
- if (patterns.length === 0) return false;
1414
-
1415
- // For union types, check each constituent
1416
- if (type && type.isUnion()) {
1417
- const nonNullTypes = type.types.filter(t =>
1418
- !(t.flags & this.ts.TypeFlags.Null) && !(t.flags & this.ts.TypeFlags.Undefined)
1419
- );
1420
- if (nonNullTypes.length === 0) return false;
1421
- // All non-null types must be ignored
1422
- return nonNullTypes.every(t => this.isIgnoredSingleType(t, patterns, typeChecker));
1423
- }
1424
-
1425
- // For non-union types, check directly
1426
- if (type && typeChecker) {
1427
- return this.isIgnoredSingleType(type, patterns, typeChecker);
1428
- }
1429
-
1430
- // Fallback: string-based matching for union types like "Document | Element | null"
1431
- const typeParts = typeName.split(' | ').map(t => t.trim());
1432
- const nonNullParts = typeParts.filter(t => t !== 'null' && t !== 'undefined');
1433
- if (nonNullParts.length === 0) return false;
1434
-
1435
- return nonNullParts.every(part => this.matchesIgnorePattern(part, patterns));
1436
- }
1437
-
1438
- /**
1439
- * Check if a single type (not a union) should be ignored.
1440
- * Checks both the type name and its base classes.
1441
- */
1442
- private isIgnoredSingleType(type: ts.Type, patterns: string[], typeChecker?: ts.TypeChecker, depth = 0): boolean {
1443
- // Prevent infinite recursion
1444
- if (depth > 20) return false;
1445
-
1446
- const typeName = type.symbol?.name || '';
1447
-
1448
- if (process.env.DEBUG) {
1449
- console.log(`TYPICAL: isIgnoredSingleType checking type: "${typeName}" (depth: ${depth})`);
1450
- }
1451
-
1452
- // Check direct name match
1453
- if (this.matchesIgnorePattern(typeName, patterns)) {
1454
- if (process.env.DEBUG) {
1455
- console.log(`TYPICAL: Type "${typeName}" matched ignore pattern directly`);
1456
- }
1457
- return true;
1458
- }
1459
-
1460
- // Check base classes (for classes extending DOM types like HTMLElement)
1461
- // This works for class types that have getBaseTypes available
1462
- const baseTypes = type.getBaseTypes?.() ?? [];
1463
- if (process.env.DEBUG && baseTypes.length > 0) {
1464
- console.log(`TYPICAL: Type "${typeName}" has ${baseTypes.length} base types: ${baseTypes.map(t => t.symbol?.name || '?').join(', ')}`);
1465
- }
1466
- for (const baseType of baseTypes) {
1467
- if (this.isIgnoredSingleType(baseType, patterns, typeChecker, depth + 1)) {
1468
- if (process.env.DEBUG) {
1469
- console.log(`TYPICAL: Type "${typeName}" ignored because base type "${baseType.symbol?.name}" is ignored`);
1470
- }
1471
- return true;
1472
- }
1473
- }
1474
-
1475
- // Also check the declared type's symbol for heritage clauses (alternative approach)
1476
- // This handles cases where getBaseTypes doesn't return what we expect
1477
- if (type.symbol?.declarations) {
1478
- for (const decl of type.symbol.declarations) {
1479
- if (this.ts.isClassDeclaration(decl) && decl.heritageClauses) {
1480
- for (const heritage of decl.heritageClauses) {
1481
- if (heritage.token === this.ts.SyntaxKind.ExtendsKeyword) {
1482
- for (const heritageType of heritage.types) {
1483
- const baseTypeName = heritageType.expression.getText();
1484
- if (process.env.DEBUG) {
1485
- console.log(`TYPICAL: Type "${typeName}" extends "${baseTypeName}" (from heritage clause)`);
1486
- }
1487
- if (this.matchesIgnorePattern(baseTypeName, patterns)) {
1488
- if (process.env.DEBUG) {
1489
- console.log(`TYPICAL: Type "${typeName}" ignored because it extends "${baseTypeName}"`);
1490
- }
1491
- return true;
1492
- }
1493
- // Recursively check the heritage type
1494
- if (typeChecker) {
1495
- const heritageTypeObj = typeChecker.getTypeAtLocation(heritageType);
1496
- if (this.isIgnoredSingleType(heritageTypeObj, patterns, typeChecker, depth + 1)) {
1497
- return true;
1498
- }
1499
-
1500
- // For mixin patterns like `extends VueWatcher(BaseElement)`, the expression is a CallExpression.
1501
- // We need to check the return type of the mixin function AND the arguments passed to it.
1502
- if (this.ts.isCallExpression(heritageType.expression)) {
1503
- // Check arguments to the mixin (e.g., BaseElement in VueWatcher(BaseElement))
1504
- for (const arg of heritageType.expression.arguments) {
1505
- const argType = typeChecker.getTypeAtLocation(arg);
1506
- if (process.env.DEBUG) {
1507
- console.log(`TYPICAL: Type "${typeName}" mixin arg: "${argType.symbol?.name}" (from call expression)`);
1508
- }
1509
- if (this.isIgnoredSingleType(argType, patterns, typeChecker, depth + 1)) {
1510
- if (process.env.DEBUG) {
1511
- console.log(`TYPICAL: Type "${typeName}" ignored because mixin argument "${argType.symbol?.name}" is ignored`);
1512
- }
1513
- return true;
1514
- }
1515
- }
1516
- }
1517
- }
1518
- }
1519
- }
1520
- }
1521
- }
1522
- }
1523
- }
1524
-
1525
- return false;
1526
- }
1527
-
1528
- /**
1529
- * Check if a single type name matches any ignore pattern.
1530
- */
1531
- private matchesIgnorePattern(typeName: string, patterns: string[]): boolean {
1532
- return patterns.some(pattern => {
1533
- // Convert glob pattern to regex: "React.*" -> /^React\..*$/
1534
- const regexStr = '^' + pattern
1535
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except *
1536
- .replace(/\*/g, '.*') + '$';
1537
- return new RegExp(regexStr).test(typeName);
1538
- });
1539
- }
1540
-
1541
- /**
1542
- * Find untransformed typia calls in the output code.
1543
- * These indicate types that typia could not process.
49
+ * @param fileName - Path to the TypeScript file
50
+ * @param mode - Output mode: 'ts' returns TypeScript, 'js' would transpile (not yet supported)
51
+ * @returns Transformed code with validation
1544
52
  */
1545
- private findUntransformedTypiaCalls(code: string): Array<{ method: string; type: string }> {
1546
- const results: Array<{ method: string; type: string }> = [];
1547
-
1548
- // Match patterns like: typia.createAssert<Type>() or typia.json.createAssertParse<Type>()
1549
- // The type argument can contain nested generics like React.FormEvent<HTMLElement>
1550
- const patterns = [
1551
- /typia\.createAssert<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
1552
- /typia\.json\.createAssertParse<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
1553
- /typia\.json\.createStringify<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
1554
- ];
1555
-
1556
- for (const pattern of patterns) {
1557
- let match;
1558
- while ((match = pattern.exec(code)) !== null) {
1559
- const methodMatch = match[0].match(/typia\.([\w.]+)</);
1560
- results.push({
1561
- method: methodMatch ? methodMatch[1] : 'unknown',
1562
- type: match[1]
1563
- });
1564
- }
53
+ async transform(fileName: string, mode: 'ts' | 'js' = 'ts'): Promise<TransformResult> {
54
+ if (mode === 'js') {
55
+ throw new Error('Mode "js" not yet supported - use "ts" and transpile separately')
1565
56
  }
1566
57
 
1567
- return results;
1568
- }
58
+ await this.ensureInitialized()
1569
59
 
1570
- /**
1571
- * Infer type information from a JSON.stringify argument for creating a reusable stringifier.
1572
- */
1573
- private inferStringifyType(
1574
- arg: ts.Expression,
1575
- typeChecker: ts.TypeChecker,
1576
- ctx: TransformContext
1577
- ): { typeText: string; typeNode: ts.TypeNode } {
1578
- const ts = this.ts;
60
+ const resolvedPath = resolve(fileName)
61
+ const result = await this.compiler.transformFile(this.projectHandle!, resolvedPath)
1579
62
 
1580
- // Type assertion: use the asserted type directly
1581
- if (ts.isAsExpression(arg)) {
1582
- const typeNode = arg.type;
1583
- const typeKey = this.getTypeKey(typeNode, typeChecker);
1584
- return { typeText: typeKey, typeNode };
1585
- }
1586
-
1587
- // Object literal: infer type from type checker
1588
- if (ts.isObjectLiteralExpression(arg)) {
1589
- const objectType = typeChecker.getTypeAtLocation(arg);
1590
- const typeNode = typeChecker.typeToTypeNode(objectType, arg, TYPE_NODE_FLAGS);
1591
- if (!typeNode) {
1592
- throw new Error('unknown type node for object literal: ' + arg.getText());
1593
- }
1594
- const typeKey = this.getTypeKey(typeNode, typeChecker, objectType);
1595
- return { typeText: typeKey, typeNode };
1596
- }
1597
-
1598
- // Other expressions: infer from type checker
1599
- const argType = typeChecker.getTypeAtLocation(arg);
1600
- const typeNode = typeChecker.typeToTypeNode(argType, arg, TYPE_NODE_FLAGS);
1601
- if (typeNode) {
1602
- const typeKey = this.getTypeKey(typeNode, typeChecker, argType);
1603
- return { typeText: typeKey, typeNode };
1604
- }
1605
-
1606
- // Fallback to unknown
1607
63
  return {
1608
- typeText: "unknown",
1609
- typeNode: ctx.factory.createKeywordTypeNode(ctx.ts.SyntaxKind.UnknownKeyword),
1610
- };
1611
- }
1612
-
1613
- // ============================================
1614
- // Flow Analysis Helpers
1615
- // ============================================
1616
-
1617
- /**
1618
- * Gets the root identifier from an expression.
1619
- * e.g., `user.address.city` -> "user"
1620
- */
1621
- private getRootIdentifier(expr: ts.Expression): string | undefined {
1622
- if (this.ts.isIdentifier(expr)) {
1623
- return expr.text;
64
+ code: result.code,
65
+ map: result.sourceMap ?? null,
1624
66
  }
1625
- if (this.ts.isPropertyAccessExpression(expr)) {
1626
- return this.getRootIdentifier(expr.expression);
1627
- }
1628
- return undefined;
1629
- }
1630
-
1631
- /**
1632
- * Check if a validated variable has been tainted (mutated) in the function body.
1633
- * A variable is tainted if it's reassigned, has properties modified, is passed
1634
- * to a function, has methods called on it, or if an await occurs.
1635
- */
1636
- private isTainted(varName: string, body: ts.Block): boolean {
1637
- let tainted = false;
1638
- const ts = this.ts;
1639
-
1640
- // Collect aliases (variables that reference properties of varName)
1641
- // e.g., const addr = user.address; -> addr is an alias
1642
- const aliases = new Set<string>([varName]);
1643
-
1644
- const collectAliases = (node: ts.Node): void => {
1645
- if (ts.isVariableStatement(node)) {
1646
- for (const decl of node.declarationList.declarations) {
1647
- if (ts.isIdentifier(decl.name) && decl.initializer) {
1648
- const initRoot = this.getRootIdentifier(decl.initializer);
1649
- if (initRoot && aliases.has(initRoot)) {
1650
- aliases.add(decl.name.text);
1651
- }
1652
- }
1653
- }
1654
- }
1655
- ts.forEachChild(node, collectAliases);
1656
- };
1657
- collectAliases(body);
1658
-
1659
- // Helper to check if any alias is involved
1660
- const involvesTrackedVar = (expr: ts.Expression): boolean => {
1661
- const root = this.getRootIdentifier(expr);
1662
- return root !== undefined && aliases.has(root);
1663
- };
1664
-
1665
- const checkTainting = (node: ts.Node): void => {
1666
- if (tainted) return;
1667
-
1668
- // Reassignment: trackedVar = ...
1669
- if (ts.isBinaryExpression(node) &&
1670
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1671
- ts.isIdentifier(node.left) &&
1672
- aliases.has(node.left.text)) {
1673
- tainted = true;
1674
- return;
1675
- }
1676
-
1677
- // Property assignment: trackedVar.x = ... or alias.x = ...
1678
- if (ts.isBinaryExpression(node) &&
1679
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1680
- ts.isPropertyAccessExpression(node.left) &&
1681
- involvesTrackedVar(node.left)) {
1682
- tainted = true;
1683
- return;
1684
- }
1685
-
1686
- // Element assignment: trackedVar[x] = ... or alias[x] = ...
1687
- if (ts.isBinaryExpression(node) &&
1688
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1689
- ts.isElementAccessExpression(node.left) &&
1690
- involvesTrackedVar(node.left.expression)) {
1691
- tainted = true;
1692
- return;
1693
- }
1694
-
1695
- // Passed as argument to a function: fn(trackedVar) or fn(alias)
1696
- if (ts.isCallExpression(node)) {
1697
- for (const arg of node.arguments) {
1698
- let hasTrackedRef = false;
1699
- const checkRef = (n: ts.Node): void => {
1700
- if (ts.isIdentifier(n) && aliases.has(n.text)) {
1701
- hasTrackedRef = true;
1702
- }
1703
- ts.forEachChild(n, checkRef);
1704
- };
1705
- checkRef(arg);
1706
- if (hasTrackedRef) {
1707
- tainted = true;
1708
- return;
1709
- }
1710
- }
1711
- }
1712
-
1713
- // Method call on the variable: trackedVar.method() or alias.method()
1714
- if (ts.isCallExpression(node) &&
1715
- ts.isPropertyAccessExpression(node.expression) &&
1716
- involvesTrackedVar(node.expression.expression)) {
1717
- tainted = true;
1718
- return;
1719
- }
1720
-
1721
- // Await expression (async boundary - external code could run)
1722
- if (ts.isAwaitExpression(node)) {
1723
- tainted = true;
1724
- return;
1725
- }
1726
-
1727
- ts.forEachChild(node, checkTainting);
1728
- };
1729
-
1730
- checkTainting(body);
1731
- return tainted;
1732
- }
1733
-
1734
- private addTypiaImport(
1735
- sourceFile: ts.SourceFile,
1736
- ctx: TransformContext
1737
- ): ts.SourceFile {
1738
- const { factory } = ctx;
1739
-
1740
- const existingImports = sourceFile.statements.filter(
1741
- ctx.ts.isImportDeclaration
1742
- );
1743
- const hasTypiaImport = existingImports.some(
1744
- (imp) =>
1745
- imp.moduleSpecifier &&
1746
- ctx.ts.isStringLiteral(imp.moduleSpecifier) &&
1747
- imp.moduleSpecifier.text === "typia"
1748
- );
1749
-
1750
- if (!hasTypiaImport) {
1751
- const typiaImport = factory.createImportDeclaration(
1752
- undefined,
1753
- factory.createImportClause(
1754
- false,
1755
- factory.createIdentifier("typia"),
1756
- undefined
1757
- ),
1758
- factory.createStringLiteral("typia")
1759
- );
1760
-
1761
- const newSourceFile = factory.updateSourceFile(
1762
- sourceFile,
1763
- factory.createNodeArray([typiaImport, ...sourceFile.statements])
1764
- );
1765
-
1766
- return newSourceFile;
1767
- }
1768
-
1769
- return sourceFile;
1770
67
  }
1771
68
 
1772
69
  /**
1773
- * Gets type text for use as a validator map key.
1774
- * Uses getText() to preserve local aliases (e.g., "User1" vs "User2"),
1775
- * but falls back to typeToString() for synthesized nodes without source positions.
1776
- *
1777
- * @param typeNode The TypeNode to get a key for
1778
- * @param typeChecker The TypeChecker to use
1779
- * @param typeObj Optional Type object - use this for synthesized nodes since
1780
- * getTypeFromTypeNode doesn't work correctly on them
70
+ * Close the Go compiler process and release resources.
71
+ * This immediately kills the process without waiting for pending operations.
1781
72
  */
1782
- private getTypeKey(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker, typeObj?: ts.Type): string {
1783
- // Check if node has a real position (not synthesized)
1784
- if (typeNode.pos >= 0 && typeNode.end > typeNode.pos) {
1785
- try {
1786
- const text = typeNode.getText();
1787
- // Check for truncation patterns in source text (shouldn't happen but be safe)
1788
- if (!text.includes('...') || !text.match(/\.\.\.\d+\s+more/)) {
1789
- return text;
1790
- }
1791
- } catch {
1792
- // Fall through to typeToString
1793
- }
1794
- }
1795
- // Fallback for synthesized nodes - use the provided Type object if available,
1796
- // otherwise try to get it from the node (which may not work correctly)
1797
- const type = typeObj ?? typeChecker.getTypeFromTypeNode(typeNode);
1798
- const typeString = typeChecker.typeToString(type, undefined, this.ts.TypeFormatFlags.NoTruncation);
1799
-
1800
- // TypeScript may still truncate very large types even with NoTruncation flag.
1801
- // Detect truncation patterns like "...19 more..." and use a hash-based key instead.
1802
- if (typeString.match(/\.\.\.\d+\s+more/)) {
1803
- const hash = this.hashString(typeString);
1804
- return `__complex_type_${hash}`;
1805
- }
1806
-
1807
- return typeString;
1808
- }
1809
-
1810
- /**
1811
- * Simple string hash for creating unique identifiers from type strings.
1812
- */
1813
- private hashString(str: string): string {
1814
- let hash = 0;
1815
- for (let i = 0; i < str.length; i++) {
1816
- const char = str.charCodeAt(i);
1817
- hash = ((hash << 5) - hash) + char;
1818
- hash = hash & hash; // Convert to 32bit integer
1819
- }
1820
- return Math.abs(hash).toString(36);
1821
- }
1822
-
1823
- /**
1824
- * Format typia's error message into a cleaner list format.
1825
- * Typia outputs verbose messages like:
1826
- * "unsupported type detected\n\n- Window.ondevicemotion: unknown\n - nonsensible intersection\n\n- Window.ondeviceorientation..."
1827
- * We want to extract just the problematic types and their issues.
1828
- */
1829
- private formatTypiaError(message: string): string {
1830
- const lines = message.split('\n');
1831
- const firstLine = lines[0]; // e.g., "unsupported type detected"
1832
-
1833
- // Parse the error entries - each starts with "- " at the beginning of a line
1834
- const issues: { type: string; reasons: string[] }[] = [];
1835
- let currentIssue: { type: string; reasons: string[] } | null = null;
1836
-
1837
- for (const line of lines.slice(1)) {
1838
- if (line.startsWith('- ')) {
1839
- // New type entry
1840
- if (currentIssue) {
1841
- issues.push(currentIssue);
1842
- }
1843
- currentIssue = { type: line.slice(2), reasons: [] };
1844
- } else if (line.startsWith(' - ') && currentIssue) {
1845
- // Reason for current type
1846
- currentIssue.reasons.push(line.slice(4));
1847
- }
1848
- }
1849
- if (currentIssue) {
1850
- issues.push(currentIssue);
1851
- }
1852
-
1853
- if (issues.length === 0) {
1854
- return ` ${firstLine}`;
1855
- }
1856
-
1857
- // Limit to 5 issues, show count of remaining
1858
- const maxIssues = 5;
1859
- const displayIssues = issues.slice(0, maxIssues);
1860
- const remainingCount = issues.length - maxIssues;
1861
-
1862
- const formatted = displayIssues.map(issue => {
1863
- const reasons = issue.reasons.map(r => ` - ${r}`).join('\n');
1864
- return ` - ${issue.type}\n${reasons}`;
1865
- }).join('\n');
1866
-
1867
- const suffix = remainingCount > 0 ? `\n (and ${remainingCount} more errors)` : '';
1868
-
1869
- return ` ${firstLine}\n${formatted}${suffix}`;
1870
- }
1871
-
1872
-
1873
- /**
1874
- * Creates a readable name suffix from a type string.
1875
- * For simple identifiers like "User" or "string", returns the name directly.
1876
- * For complex types, returns a numeric index.
1877
- */
1878
- private getTypeNameSuffix(typeText: string, existingNames: Set<string>, fallbackIndex: number): string {
1879
- // Complex types from getTypeKey() - use numeric index
1880
- if (typeText.startsWith('__complex_type_')) {
1881
- return String(fallbackIndex);
1882
- }
1883
-
1884
- // Check if it's a simple identifier (letters, numbers, underscore, starting with letter/underscore)
1885
- if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(typeText)) {
1886
- // It's a simple type name like "User", "string", "MyType"
1887
- let name = typeText;
1888
- // Handle collisions by appending a number
1889
- if (existingNames.has(name)) {
1890
- let i = 2;
1891
- while (existingNames.has(`${typeText}${i}`)) {
1892
- i++;
1893
- }
1894
- name = `${typeText}${i}`;
1895
- }
1896
- return name;
1897
- }
1898
- // Complex type - use numeric index
1899
- return String(fallbackIndex);
1900
- }
1901
-
1902
- /**
1903
- * Generic method to get or create a typed function (validator, stringifier, or parser).
1904
- */
1905
- private getOrCreateTypedFunction(
1906
- kind: 'assert' | 'stringify' | 'parse',
1907
- typeText: string,
1908
- typeNode: ts.TypeNode
1909
- ): string {
1910
- const maps = {
1911
- assert: this.typeValidators,
1912
- stringify: this.typeStringifiers,
1913
- parse: this.typeParsers,
1914
- };
1915
- const prefixes = {
1916
- assert: '__typical_assert_',
1917
- stringify: '__typical_stringify_',
1918
- parse: '__typical_parse_',
1919
- };
1920
-
1921
- const map = maps[kind];
1922
- const prefix = prefixes[kind];
1923
-
1924
- if (map.has(typeText)) {
1925
- return map.get(typeText)!.name;
1926
- }
1927
-
1928
- const existingSuffixes = [...map.values()].map(v => v.name.slice(prefix.length));
1929
- const existingNames = new Set(existingSuffixes);
1930
- const numericCount = existingSuffixes.filter(s => /^\d+$/.test(s)).length;
1931
- const suffix = this.getTypeNameSuffix(typeText, existingNames, numericCount);
1932
- const name = `${prefix}${suffix}`;
1933
- map.set(typeText, { name, typeNode });
1934
- return name;
1935
- }
1936
-
1937
- private getOrCreateValidator(typeText: string, typeNode: ts.TypeNode): string {
1938
- return this.getOrCreateTypedFunction('assert', typeText, typeNode);
1939
- }
1940
-
1941
- private getOrCreateStringifier(typeText: string, typeNode: ts.TypeNode): string {
1942
- return this.getOrCreateTypedFunction('stringify', typeText, typeNode);
1943
- }
1944
-
1945
- private getOrCreateParser(typeText: string, typeNode: ts.TypeNode): string {
1946
- return this.getOrCreateTypedFunction('parse', typeText, typeNode);
1947
- }
1948
-
1949
- /**
1950
- * Creates a nested property access expression from an array of identifiers.
1951
- * e.g., ['typia', 'json', 'createStringify'] -> typia.json.createStringify
1952
- */
1953
- private createPropertyAccessChain(factory: ts.NodeFactory, parts: string[]): ts.Expression {
1954
- let expr: ts.Expression = factory.createIdentifier(parts[0]);
1955
- for (let i = 1; i < parts.length; i++) {
1956
- expr = factory.createPropertyAccessExpression(expr, parts[i]);
1957
- }
1958
- return expr;
1959
- }
1960
-
1961
- private createValidatorStatements(ctx: TransformContext): ts.Statement[] {
1962
- const { factory } = ctx;
1963
- const statements: ts.Statement[] = [];
1964
-
1965
- const configs: Array<{
1966
- map: Map<string, { name: string; typeNode: ts.TypeNode }>;
1967
- methodPath: string[];
1968
- }> = [
1969
- { map: this.typeValidators, methodPath: ['typia', 'createAssert'] },
1970
- { map: this.typeStringifiers, methodPath: ['typia', 'json', 'createStringify'] },
1971
- { map: this.typeParsers, methodPath: ['typia', 'json', 'createAssertParse'] },
1972
- ];
1973
-
1974
- for (const { map, methodPath } of configs) {
1975
- for (const [, { name, typeNode }] of map) {
1976
- const createCall = factory.createCallExpression(
1977
- this.createPropertyAccessChain(factory, methodPath),
1978
- [typeNode],
1979
- []
1980
- );
1981
-
1982
- const declaration = factory.createVariableStatement(
1983
- undefined,
1984
- factory.createVariableDeclarationList(
1985
- [factory.createVariableDeclaration(name, undefined, undefined, createCall)],
1986
- ctx.ts.NodeFlags.Const
1987
- )
1988
- );
1989
- statements.push(declaration);
1990
- }
1991
- }
1992
-
1993
- return statements;
73
+ async close(): Promise<void> {
74
+ this.projectHandle = null
75
+ this.initPromise = null
76
+ await this.compiler.close()
1994
77
  }
1995
78
  }