@elliots/typical 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +234 -200
  2. package/dist/src/cli.js +12 -85
  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 +12 -0
  6. package/dist/src/config.js +40 -38
  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 -1
  11. package/dist/src/esm-loader.js +30 -17
  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 +5 -4
  16. package/dist/src/index.js +1 -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 +1 -1
  28. package/dist/src/source-map.js +1 -1
  29. package/dist/src/source-map.js.map +1 -1
  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 -193
  35. package/dist/src/transformer.js +41 -1917
  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 +51 -47
  42. package/src/cli.ts +41 -128
  43. package/src/config.ts +106 -91
  44. package/src/esm-loader-register.ts +2 -2
  45. package/src/esm-loader.ts +44 -29
  46. package/src/index.ts +5 -10
  47. package/src/patch-fs.cjs +14 -14
  48. package/src/timing.ts +74 -0
  49. package/src/transformer.ts +47 -2592
  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/source-map.ts +0 -202
  56. package/src/tsc-plugin.ts +0 -12
@@ -1,2623 +1,78 @@
1
- import ts from "typescript";
2
- import fs from "fs";
3
- import path from "path";
4
- import { loadConfig, validateConfig, TypicalConfig, getCompiledIgnorePatterns, CompiledIgnorePatterns } from "./config.js";
5
- import { shouldTransformFile } from "./file-filter.js";
6
- import { hoistRegexConstructors } from "./regex-hoister.js";
7
- import {
8
- TransformResult,
9
- composeSourceMaps,
10
- } from "./source-map.js";
11
- import type { EncodedSourceMap } from '@ampproject/remapping';
12
-
13
- import { transform as typiaTransform } from "typia/lib/transform.js";
14
- import { setupTsProgram } from "./setup.js";
15
-
16
- // Re-export TransformResult for consumers
17
- export type { TransformResult } from "./source-map.js";
18
-
19
- // Flags for typeToTypeNode to prefer type aliases over import() syntax
20
- const TYPE_NODE_FLAGS = ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
21
-
22
- // Source map markers:
23
- // - @T:line:col - Type annotation marker (maps generated code to source type annotation)
24
- // - @L:line - Line marker (identity mapping - maps output line to source line)
25
- //
26
- // Lines with @T markers map to the specified type annotation position
27
- // Lines with @L markers establish identity mapping (output line N maps to source line N)
28
- // Lines without markers inherit from the most recent marker above
29
- //
30
- // Match single-line comment markers: //@T:line:col or //@L:line
31
- const TYPE_MARKER_REGEX = /\/\/@T:(\d+):(\d+)/g;
32
- const LINE_MARKER_REGEX = /\/\/@L:(\d+)/g;
33
- // Strip all markers
34
- const ALL_MARKERS_REGEX = /\/\/@[TL]:\d+(?::\d+)?\n?/g;
35
-
36
- /**
37
- * Add a type annotation marker comment to a node.
38
- * The marker encodes the original line:column position of the type annotation
39
- * so validation errors can be traced back to the source.
40
- *
41
- * Uses a single-line comment (//) which forces a newline after it,
42
- * ensuring each marked statement is on its own line for accurate source maps.
43
- */
44
- function addSourceMapMarker<T extends ts.Node>(
45
- node: T,
46
- sourceFile: ts.SourceFile,
47
- originalNode: ts.Node
48
- ): T {
49
- const pos = originalNode.getStart(sourceFile);
50
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
51
- // Use 1-based line numbers for source maps
52
- // Single-line comment forces a newline after it
53
- const marker = `@T:${line + 1}:${character}`;
54
- if (process.env.DEBUG) {
55
- console.log(`TYPICAL: Adding source map marker //${marker}`);
56
- }
57
- const result = ts.addSyntheticLeadingComment(
58
- node,
59
- ts.SyntaxKind.SingleLineCommentTrivia,
60
- marker,
61
- true // trailing newline
62
- );
63
- if (process.env.DEBUG) {
64
- const comments = ts.getSyntheticLeadingComments(result);
65
- console.log(`TYPICAL: Synthetic comments after addSourceMapMarker:`, comments?.length);
66
- }
67
- return result;
68
- }
69
-
70
- /**
71
- * Add a line marker comment to a node for identity mapping.
72
- * The marker encodes the original line number so the output line maps to itself.
73
- *
74
- * Uses a single-line comment (//) which forces a newline after it.
75
- */
76
- function addLineMarker<T extends ts.Node>(
77
- node: T,
78
- sourceFile: ts.SourceFile,
79
- originalNode: ts.Node
80
- ): T {
81
- const pos = originalNode.getStart(sourceFile);
82
- const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
83
- // Use 1-based line numbers for source maps
84
- const marker = `@L:${line + 1}`;
85
- if (process.env.DEBUG) {
86
- console.log(`TYPICAL: Adding line marker //${marker}`);
87
- }
88
- return ts.addSyntheticLeadingComment(
89
- node,
90
- ts.SyntaxKind.SingleLineCommentTrivia,
91
- marker,
92
- true // trailing newline
93
- );
94
- }
95
-
96
1
  /**
97
- * Parse source map markers from code and build a source map.
98
- * Markers are single-line comments on their own line:
99
- * - //@T:line:col - Type annotation marker (maps to specific source position)
100
- * - //@L:line - Line marker (identity mapping to source line, col 0)
2
+ * TypicalTransformer - Thin wrapper around the Go compiler.
101
3
  *
102
- * The marker applies to the NEXT line (the actual code statement).
103
- * Lines without markers inherit from the most recent marker above.
104
- * Returns the code with markers stripped and the generated source map.
105
- */
106
- function parseMarkersAndBuildSourceMap(
107
- code: string,
108
- fileName: string,
109
- originalSource: string,
110
- includeContent: boolean
111
- ): { code: string; map: EncodedSourceMap } {
112
- const lines = code.split('\n');
113
-
114
- // Current mapping position (inherited by unmarked lines)
115
- let currentOrigLine = 1;
116
- let currentOrigCol = 0;
117
- let pendingMarker: { line: number; col: number } | null = null;
118
-
119
- const mappings: Array<{ generatedLine: number; generatedCol: number; originalLine: number; originalCol: number }> = [];
120
- let outputLineNum = 0; // 0-indexed output line counter (after stripping markers)
121
-
122
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
123
- const line = lines[lineIdx];
124
-
125
- // Check for type annotation marker (@T:line:col)
126
- TYPE_MARKER_REGEX.lastIndex = 0;
127
- const typeMatch = TYPE_MARKER_REGEX.exec(line);
128
-
129
- if (typeMatch) {
130
- // This is a @T marker line - store the position for the next line
131
- pendingMarker = {
132
- line: parseInt(typeMatch[1], 10),
133
- col: parseInt(typeMatch[2], 10),
134
- };
135
- // Don't output this line or create a mapping for it
136
- continue;
137
- }
138
-
139
- // Check for line marker (@L:line)
140
- LINE_MARKER_REGEX.lastIndex = 0;
141
- const lineMatch = LINE_MARKER_REGEX.exec(line);
142
-
143
- if (lineMatch) {
144
- // This is a @L marker line - identity mapping (col 0)
145
- pendingMarker = {
146
- line: parseInt(lineMatch[1], 10),
147
- col: 0,
148
- };
149
- // Don't output this line or create a mapping for it
150
- continue;
151
- }
152
-
153
- // This is a code line - apply pending marker if any
154
- if (pendingMarker) {
155
- currentOrigLine = pendingMarker.line;
156
- currentOrigCol = pendingMarker.col;
157
- pendingMarker = null;
158
- }
159
-
160
- outputLineNum++;
161
-
162
- // Create mapping for this line
163
- mappings.push({
164
- generatedLine: outputLineNum,
165
- generatedCol: 0,
166
- originalLine: currentOrigLine,
167
- originalCol: currentOrigCol,
168
- });
169
- }
170
-
171
- // Strip all markers from the code
172
- const cleanCode = code.replace(ALL_MARKERS_REGEX, '');
173
-
174
- // Build VLQ-encoded source map
175
- const map = buildSourceMapFromMappings(mappings, fileName, originalSource, includeContent);
176
-
177
- return { code: cleanCode, map };
178
- }
179
-
180
- /**
181
- * Build a source map from a list of position mappings.
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.
182
7
  */
183
- function buildSourceMapFromMappings(
184
- mappings: Array<{ generatedLine: number; generatedCol: number; originalLine: number; originalCol: number }>,
185
- fileName: string,
186
- originalSource: string,
187
- includeContent: boolean
188
- ): EncodedSourceMap {
189
- // Group mappings by generated line
190
- const lineMap = new Map<number, Array<{ generatedCol: number; originalLine: number; originalCol: number }>>();
191
- for (const m of mappings) {
192
- if (!lineMap.has(m.generatedLine)) {
193
- lineMap.set(m.generatedLine, []);
194
- }
195
- lineMap.get(m.generatedLine)!.push({
196
- generatedCol: m.generatedCol,
197
- originalLine: m.originalLine,
198
- originalCol: m.originalCol,
199
- });
200
- }
201
-
202
- // Build VLQ-encoded mappings string
203
- const maxLine = Math.max(...mappings.map(m => m.generatedLine), 0);
204
- const mappingLines: string[] = [];
205
-
206
- let prevGenCol = 0;
207
- let prevOrigLine = 0;
208
- let prevOrigCol = 0;
209
-
210
- for (let line = 1; line <= maxLine; line++) {
211
- const lineMappings = lineMap.get(line);
212
- if (!lineMappings || lineMappings.length === 0) {
213
- mappingLines.push('');
214
- continue;
215
- }
216
-
217
- // Sort by generated column
218
- lineMappings.sort((a, b) => a.generatedCol - b.generatedCol);
219
-
220
- const segments: string[] = [];
221
- prevGenCol = 0; // Reset for each line
222
-
223
- for (const m of lineMappings) {
224
- // VLQ encode: [genCol, sourceIdx=0, origLine, origCol]
225
- const segment = vlqEncode([
226
- m.generatedCol - prevGenCol,
227
- 0, // source index (we only have one source)
228
- (m.originalLine - 1) - prevOrigLine, // 0-based, relative
229
- m.originalCol - prevOrigCol,
230
- ]);
231
- segments.push(segment);
232
-
233
- prevGenCol = m.generatedCol;
234
- prevOrigLine = m.originalLine - 1;
235
- prevOrigCol = m.originalCol;
236
- }
237
-
238
- mappingLines.push(segments.join(','));
239
- }
240
-
241
- const map: EncodedSourceMap = {
242
- version: 3,
243
- file: fileName,
244
- sources: [fileName],
245
- names: [],
246
- mappings: mappingLines.join(';'),
247
- };
248
-
249
- if (includeContent) {
250
- map.sourcesContent = [originalSource];
251
- }
252
-
253
- return map;
254
- }
255
-
256
- // VLQ encoding for source maps
257
- const VLQ_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
258
- const VLQ_BASE = 32;
259
- const VLQ_CONTINUATION_BIT = 32;
260
-
261
- function vlqEncode(values: number[]): string {
262
- return values.map(vlqEncodeInteger).join('');
263
- }
264
8
 
265
- function vlqEncodeInteger(value: number): string {
266
- let result = '';
267
- let vlq = value < 0 ? ((-value) << 1) | 1 : value << 1;
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'
268
13
 
269
- do {
270
- let digit = vlq & 31;
271
- vlq >>>= 5;
272
- if (vlq > 0) {
273
- digit |= VLQ_CONTINUATION_BIT;
274
- }
275
- result += VLQ_BASE64[digit];
276
- } while (vlq > 0);
277
-
278
- return result;
279
- }
280
-
281
- export interface TransformContext {
282
- ts: typeof ts;
283
- factory: ts.NodeFactory;
284
- context: ts.TransformationContext;
285
- sourceFile: ts.SourceFile;
286
- }
287
-
288
- /**
289
- * Internal state for a single file transformation.
290
- * Passed between visitor functions to track mutable state.
291
- */
292
- interface FileTransformState {
293
- needsTypiaImport: boolean;
14
+ export interface TransformResult {
15
+ code: string
16
+ map: RawSourceMap | null
294
17
  }
295
18
 
296
19
  export class TypicalTransformer {
297
- public config: TypicalConfig;
298
- private program: ts.Program;
299
- private ts: typeof ts;
300
- private compiledPatterns: CompiledIgnorePatterns | null = null;
301
- private typeValidators = new Map<
302
- string,
303
- { name: string; typeNode: ts.TypeNode }
304
- >(); // type -> { validator variable name, type node }
305
- private typeStringifiers = new Map<
306
- string,
307
- { name: string; typeNode: ts.TypeNode }
308
- >(); // type -> { stringifier variable name, type node }
309
- private typeParsers = new Map<
310
- string,
311
- { name: string; typeNode: ts.TypeNode }
312
- >(); // type -> { parser variable name, type node }
313
-
314
- constructor(
315
- config?: TypicalConfig,
316
- program?: ts.Program,
317
- tsInstance?: typeof ts
318
- ) {
319
- this.config = config ?? loadConfig();
320
- this.ts = tsInstance ?? ts;
321
- this.program = program ?? setupTsProgram(this.ts);
322
- }
323
-
324
- /**
325
- * Create a new TypeScript program with transformed source code.
326
- * This is needed so typia can resolve types from our generated typia.createAssert<T>() calls.
327
- */
328
- private createTypiaProgram(
329
- fileName: string,
330
- transformedCode: string,
331
- languageVersion: ts.ScriptTarget = this.ts.ScriptTarget.ES2020
332
- ): { newProgram: ts.Program; boundSourceFile: ts.SourceFile } {
333
- // Create a new source file from the transformed code
334
- const newSourceFile = this.ts.createSourceFile(
335
- fileName,
336
- transformedCode,
337
- languageVersion,
338
- true
339
- );
340
-
341
- // Build map of all source files, replacing the transformed one
342
- const compilerOptions = this.program.getCompilerOptions();
343
- const originalSourceFiles = new Map<string, ts.SourceFile>();
344
- for (const sf of this.program.getSourceFiles()) {
345
- originalSourceFiles.set(sf.fileName, sf);
346
- }
347
- originalSourceFiles.set(fileName, newSourceFile);
348
-
349
- // Create custom compiler host that serves our transformed file
350
- const customHost: ts.CompilerHost = {
351
- getSourceFile: (hostFileName, langVersion) => {
352
- if (originalSourceFiles.has(hostFileName)) {
353
- return originalSourceFiles.get(hostFileName);
354
- }
355
- return this.ts.createSourceFile(
356
- hostFileName,
357
- this.ts.sys.readFile(hostFileName) || "",
358
- langVersion,
359
- true
360
- );
361
- },
362
- getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
363
- writeFile: () => {},
364
- getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
365
- getCanonicalFileName: (fn) =>
366
- this.ts.sys.useCaseSensitiveFileNames ? fn : fn.toLowerCase(),
367
- useCaseSensitiveFileNames: () => this.ts.sys.useCaseSensitiveFileNames,
368
- getNewLine: () => this.ts.sys.newLine,
369
- fileExists: (fn) => originalSourceFiles.has(fn) || this.ts.sys.fileExists(fn),
370
- readFile: (fn) => this.ts.sys.readFile(fn),
371
- };
372
-
373
- // Create new program, passing oldProgram to reuse dependency context
374
- const newProgram = this.ts.createProgram(
375
- Array.from(originalSourceFiles.keys()),
376
- compilerOptions,
377
- customHost,
378
- this.program
379
- );
380
-
381
- // Get the bound source file from the new program (has proper symbol tables)
382
- const boundSourceFile = newProgram.getSourceFile(fileName);
383
- if (!boundSourceFile) {
384
- throw new Error(`Failed to get bound source file: ${fileName}`);
385
- }
386
-
387
- return { newProgram, boundSourceFile };
388
- }
389
-
390
- /**
391
- * Write intermediate file for debugging purposes.
392
- * Creates a .typical.ts file showing the code after typical's transformations
393
- * but before typia processes it.
394
- */
395
- private writeIntermediateFile(fileName: string, code: string): void {
396
- if (!this.config.debug?.writeIntermediateFiles) {
397
- return;
398
- }
399
-
400
- const compilerOptions = this.program.getCompilerOptions();
401
- const outDir = compilerOptions.outDir || ".";
402
- const rootDir = compilerOptions.rootDir || ".";
403
-
404
- const relativePath = path.relative(rootDir, fileName);
405
- const intermediateFileName = relativePath.replace(/\.(tsx?)$/, ".typical.$1");
406
- const intermediateFilePath = path.join(outDir, intermediateFileName);
407
-
408
- const dir = path.dirname(intermediateFilePath);
409
- if (!fs.existsSync(dir)) {
410
- fs.mkdirSync(dir, { recursive: true });
411
- }
412
-
413
- fs.writeFileSync(intermediateFilePath, code);
414
- console.log(`TYPICAL: Wrote intermediate file: ${intermediateFilePath}`);
415
- }
416
-
417
- /**
418
- * Format typia diagnostic errors into readable error messages.
419
- */
420
- private formatTypiaErrors(errors: ts.Diagnostic[]): string[] {
421
- return errors.map(d => {
422
- const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
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
423
25
 
424
- if (d.file && d.start !== undefined && d.length !== undefined) {
425
- const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
426
- // Extract the actual source code that caused the error
427
- const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
428
- // Truncate long snippets
429
- const snippet = sourceSnippet.length > 100
430
- ? sourceSnippet.substring(0, 100) + '...'
431
- : sourceSnippet;
432
-
433
- // Format the error message - extract type issues from typia's verbose output
434
- const formattedIssues = this.formatTypiaError(fullMessage);
435
-
436
- return `${d.file.fileName}:${line + 1}:${character + 1}\n` +
437
- ` Code: ${snippet}\n` +
438
- formattedIssues;
439
- }
440
- return this.formatTypiaError(fullMessage);
441
- });
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() })
442
30
  }
443
31
 
444
32
  /**
445
- * Check for untransformed typia calls and throw an error if found.
446
- * This is a fallback in case typia silently fails without reporting a diagnostic.
33
+ * Ensure the Go compiler is started and project is loaded.
34
+ * Uses lazy initialization - only starts on first transform.
447
35
  */
448
- private checkUntransformedTypiaCalls(code: string, fileName: string): void {
449
- const untransformedCalls = this.findUntransformedTypiaCalls(code);
450
- if (untransformedCalls.length > 0) {
451
- const failedTypes = untransformedCalls.map(c => c.type).filter((v, i, a) => a.indexOf(v) === i);
452
- throw new Error(
453
- `TYPICAL: Failed to transform the following types (typia cannot process them):\n` +
454
- failedTypes.map(t => ` - ${t}`).join('\n') +
455
- `\n\nTo skip validation for these types, add to ignoreTypes in typical.json:\n` +
456
- ` "ignoreTypes": [${failedTypes.map(t => `"${t}"`).join(', ')}]` +
457
- `\n\nFile: ${fileName}`
458
- );
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
+ })()
459
42
  }
460
- }
461
-
462
- public createSourceFile(fileName: string, content: string): ts.SourceFile {
463
- return this.ts.createSourceFile(
464
- fileName,
465
- content,
466
- this.ts.ScriptTarget.ES2020,
467
- true
468
- );
43
+ await this.initPromise
469
44
  }
470
45
 
471
46
  /**
472
- * Transform options for controlling source map generation.
473
- */
474
- public transform(
475
- sourceFile: ts.SourceFile | string,
476
- mode: "basic" | "typia" | "js",
477
- options: {
478
- sourceMap?: boolean;
479
- skippedTypes?: Set<string>;
480
- } = {}
481
- ): TransformResult {
482
- const { sourceMap = false, skippedTypes = new Set() } = options;
483
-
484
- if (typeof sourceFile === "string") {
485
- const file = this.program.getSourceFile(sourceFile);
486
- if (!file) {
487
- throw new Error(`Source file not found in program: ${sourceFile}`);
488
- }
489
- sourceFile = file;
490
- }
491
-
492
- const fileName = sourceFile.fileName;
493
- const originalSource = sourceFile.getFullText();
494
- const printer = this.ts.createPrinter();
495
- const includeContent = this.config.sourceMap?.includeContent ?? true;
496
-
497
- // Phase 1: typical's own transformations (adds source map markers as comments)
498
- const typicalTransformer = this.getTypicalOnlyTransformer(skippedTypes);
499
- const phase1Result = this.ts.transform(sourceFile, [typicalTransformer]);
500
- let transformedCode = printer.printFile(phase1Result.transformed[0]);
501
- if (process.env.DEBUG) {
502
- console.log("TYPICAL: After phase1 print (first 500):", transformedCode.substring(0, 500));
503
- console.log("TYPICAL: Contains //@T:", transformedCode.includes("//@T:"));
504
- }
505
- phase1Result.dispose();
506
-
507
- if (mode === "basic") {
508
- // For basic mode, parse markers and build source map, then strip markers
509
- if (sourceMap) {
510
- const { code, map } = parseMarkersAndBuildSourceMap(
511
- transformedCode,
512
- fileName,
513
- originalSource,
514
- includeContent
515
- );
516
- return { code, map };
517
- }
518
- // No source map requested - just strip markers
519
- return {
520
- code: transformedCode.replace(ALL_MARKERS_REGEX, ''),
521
- map: null,
522
- };
523
- }
524
-
525
- // Phase 2: if code has typia calls, run typia transformer in its own context
526
- // The markers survive through typia since they're comments
527
- if (transformedCode.includes("typia.")) {
528
- const result = this.applyTypiaTransform(sourceFile.fileName, transformedCode, printer);
529
- if (typeof result === 'object' && 'retry' in result && result.retry) {
530
- // Typia failed on a type - add to skipped and retry the whole transform
531
- skippedTypes.add(result.failedType);
532
- // Clear validator caches since we're retrying
533
- this.typeValidators.clear();
534
- this.typeStringifiers.clear();
535
- this.typeParsers.clear();
536
- return this.transform(sourceFile, mode, { sourceMap, skippedTypes });
537
- }
538
- transformedCode = (result as { code: string }).code;
539
- }
540
-
541
- if (mode === "typia") {
542
- // For typia mode, parse markers and build source map, then strip markers
543
- if (sourceMap) {
544
- const { code, map } = parseMarkersAndBuildSourceMap(
545
- transformedCode,
546
- fileName,
547
- originalSource,
548
- includeContent
549
- );
550
- return { code, map };
551
- }
552
- // No source map requested - just strip markers
553
- return {
554
- code: transformedCode.replace(ALL_MARKERS_REGEX, ''),
555
- map: null,
556
- };
557
- }
558
-
559
- // Mode "js" - first parse markers to build our source map, then transpile
560
- let typicalMap: EncodedSourceMap | null = null;
561
- if (sourceMap) {
562
- const parsed = parseMarkersAndBuildSourceMap(
563
- transformedCode,
564
- fileName,
565
- originalSource,
566
- includeContent
567
- );
568
- transformedCode = parsed.code;
569
- typicalMap = parsed.map;
570
- } else {
571
- // Strip markers even without source map
572
- transformedCode = transformedCode.replace(ALL_MARKERS_REGEX, '');
573
- }
574
-
575
- // Transpile to JavaScript with source map support
576
- const compilerOptions = {
577
- ...this.program.getCompilerOptions(),
578
- sourceMap: sourceMap,
579
- inlineSourceMap: false,
580
- inlineSources: false,
581
- };
582
-
583
- const compileResult = ts.transpileModule(transformedCode, {
584
- compilerOptions,
585
- fileName,
586
- });
587
-
588
- // Compose the two source maps: typical -> original AND js -> typical
589
- if (sourceMap && typicalMap) {
590
- let jsMap: EncodedSourceMap | null = null;
591
- if (compileResult.sourceMapText) {
592
- try {
593
- jsMap = JSON.parse(compileResult.sourceMapText) as EncodedSourceMap;
594
- jsMap.sources = [fileName];
595
- } catch {
596
- // Failed to parse, continue without
597
- }
598
- }
599
-
600
- // Compose maps: jsMap traces JS->TS, typicalMap traces TS->original
601
- // Result traces JS->original
602
- const composedMap = composeSourceMaps([typicalMap, jsMap], fileName);
603
- if (composedMap && includeContent) {
604
- composedMap.sourcesContent = [originalSource];
605
- }
606
-
607
- return {
608
- code: compileResult.outputText,
609
- map: composedMap,
610
- };
611
- }
612
-
613
- return {
614
- code: compileResult.outputText,
615
- map: null,
616
- };
617
- }
618
-
619
- /**
620
- * Legacy transform method that returns just the code string.
621
- * @deprecated Use transform() with options.sourceMap instead
622
- */
623
- public transformCode(
624
- sourceFile: ts.SourceFile | string,
625
- mode: "basic" | "typia" | "js",
626
- skippedTypes: Set<string> = new Set()
627
- ): string {
628
- return this.transform(sourceFile, mode, { skippedTypes }).code;
629
- }
630
-
631
- /**
632
- * Apply typia transformation in a separate ts.transform() context.
633
- * This avoids mixing program contexts and eliminates the need for import recreation.
634
- * Returns either the transformed code, or a retry signal with the failed type.
635
- * Source map markers in the code are preserved through the typia transformation.
636
- */
637
- private applyTypiaTransform(
638
- fileName: string,
639
- code: string,
640
- printer: ts.Printer
641
- ): { code: string } | { retry: true; failedType: string } {
642
- this.writeIntermediateFile(fileName, code);
643
-
644
- if (process.env.DEBUG) {
645
- console.log("TYPICAL: Before typia transform (first 500 chars):", code.substring(0, 500));
646
- }
647
-
648
- // Create a new program with the transformed source file so typia can resolve types
649
- const { newProgram, boundSourceFile } = this.createTypiaProgram(fileName, code);
650
-
651
- // Collect typia diagnostics to detect transformation failures
652
- const diagnostics: ts.Diagnostic[] = [];
653
-
654
- // Create typia transformer with the new program
655
- const typiaTransformerFactory = typiaTransform(
656
- newProgram,
657
- {},
658
- {
659
- addDiagnostic(diag: ts.Diagnostic) {
660
- diagnostics.push(diag);
661
- if (process.env.DEBUG) {
662
- console.warn("Typia diagnostic:", diag);
663
- }
664
- return diagnostics.length - 1;
665
- },
666
- }
667
- );
668
-
669
- // Run typia's transformer in its own ts.transform() call
670
- const typiaResult = this.ts.transform(boundSourceFile, [typiaTransformerFactory]);
671
- let typiaTransformed = typiaResult.transformed[0];
672
- typiaResult.dispose();
673
-
674
- if (process.env.DEBUG) {
675
- const afterTypia = printer.printFile(typiaTransformed);
676
- console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
677
- }
678
-
679
- // Check for typia errors via diagnostics
680
- const errors = diagnostics.filter(d => d.category === this.ts.DiagnosticCategory.Error);
681
- if (errors.length > 0) {
682
- // Check if any error is due to Window/globalThis intersection (DOM types)
683
- for (const d of errors) {
684
- const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
685
- if (fullMessage.includes('Window & typeof globalThis') || fullMessage.includes('typeof globalThis')) {
686
- // Find the validator that failed - look for the type in the error
687
- // Error format: "Code: typia.createAssert<{ value: number; table: Table; }>()"
688
- if (d.file && d.start !== undefined && d.length !== undefined) {
689
- const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
690
- // Extract the type from typia.createAssert<TYPE>()
691
- const typeMatch = sourceSnippet.match(/typia\.\w+<([^>]+(?:<[^>]*>)*)>\(\)/);
692
- if (typeMatch) {
693
- const failedType = typeMatch[1].trim();
694
- console.warn(`TYPICAL: Skipping validation for type due to Window/globalThis (typia cannot process DOM types): ${failedType.substring(0, 100)}...`);
695
-
696
- // Add to ignored types and signal retry needed
697
- return { retry: true, failedType };
698
- }
699
- }
700
- }
701
- }
702
-
703
- // No retryable errors, throw the original error
704
- const errorMessages = this.formatTypiaErrors(errors);
705
- throw new Error(
706
- `TYPICAL: Typia transformation failed:\n\n${errorMessages.join('\n\n')}`
707
- );
708
- }
709
-
710
- // Hoist RegExp constructors to top-level constants for performance
711
- if (this.config.hoistRegex !== false) {
712
- // Need to run hoisting in a transform context
713
- const hoistResult = this.ts.transform(typiaTransformed, [
714
- (context) => (sf) => hoistRegexConstructors(sf, this.ts, context.factory)
715
- ]);
716
- typiaTransformed = hoistResult.transformed[0];
717
- hoistResult.dispose();
718
- }
719
-
720
- const finalCode = printer.printFile(typiaTransformed);
721
-
722
- // Check for untransformed typia calls as a fallback
723
- this.checkUntransformedTypiaCalls(finalCode, fileName);
724
-
725
- // Source map markers (@T:line:col) are preserved through typia transformation
726
- // and will be parsed later in the transform() method
727
- return { code: finalCode };
728
- }
729
-
730
- /**
731
- * Get a transformer that only applies typical's transformations (no typia).
732
- * @param skippedTypes Set of type strings to skip validation for (used for retry after typia errors)
733
- */
734
- private getTypicalOnlyTransformer(skippedTypes: Set<string> = new Set()): ts.TransformerFactory<ts.SourceFile> {
735
- return (context: ts.TransformationContext) => {
736
- const factory = context.factory;
737
- const typeChecker = this.program.getTypeChecker();
738
-
739
- return (sourceFile: ts.SourceFile) => {
740
- // Check if this file should be transformed based on include/exclude patterns
741
- if (!this.shouldTransformFile(sourceFile.fileName)) {
742
- return sourceFile;
743
- }
744
-
745
- if (process.env.DEBUG) {
746
- console.log("TYPICAL: processing ", sourceFile.fileName);
747
- }
748
-
749
- const transformContext: TransformContext = {
750
- ts: this.ts,
751
- factory,
752
- context,
753
- sourceFile,
754
- };
755
-
756
- return this.transformSourceFile(sourceFile, transformContext, typeChecker, skippedTypes);
757
- };
758
- };
759
- }
760
-
761
- /**
762
- * Get a combined transformer for use with ts-patch/ttsc.
763
- * This is used by the TSC plugin where we need a single transformer factory.
47
+ * Transform a TypeScript file by adding runtime validation.
764
48
  *
765
- * Note: Even for ts-patch, we need to create a new program with the transformed
766
- * source so typia can resolve the types from our generated typia.createAssert<T>() calls.
767
- */
768
- public getTransformer(withTypia: boolean): ts.TransformerFactory<ts.SourceFile> {
769
- return (context: ts.TransformationContext) => {
770
- const factory = context.factory;
771
- const typeChecker = this.program.getTypeChecker();
772
-
773
- return (sourceFile: ts.SourceFile) => {
774
- // Check if this file should be transformed based on include/exclude patterns
775
- if (!this.shouldTransformFile(sourceFile.fileName)) {
776
- return sourceFile;
777
- }
778
-
779
- if (process.env.DEBUG) {
780
- console.log("TYPICAL: processing ", sourceFile.fileName);
781
- }
782
-
783
- const transformContext: TransformContext = {
784
- ts: this.ts,
785
- factory,
786
- context,
787
- sourceFile,
788
- };
789
-
790
- // Apply typical's transformations
791
- let transformedSourceFile = this.transformSourceFile(
792
- sourceFile,
793
- transformContext,
794
- typeChecker
795
- );
796
-
797
- if (!withTypia) {
798
- return transformedSourceFile;
799
- }
800
-
801
- // Print the transformed code to check for typia calls
802
- const printer = this.ts.createPrinter();
803
- const transformedCode = printer.printFile(transformedSourceFile);
804
-
805
- if (!transformedCode.includes("typia.")) {
806
- return transformedSourceFile;
807
- }
808
-
809
- this.writeIntermediateFile(sourceFile.fileName, transformedCode);
810
-
811
- if (process.env.DEBUG) {
812
- console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
813
- }
814
-
815
- // Create a new program with the transformed source file so typia can resolve types
816
- const { newProgram, boundSourceFile } = this.createTypiaProgram(
817
- sourceFile.fileName,
818
- transformedCode,
819
- sourceFile.languageVersion
820
- );
821
-
822
- // Collect typia diagnostics to detect transformation failures
823
- const diagnostics: ts.Diagnostic[] = [];
824
-
825
- // Create typia transformer with the new program
826
- const typiaTransformerFactory = typiaTransform(
827
- newProgram,
828
- {},
829
- {
830
- addDiagnostic(diag: ts.Diagnostic) {
831
- diagnostics.push(diag);
832
- if (process.env.DEBUG) {
833
- console.warn("Typia diagnostic:", diag);
834
- }
835
- return diagnostics.length - 1;
836
- },
837
- }
838
- );
839
-
840
- // Apply typia's transformer to the bound source file
841
- const typiaNodeTransformer = typiaTransformerFactory(context);
842
- transformedSourceFile = typiaNodeTransformer(boundSourceFile);
843
-
844
- if (process.env.DEBUG) {
845
- const afterTypia = printer.printFile(transformedSourceFile);
846
- console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
847
- }
848
-
849
- // Check for typia errors via diagnostics
850
- const errors = diagnostics.filter(d => d.category === this.ts.DiagnosticCategory.Error);
851
- if (errors.length > 0) {
852
- const errorMessages = this.formatTypiaErrors(errors);
853
- throw new Error(
854
- `TYPICAL: Typia transformation failed:\n\n${errorMessages.join('\n\n')}`
855
- );
856
- }
857
-
858
- // Hoist RegExp constructors to top-level constants for performance
859
- if (this.config.hoistRegex !== false) {
860
- transformedSourceFile = hoistRegexConstructors(
861
- transformedSourceFile,
862
- this.ts,
863
- factory
864
- );
865
- }
866
-
867
- // Check for untransformed typia calls as a fallback
868
- const finalCode = printer.printFile(transformedSourceFile);
869
- this.checkUntransformedTypiaCalls(finalCode, sourceFile.fileName);
870
-
871
- return transformedSourceFile;
872
- };
873
- };
874
- }
875
-
876
- /**
877
- * Transform JSON.stringify or JSON.parse calls to use typia's validated versions.
878
- * Returns the transformed node if applicable, or undefined to indicate no transformation.
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
879
52
  */
880
- private transformJSONCall(
881
- node: ts.CallExpression,
882
- ctx: TransformContext,
883
- typeChecker: ts.TypeChecker,
884
- shouldSkipType: (typeText: string) => boolean
885
- ): ts.Node | undefined {
886
- const { ts, factory } = ctx;
887
- const propertyAccess = node.expression as ts.PropertyAccessExpression;
888
-
889
- if (propertyAccess.name.text === "stringify") {
890
- // For stringify, we need to infer the type from the argument
891
- // First check if the argument type is 'any' - if so, skip transformation
892
- if (node.arguments.length > 0) {
893
- const arg = node.arguments[0];
894
- const argType = typeChecker.getTypeAtLocation(arg);
895
- if (this.isAnyOrUnknownTypeFlags(argType)) {
896
- return undefined; // Don't transform JSON.stringify for any/unknown types
897
- }
898
- }
899
-
900
- if (this.config.reusableValidators) {
901
- // Infer type from argument
902
- const arg = node.arguments[0];
903
- const { typeText, typeNode } = this.inferStringifyType(arg, typeChecker, ctx);
904
-
905
- const stringifierName = this.getOrCreateStringifier(typeText, typeNode);
906
- return factory.createCallExpression(
907
- factory.createIdentifier(stringifierName),
908
- undefined,
909
- node.arguments
910
- );
911
- } else {
912
- // Use inline typia.json.stringify
913
- return factory.updateCallExpression(
914
- node,
915
- factory.createPropertyAccessExpression(
916
- factory.createPropertyAccessExpression(
917
- factory.createIdentifier("typia"),
918
- "json"
919
- ),
920
- "stringify"
921
- ),
922
- node.typeArguments,
923
- node.arguments
924
- );
925
- }
926
- } else if (propertyAccess.name.text === "parse") {
927
- // For JSON.parse, we need to infer the expected type from context
928
- // Check if this is part of a variable declaration or type assertion
929
- let targetType: ts.TypeNode | undefined;
930
-
931
- // Look for type annotations in parent nodes
932
- let parent = node.parent;
933
- while (parent) {
934
- if (ts.isVariableDeclaration(parent) && parent.type) {
935
- targetType = parent.type;
936
- break;
937
- } else if (ts.isAsExpression(parent)) {
938
- targetType = parent.type;
939
- break;
940
- } else if (ts.isReturnStatement(parent)) {
941
- // Look for function return type
942
- let funcParent = parent.parent;
943
- while (funcParent) {
944
- if (
945
- (ts.isFunctionDeclaration(funcParent) ||
946
- ts.isArrowFunction(funcParent) ||
947
- ts.isMethodDeclaration(funcParent)) &&
948
- funcParent.type
949
- ) {
950
- targetType = funcParent.type;
951
- break;
952
- }
953
- funcParent = funcParent.parent;
954
- }
955
- break;
956
- } else if (ts.isArrowFunction(parent) && parent.type) {
957
- // Arrow function with expression body (not block)
958
- // e.g., (s: string): User => JSON.parse(s)
959
- targetType = parent.type;
960
- break;
961
- }
962
- parent = parent.parent;
963
- }
964
-
965
- if (targetType && this.isAnyOrUnknownType(targetType)) {
966
- // Don't transform JSON.parse for any/unknown types
967
- return undefined;
968
- }
969
-
970
- // If we can't determine the target type and there's no explicit type argument,
971
- // don't transform - we can't validate against an unknown type
972
- if (!targetType && !node.typeArguments) {
973
- return undefined;
974
- }
975
-
976
- if (this.config.reusableValidators && targetType) {
977
- // Use reusable parser - use typeNode text to preserve local aliases
978
- const typeText = this.getTypeKey(targetType, typeChecker);
979
-
980
- // Skip types that failed in typia (retry mechanism)
981
- if (shouldSkipType(typeText)) {
982
- if (process.env.DEBUG) {
983
- console.log(`TYPICAL: Skipping previously failed type for JSON.parse: ${typeText}`);
984
- }
985
- return undefined;
986
- }
987
-
988
- const parserName = this.getOrCreateParser(typeText, targetType);
989
-
990
- return factory.createCallExpression(
991
- factory.createIdentifier(parserName),
992
- undefined,
993
- node.arguments
994
- );
995
- } else {
996
- // Use inline typia.json.assertParse
997
- const typeArguments = targetType
998
- ? [targetType]
999
- : node.typeArguments;
1000
-
1001
- return factory.updateCallExpression(
1002
- node,
1003
- factory.createPropertyAccessExpression(
1004
- factory.createPropertyAccessExpression(
1005
- factory.createIdentifier("typia"),
1006
- "json"
1007
- ),
1008
- "assertParse"
1009
- ),
1010
- typeArguments,
1011
- node.arguments
1012
- );
1013
- }
1014
- }
1015
-
1016
- return undefined;
1017
- }
1018
-
1019
- /**
1020
- * Check if a type should be skipped (failed in typia on previous attempt).
1021
- */
1022
- private shouldSkipType(typeText: string, skippedTypes: Set<string>): boolean {
1023
- if (skippedTypes.size === 0) return false;
1024
- // Normalize: remove all whitespace and semicolons for comparison
1025
- const normalize = (s: string) => s.replace(/[\s;]+/g, '').toLowerCase();
1026
- const normalized = normalize(typeText);
1027
- for (const skipped of skippedTypes) {
1028
- const skippedNormalized = normalize(skipped);
1029
- if (normalized === skippedNormalized || normalized.includes(skippedNormalized) || skippedNormalized.includes(normalized)) {
1030
- if (process.env.DEBUG) {
1031
- console.log(`TYPICAL: Matched skipped type: "${typeText.substring(0, 50)}..." matches "${skipped.substring(0, 50)}..."`);
1032
- }
1033
- return true;
1034
- }
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')
1035
56
  }
1036
- return false;
1037
- }
1038
-
1039
- /**
1040
- * Create an AST visitor function for transforming a source file.
1041
- * The visitor handles JSON calls, type casts, and function declarations.
1042
- */
1043
- private createVisitor(
1044
- ctx: TransformContext,
1045
- typeChecker: ts.TypeChecker,
1046
- skippedTypes: Set<string>,
1047
- state: FileTransformState
1048
- ): (node: ts.Node) => ts.Node {
1049
- const { ts } = ctx;
1050
- const shouldSkipType = (typeText: string) => this.shouldSkipType(typeText, skippedTypes);
1051
-
1052
- // Forward declaration for mutual recursion
1053
- let transformFunction: (func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration) => ts.Node;
1054
-
1055
- const visit = (node: ts.Node): ts.Node => {
1056
- // Transform JSON calls first (before they get wrapped in functions)
1057
- if (
1058
- ts.isCallExpression(node) &&
1059
- ts.isPropertyAccessExpression(node.expression)
1060
- ) {
1061
- const propertyAccess = node.expression;
1062
- if (
1063
- ts.isIdentifier(propertyAccess.expression) &&
1064
- propertyAccess.expression.text === "JSON"
1065
- ) {
1066
- const transformed = this.transformJSONCall(node, ctx, typeChecker, shouldSkipType);
1067
- if (transformed) {
1068
- state.needsTypiaImport = true;
1069
- return transformed;
1070
- }
1071
- return node;
1072
- }
1073
- }
1074
-
1075
- // Transform type assertions (as expressions) when validateCasts is enabled
1076
- // e.g., `obj as User` becomes `__typical_assert_N(obj)`
1077
- if (this.config.validateCasts && ts.isAsExpression(node)) {
1078
- const targetType = node.type;
1079
-
1080
- // Skip 'as any' and 'as unknown' casts - these are intentional escapes
1081
- if (this.isAnyOrUnknownType(targetType)) {
1082
- return ctx.ts.visitEachChild(node, visit, ctx.context);
1083
- }
1084
-
1085
- // Skip primitive types - no runtime validation needed
1086
- if (targetType.kind === ts.SyntaxKind.StringKeyword ||
1087
- targetType.kind === ts.SyntaxKind.NumberKeyword ||
1088
- targetType.kind === ts.SyntaxKind.BooleanKeyword) {
1089
- return ctx.ts.visitEachChild(node, visit, ctx.context);
1090
- }
1091
-
1092
- // Skip types matching ignoreTypes patterns (including classes extending DOM types)
1093
- const typeText = this.getTypeKey(targetType, typeChecker);
1094
- const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
1095
- if (this.isIgnoredType(typeText, typeChecker, targetTypeObj)) {
1096
- if (process.env.DEBUG) {
1097
- console.log(`TYPICAL: Skipping ignored type for cast: ${typeText}`);
1098
- }
1099
- return ctx.ts.visitEachChild(node, visit, ctx.context);
1100
- }
1101
-
1102
- // Skip types that failed in typia (retry mechanism)
1103
- if (shouldSkipType(typeText)) {
1104
- if (process.env.DEBUG) {
1105
- console.log(`TYPICAL: Skipping previously failed type for cast: ${typeText}`);
1106
- }
1107
- return ctx.ts.visitEachChild(node, visit, ctx.context);
1108
- }
1109
-
1110
- state.needsTypiaImport = true;
1111
-
1112
- // Visit the expression first to transform any nested casts
1113
- const visitedExpression = ctx.ts.visitNode(node.expression, visit) as ts.Expression;
1114
-
1115
- if (this.config.reusableValidators) {
1116
- // Use typeNode text to preserve local aliases
1117
- const typeText = this.getTypeKey(targetType, typeChecker);
1118
- const validatorName = this.getOrCreateValidator(typeText, targetType);
1119
-
1120
- // Replace `expr as Type` with `__typical_assert_N(expr)`
1121
- return ctx.factory.createCallExpression(
1122
- ctx.factory.createIdentifier(validatorName),
1123
- undefined,
1124
- [visitedExpression]
1125
- );
1126
- } else {
1127
- // Inline validator: typia.assert<Type>(expr)
1128
- return ctx.factory.createCallExpression(
1129
- ctx.factory.createPropertyAccessExpression(
1130
- ctx.factory.createIdentifier("typia"),
1131
- "assert"
1132
- ),
1133
- [targetType],
1134
- [visitedExpression]
1135
- );
1136
- }
1137
- }
1138
-
1139
- // Transform function declarations
1140
- if (ts.isFunctionDeclaration(node)) {
1141
- state.needsTypiaImport = true;
1142
- return transformFunction(node);
1143
- }
1144
-
1145
- // Transform arrow functions
1146
- if (ts.isArrowFunction(node)) {
1147
- state.needsTypiaImport = true;
1148
- return transformFunction(node);
1149
- }
1150
-
1151
- // Transform method declarations
1152
- if (ts.isMethodDeclaration(node)) {
1153
- state.needsTypiaImport = true;
1154
- return transformFunction(node);
1155
- }
1156
-
1157
- return ctx.ts.visitEachChild(node, visit, ctx.context);
1158
- };
1159
-
1160
- transformFunction = (
1161
- func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration
1162
- ): ts.Node => {
1163
- const body = func.body;
1164
-
1165
- // For arrow functions with expression bodies (not blocks),
1166
- // still visit the expression to transform JSON calls etc.
1167
- // Also handle Promise return types with .then() validation
1168
- if (body && !ts.isBlock(body) && ts.isArrowFunction(func)) {
1169
- let visitedBody = ctx.ts.visitNode(body, visit) as ts.Expression;
1170
-
1171
- // Check if this is a non-async function with Promise return type
1172
- let returnType = func.type;
1173
- let returnTypeForString: ts.Type | undefined;
1174
-
1175
- if (returnType) {
1176
- returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
1177
- } else {
1178
- // Try to infer the return type from the signature
1179
- try {
1180
- const signature = typeChecker.getSignatureFromDeclaration(func);
1181
- if (signature) {
1182
- const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
1183
- returnType = typeChecker.typeToTypeNode(
1184
- inferredReturnType,
1185
- func,
1186
- TYPE_NODE_FLAGS
1187
- );
1188
- returnTypeForString = inferredReturnType;
1189
- }
1190
- } catch {
1191
- // Skip inference
1192
- }
1193
- }
1194
-
1195
- // Check for Promise<T> return type
1196
- if (returnType && returnTypeForString) {
1197
- const promiseSymbol = returnTypeForString.getSymbol();
1198
- if (promiseSymbol && promiseSymbol.getName() === "Promise") {
1199
- const typeArgs = (returnTypeForString as ts.TypeReference).typeArguments;
1200
- if (typeArgs && typeArgs.length > 0) {
1201
- const innerType = typeArgs[0];
1202
- let innerTypeNode: ts.TypeNode | undefined;
1203
-
1204
- if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
1205
- innerTypeNode = returnType.typeArguments[0];
1206
- } else {
1207
- innerTypeNode = typeChecker.typeToTypeNode(
1208
- innerType,
1209
- func,
1210
- TYPE_NODE_FLAGS
1211
- );
1212
- }
1213
-
1214
- // Only add validation if validateFunctions is enabled
1215
- if (this.config.validateFunctions !== false && innerTypeNode && !this.isAnyOrUnknownType(innerTypeNode)) {
1216
- const innerTypeText = this.getTypeKey(innerTypeNode, typeChecker, innerType);
1217
- if (!this.isIgnoredType(innerTypeText, typeChecker, innerType) && !shouldSkipType(innerTypeText)) {
1218
- // Wrap expression with .then(validator)
1219
- const validatorName = this.config.reusableValidators
1220
- ? this.getOrCreateValidator(innerTypeText, innerTypeNode)
1221
- : null;
1222
-
1223
- if (validatorName) {
1224
- state.needsTypiaImport = true;
1225
- visitedBody = ctx.factory.createCallExpression(
1226
- ctx.factory.createPropertyAccessExpression(
1227
- visitedBody,
1228
- ctx.factory.createIdentifier("then")
1229
- ),
1230
- undefined,
1231
- [ctx.factory.createIdentifier(validatorName)]
1232
- );
1233
- }
1234
- }
1235
- }
1236
- }
1237
- }
1238
- }
1239
-
1240
- if (visitedBody !== body) {
1241
- return ctx.factory.updateArrowFunction(
1242
- func,
1243
- func.modifiers,
1244
- func.typeParameters,
1245
- func.parameters,
1246
- func.type,
1247
- func.equalsGreaterThanToken,
1248
- visitedBody
1249
- );
1250
- }
1251
- return func;
1252
- }
1253
-
1254
- if (!body || !ts.isBlock(body)) return func;
1255
-
1256
- // Track validated variables (params and consts with type annotations)
1257
- const validatedVariables = new Map<string, ts.Type>();
1258
-
1259
- // Add parameter validation (only if validateFunctions is enabled)
1260
- const validationStatements: ts.Statement[] = [];
1261
-
1262
- // Skip parameter validation if validateFunctions is disabled
1263
- const shouldValidateFunctions = this.config.validateFunctions !== false;
1264
-
1265
- func.parameters.forEach((param) => {
1266
- if (shouldValidateFunctions && param.type) {
1267
- // Skip 'any' and 'unknown' types - no point validating them
1268
- if (this.isAnyOrUnknownType(param.type)) {
1269
- return;
1270
- }
1271
-
1272
- // Skip types matching ignoreTypes patterns (including classes extending DOM types)
1273
- const typeText = this.getTypeKey(param.type, typeChecker);
1274
- const paramType = typeChecker.getTypeFromTypeNode(param.type);
1275
-
1276
- // Skip type parameters (generics) - can't be validated at runtime
1277
- if (this.isAnyOrUnknownTypeFlags(paramType)) {
1278
- if (process.env.DEBUG) {
1279
- console.log(`TYPICAL: Skipping type parameter/any for parameter: ${typeText}`);
1280
- }
1281
- return;
1282
- }
1283
-
1284
- if (process.env.DEBUG) {
1285
- console.log(`TYPICAL: Processing parameter type: ${typeText}`);
1286
- }
1287
- if (this.isIgnoredType(typeText, typeChecker, paramType)) {
1288
- if (process.env.DEBUG) {
1289
- console.log(`TYPICAL: Skipping ignored type for parameter: ${typeText}`);
1290
- }
1291
- return;
1292
- }
1293
-
1294
- // Skip types that failed in typia (retry mechanism)
1295
- if (shouldSkipType(typeText)) {
1296
- if (process.env.DEBUG) {
1297
- console.log(`TYPICAL: Skipping previously failed type for parameter: ${typeText}`);
1298
- }
1299
- return;
1300
- }
1301
-
1302
- const paramName = ts.isIdentifier(param.name)
1303
- ? param.name.text
1304
- : "param";
1305
- const paramIdentifier = ctx.factory.createIdentifier(paramName);
1306
-
1307
- // Track this parameter as validated for flow analysis
1308
- validatedVariables.set(paramName, paramType);
1309
-
1310
- if (this.config.reusableValidators) {
1311
- // Use reusable validators - use typeNode text to preserve local aliases
1312
- const validatorName = this.getOrCreateValidator(
1313
- typeText,
1314
- param.type
1315
- );
1316
-
1317
- const validatorCall = ctx.factory.createCallExpression(
1318
- ctx.factory.createIdentifier(validatorName),
1319
- undefined,
1320
- [paramIdentifier]
1321
- );
1322
- let assertCall: ts.Statement =
1323
- ctx.factory.createExpressionStatement(validatorCall);
1324
-
1325
- // Add source map marker pointing to the parameter's type annotation
1326
- assertCall = addSourceMapMarker(assertCall, ctx.sourceFile, param.type!);
1327
-
1328
- validationStatements.push(assertCall);
1329
- } else {
1330
- // Use inline typia.assert calls
1331
- const typiaIdentifier = ctx.factory.createIdentifier("typia");
1332
- const assertIdentifier = ctx.factory.createIdentifier("assert");
1333
- const propertyAccess = ctx.factory.createPropertyAccessExpression(
1334
- typiaIdentifier,
1335
- assertIdentifier
1336
- );
1337
- const callExpression = ctx.factory.createCallExpression(
1338
- propertyAccess,
1339
- [param.type],
1340
- [paramIdentifier]
1341
- );
1342
- let assertCall: ts.Statement =
1343
- ctx.factory.createExpressionStatement(callExpression);
1344
-
1345
- // Add source map marker pointing to the parameter's type annotation
1346
- assertCall = addSourceMapMarker(assertCall, ctx.sourceFile, param.type!);
1347
57
 
1348
- validationStatements.push(assertCall);
1349
- }
1350
- }
1351
- });
58
+ await this.ensureInitialized()
1352
59
 
1353
- // First visit all child nodes (including JSON calls) before adding validation
1354
- const visitedBody = ctx.ts.visitNode(body, visit) as ts.Block;
60
+ const resolvedPath = resolve(fileName)
61
+ const result = await this.compiler.transformFile(this.projectHandle!, resolvedPath)
1355
62
 
1356
- // Also track const declarations with type annotations as validated
1357
- // (the assignment will be validated, and const can't be reassigned)
1358
- const collectConstDeclarations = (node: ts.Node): void => {
1359
- if (ts.isVariableStatement(node)) {
1360
- const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
1361
- if (isConst) {
1362
- for (const decl of node.declarationList.declarations) {
1363
- if (decl.type && ts.isIdentifier(decl.name)) {
1364
- // Skip any/unknown types
1365
- if (!this.isAnyOrUnknownType(decl.type)) {
1366
- const constType = typeChecker.getTypeFromTypeNode(decl.type);
1367
- validatedVariables.set(decl.name.text, constType);
1368
- }
1369
- }
1370
- }
1371
- }
1372
- }
1373
- ts.forEachChild(node, collectConstDeclarations);
1374
- };
1375
- collectConstDeclarations(visitedBody);
1376
-
1377
- // Transform return statements - use explicit type or infer from type checker
1378
- let transformedStatements = visitedBody.statements;
1379
- let returnType = func.type;
1380
-
1381
- // Check if this is an async function
1382
- const isAsync = func.modifiers?.some(
1383
- (mod) => mod.kind === ts.SyntaxKind.AsyncKeyword
1384
- );
1385
-
1386
- // If no explicit return type, try to infer it from the type checker
1387
- let returnTypeForString: ts.Type | undefined;
1388
- if (!returnType) {
1389
- try {
1390
- const signature = typeChecker.getSignatureFromDeclaration(func);
1391
- if (signature) {
1392
- const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
1393
- returnType = typeChecker.typeToTypeNode(
1394
- inferredReturnType,
1395
- func,
1396
- TYPE_NODE_FLAGS
1397
- );
1398
- returnTypeForString = inferredReturnType;
1399
- }
1400
- } catch {
1401
- // Could not infer signature (e.g., untyped arrow function callback)
1402
- // Skip return type validation for this function
1403
- }
1404
- } else {
1405
- // For explicit return types, get the Type from the TypeNode
1406
- returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
1407
- }
1408
-
1409
- // Handle Promise return types
1410
- // Track whether this is a non-async function returning Promise (needs .then() wrapper)
1411
- let isNonAsyncPromiseReturn = false;
1412
-
1413
- if (returnType && returnTypeForString) {
1414
- const promiseSymbol = returnTypeForString.getSymbol();
1415
- if (promiseSymbol && promiseSymbol.getName() === "Promise") {
1416
- // Unwrap Promise<T> to get T for validation
1417
- const typeArgs = (returnTypeForString as ts.TypeReference).typeArguments;
1418
- if (typeArgs && typeArgs.length > 0) {
1419
- returnTypeForString = typeArgs[0];
1420
- // Also update the TypeNode to match
1421
- if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
1422
- returnType = returnType.typeArguments[0];
1423
- } else {
1424
- // Create a new type node from the unwrapped type
1425
- returnType = typeChecker.typeToTypeNode(
1426
- returnTypeForString,
1427
- func,
1428
- TYPE_NODE_FLAGS
1429
- );
1430
- }
1431
-
1432
- if (!isAsync) {
1433
- // For non-async functions returning Promise, we'll use .then(validator)
1434
- isNonAsyncPromiseReturn = true;
1435
- if (process.env.DEBUG) {
1436
- console.log(`TYPICAL: Non-async Promise return type - will use .then() for validation`);
1437
- }
1438
- }
1439
- }
1440
- }
1441
- }
1442
-
1443
- // Skip 'any' and 'unknown' return types - no point validating them
1444
- // Also skip types matching ignoreTypes patterns (including classes extending DOM types)
1445
- // Also skip types containing type parameters or constructor types
1446
- const returnTypeText = returnType && returnTypeForString
1447
- ? this.getTypeKey(returnType, typeChecker, returnTypeForString)
1448
- : null;
1449
- if (process.env.DEBUG && returnTypeText) {
1450
- console.log(`TYPICAL: Checking return type: "${returnTypeText}" (isAsync: ${isAsync})`);
1451
- }
1452
-
1453
- // Skip if return type contains type parameters, constructor types, or is otherwise unvalidatable
1454
- const shouldSkipReturnType = returnTypeForString && this.containsUnvalidatableType(returnTypeForString);
1455
- if (shouldSkipReturnType && process.env.DEBUG) {
1456
- console.log(`TYPICAL: Skipping unvalidatable return type: ${returnTypeText}`);
1457
- }
1458
-
1459
- const isIgnoredReturnType = returnTypeText && this.isIgnoredType(returnTypeText, typeChecker, returnTypeForString);
1460
- if (isIgnoredReturnType && process.env.DEBUG) {
1461
- console.log(`TYPICAL: Skipping ignored type for return: ${returnTypeText}`);
1462
- }
1463
- const isSkippedReturnType = returnTypeText && shouldSkipType(returnTypeText);
1464
- if (isSkippedReturnType && process.env.DEBUG) {
1465
- console.log(`TYPICAL: Skipping previously failed type for return: ${returnTypeText}`);
1466
- }
1467
- if (shouldValidateFunctions && returnType && returnTypeForString && !this.isAnyOrUnknownType(returnType) && !isIgnoredReturnType && !shouldSkipReturnType && !isSkippedReturnType) {
1468
- const returnTransformer = (node: ts.Node): ts.Node => {
1469
- // Don't recurse into nested functions - they have their own return types
1470
- if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) ||
1471
- ts.isFunctionExpression(node) || ts.isMethodDeclaration(node)) {
1472
- return node;
1473
- }
1474
-
1475
- if (ts.isReturnStatement(node) && node.expression) {
1476
- // Skip return validation if the expression already contains a __typical _parse_* call
1477
- // since typia.assertParse already validates the parsed data
1478
- const containsTypicalParse = (n: ts.Node): boolean => {
1479
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
1480
- const name = n.expression.text;
1481
- if (name.startsWith("__typical" + "_parse_")) {
1482
- return true;
1483
- }
1484
- }
1485
- return ts.forEachChild(n, containsTypicalParse) || false;
1486
- };
1487
- if (containsTypicalParse(node.expression)) {
1488
- return node; // Already validated by parse, skip return validation
1489
- }
1490
-
1491
- // Flow analysis: Skip return validation if returning a validated variable
1492
- // (or property of one) that hasn't been tainted
1493
- const rootVar = this.getRootIdentifier(node.expression);
1494
- if (rootVar && validatedVariables.has(rootVar)) {
1495
- // Check if the variable has been tainted (mutated, passed to function, etc.)
1496
- if (!this.isTainted(rootVar, visitedBody)) {
1497
- // Return expression is rooted at a validated, untainted variable
1498
- // For direct returns (identifier) or property access, we can skip validation
1499
- if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
1500
- return node; // Skip validation - already validated and untainted
1501
- }
1502
- }
1503
- }
1504
-
1505
- // For non-async functions returning Promise, use .then(validator)
1506
- // For async functions, await the expression before validating
1507
- if (isNonAsyncPromiseReturn) {
1508
- // return expr.then(validator)
1509
- const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
1510
- const validatorName = this.config.reusableValidators
1511
- ? this.getOrCreateValidator(returnTypeText, returnType)
1512
- : null;
1513
-
1514
- // Create the validator reference (either reusable or inline)
1515
- let validatorExpr: ts.Expression;
1516
- if (validatorName) {
1517
- validatorExpr = ctx.factory.createIdentifier(validatorName);
1518
- } else {
1519
- // Inline: typia.assert<T>
1520
- validatorExpr = ctx.factory.createPropertyAccessExpression(
1521
- ctx.factory.createIdentifier("typia"),
1522
- ctx.factory.createIdentifier("assert")
1523
- );
1524
- // Note: For inline mode, we'd need to create a wrapper arrow function
1525
- // to pass the type argument. For simplicity, just use the property access
1526
- // and let typia handle it (though this won't work without type args)
1527
- // In practice, reusableValidators should be true for this to work well
1528
- }
1529
-
1530
- // expr.then(validator)
1531
- const thenCall = ctx.factory.createCallExpression(
1532
- ctx.factory.createPropertyAccessExpression(
1533
- node.expression,
1534
- ctx.factory.createIdentifier("then")
1535
- ),
1536
- undefined,
1537
- [validatorExpr]
1538
- );
1539
-
1540
- let updatedReturn = ctx.factory.updateReturnStatement(node, thenCall);
1541
- // Add source map marker pointing to the return type annotation
1542
- if (returnType && returnType.pos >= 0) {
1543
- updatedReturn = addSourceMapMarker(updatedReturn, ctx.sourceFile, returnType);
1544
- }
1545
- return updatedReturn;
1546
- }
1547
-
1548
- // For async functions, we need to await the expression before validating
1549
- // because the return expression might be a Promise
1550
- let expressionToValidate = node.expression;
1551
-
1552
- if (isAsync) {
1553
- // Check if the expression is already an await expression
1554
- const isAlreadyAwaited = ts.isAwaitExpression(node.expression);
1555
-
1556
- if (!isAlreadyAwaited) {
1557
- // Wrap in await: return validate(await expr)
1558
- expressionToValidate = ctx.factory.createAwaitExpression(node.expression);
1559
- }
1560
- }
1561
-
1562
- if (this.config.reusableValidators) {
1563
- // Use reusable validators - use typeNode text to preserve local aliases
1564
- // Pass returnTypeForString for synthesized nodes (inferred return types)
1565
- const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
1566
- const validatorName = this.getOrCreateValidator(
1567
- returnTypeText,
1568
- returnType
1569
- );
1570
-
1571
- const validatorCall = ctx.factory.createCallExpression(
1572
- ctx.factory.createIdentifier(validatorName),
1573
- undefined,
1574
- [expressionToValidate]
1575
- );
1576
-
1577
- let updatedReturn = ctx.factory.updateReturnStatement(node, validatorCall);
1578
- // Add source map marker pointing to the return type annotation
1579
- if (returnType && returnType.pos >= 0) {
1580
- updatedReturn = addSourceMapMarker(updatedReturn, ctx.sourceFile, returnType);
1581
- }
1582
- return updatedReturn;
1583
- } else {
1584
- // Use inline typia.assert calls
1585
- const typiaIdentifier = ctx.factory.createIdentifier("typia");
1586
- const assertIdentifier = ctx.factory.createIdentifier("assert");
1587
- const propertyAccess = ctx.factory.createPropertyAccessExpression(
1588
- typiaIdentifier,
1589
- assertIdentifier
1590
- );
1591
- const callExpression = ctx.factory.createCallExpression(
1592
- propertyAccess,
1593
- [returnType],
1594
- [expressionToValidate]
1595
- );
1596
-
1597
- let updatedReturn = ctx.factory.updateReturnStatement(node, callExpression);
1598
- // Add source map marker pointing to the return type annotation
1599
- if (returnType && returnType.pos >= 0) {
1600
- updatedReturn = addSourceMapMarker(updatedReturn, ctx.sourceFile, returnType);
1601
- }
1602
- return updatedReturn;
1603
- }
1604
- }
1605
- return ctx.ts.visitEachChild(node, returnTransformer, ctx.context);
1606
- };
1607
-
1608
- transformedStatements = ctx.ts.visitNodes(
1609
- visitedBody.statements,
1610
- returnTransformer
1611
- ) as ts.NodeArray<ts.Statement>;
1612
- }
1613
-
1614
- // Insert validation statements at the beginning
1615
- const newStatements = ctx.factory.createNodeArray([
1616
- ...validationStatements,
1617
- ...transformedStatements,
1618
- ]);
1619
- const newBody = ctx.factory.updateBlock(visitedBody, newStatements);
1620
-
1621
- if (ts.isFunctionDeclaration(func)) {
1622
- return ctx.factory.updateFunctionDeclaration(
1623
- func,
1624
- func.modifiers,
1625
- func.asteriskToken,
1626
- func.name,
1627
- func.typeParameters,
1628
- func.parameters,
1629
- func.type,
1630
- newBody
1631
- );
1632
- } else if (ts.isArrowFunction(func)) {
1633
- return ctx.factory.updateArrowFunction(
1634
- func,
1635
- func.modifiers,
1636
- func.typeParameters,
1637
- func.parameters,
1638
- func.type,
1639
- func.equalsGreaterThanToken,
1640
- newBody
1641
- );
1642
- } else if (ts.isMethodDeclaration(func)) {
1643
- return ctx.factory.updateMethodDeclaration(
1644
- func,
1645
- func.modifiers,
1646
- func.asteriskToken,
1647
- func.name,
1648
- func.questionToken,
1649
- func.typeParameters,
1650
- func.parameters,
1651
- func.type,
1652
- newBody
1653
- );
1654
- }
1655
-
1656
- return func;
1657
- };
1658
-
1659
- return visit;
1660
- }
1661
-
1662
- /**
1663
- * Transform a single source file with TypeScript AST
1664
- */
1665
- private transformSourceFile(
1666
- sourceFile: ts.SourceFile,
1667
- ctx: TransformContext,
1668
- typeChecker: ts.TypeChecker,
1669
- skippedTypes: Set<string> = new Set()
1670
- ): ts.SourceFile {
1671
- if (!sourceFile.fileName.includes('transformer.test.ts')) {
1672
- // Check if this file has already been transformed by us
1673
- const sourceText = sourceFile.getFullText();
1674
- if (sourceText.includes('__typical_' + 'assert_') || sourceText.includes('__typical_' + 'stringify_') || sourceText.includes('__typical_' + 'parse_')) {
1675
- throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
1676
- }
1677
- }
1678
-
1679
- // Reset caches for each file
1680
- this.typeValidators.clear();
1681
- this.typeStringifiers.clear();
1682
- this.typeParsers.clear();
1683
-
1684
- // Create state object to track mutable state across visitor calls
1685
- const state: FileTransformState = { needsTypiaImport: false };
1686
-
1687
- // Create visitor and transform the source file
1688
- const visit = this.createVisitor(ctx, typeChecker, skippedTypes, state);
1689
- let transformedSourceFile = ctx.ts.visitNode(
1690
- sourceFile,
1691
- visit
1692
- ) as ts.SourceFile;
1693
-
1694
- // Add typia import and validator statements if needed
1695
- if (state.needsTypiaImport) {
1696
- transformedSourceFile = this.addTypiaImport(transformedSourceFile, ctx);
1697
-
1698
- // Add validator statements after imports (only if using reusable validators)
1699
- if (this.config.reusableValidators) {
1700
- const validatorStmts = this.createValidatorStatements(ctx);
1701
-
1702
- if (validatorStmts.length > 0) {
1703
- const importStatements = transformedSourceFile.statements.filter(
1704
- ctx.ts.isImportDeclaration
1705
- );
1706
- const otherStatements = transformedSourceFile.statements.filter(
1707
- (stmt) => !ctx.ts.isImportDeclaration(stmt)
1708
- );
1709
-
1710
- const newStatements = ctx.factory.createNodeArray([
1711
- ...importStatements,
1712
- ...validatorStmts,
1713
- ...otherStatements,
1714
- ]);
1715
-
1716
- transformedSourceFile = ctx.factory.updateSourceFile(
1717
- transformedSourceFile,
1718
- newStatements
1719
- );
1720
- }
1721
- }
1722
- }
1723
-
1724
- // Add line markers to original statements for source map identity mappings.
1725
- // This ensures original source lines map to themselves rather than inheriting
1726
- // from previous @T markers.
1727
- transformedSourceFile = this.addLineMarkersToStatements(transformedSourceFile, ctx, sourceFile);
1728
-
1729
- return transformedSourceFile;
1730
- }
1731
-
1732
- /**
1733
- * Add @L line markers to nodes that have original source positions.
1734
- * This preserves identity mappings for original code, so lines from the source
1735
- * file map back to themselves rather than inheriting from generated code markers.
1736
- *
1737
- * We need to add markers to every node that will be printed on its own line,
1738
- * including nested members of interfaces, classes, etc.
1739
- */
1740
- private addLineMarkersToStatements(
1741
- transformedFile: ts.SourceFile,
1742
- ctx: TransformContext,
1743
- originalSourceFile: ts.SourceFile
1744
- ): ts.SourceFile {
1745
- const { ts, factory } = ctx;
1746
-
1747
- // Check if a node already has a marker comment
1748
- const hasMarker = (node: ts.Node): boolean => {
1749
- const existingComments = ts.getSyntheticLeadingComments(node);
1750
- return existingComments?.some(c =>
1751
- c.text.startsWith('@T:') || c.text.startsWith('@L:')
1752
- ) ?? false;
1753
- };
1754
-
1755
- // Check if node has valid original position
1756
- const hasOriginalPosition = (node: ts.Node): boolean => {
1757
- return node.pos >= 0 && node.end > node.pos;
1758
- };
1759
-
1760
- // Recursively process a node and its children to add line markers
1761
- const addMarkersToNode = <T extends ts.Node>(node: T): T => {
1762
- // Handle interface declarations - add markers to members
1763
- if (ts.isInterfaceDeclaration(node)) {
1764
- const markedMembers = node.members.map(member => {
1765
- if (!hasMarker(member) && hasOriginalPosition(member)) {
1766
- return addLineMarker(member, originalSourceFile, member);
1767
- }
1768
- return member;
1769
- });
1770
- const updatedNode = factory.updateInterfaceDeclaration(
1771
- node,
1772
- node.modifiers,
1773
- node.name,
1774
- node.typeParameters,
1775
- node.heritageClauses,
1776
- markedMembers
1777
- );
1778
- // Also mark the interface itself
1779
- if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
1780
- return addLineMarker(updatedNode, originalSourceFile, node) as unknown as T;
1781
- }
1782
- return updatedNode as unknown as T;
1783
- }
1784
-
1785
- // Handle type alias declarations
1786
- if (ts.isTypeAliasDeclaration(node)) {
1787
- if (!hasMarker(node) && hasOriginalPosition(node)) {
1788
- return addLineMarker(node, originalSourceFile, node);
1789
- }
1790
- return node;
1791
- }
1792
-
1793
- // Handle class declarations - add markers to members
1794
- if (ts.isClassDeclaration(node)) {
1795
- const markedMembers = node.members.map(member => {
1796
- // Recursively process method bodies
1797
- let processedMember = member;
1798
- if (ts.isMethodDeclaration(member) && member.body) {
1799
- const markedBody = addMarkersToBlock(member.body);
1800
- if (markedBody !== member.body) {
1801
- processedMember = factory.updateMethodDeclaration(
1802
- member,
1803
- member.modifiers,
1804
- member.asteriskToken,
1805
- member.name,
1806
- member.questionToken,
1807
- member.typeParameters,
1808
- member.parameters,
1809
- member.type,
1810
- markedBody
1811
- );
1812
- }
1813
- }
1814
- if (!hasMarker(processedMember) && hasOriginalPosition(member)) {
1815
- return addLineMarker(processedMember, originalSourceFile, member);
1816
- }
1817
- return processedMember;
1818
- });
1819
- const updatedNode = factory.updateClassDeclaration(
1820
- node,
1821
- node.modifiers,
1822
- node.name,
1823
- node.typeParameters,
1824
- node.heritageClauses,
1825
- markedMembers
1826
- );
1827
- if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
1828
- return addLineMarker(updatedNode, originalSourceFile, node) as unknown as T;
1829
- }
1830
- return updatedNode as unknown as T;
1831
- }
1832
-
1833
- // Handle function declarations - add markers to body statements
1834
- if (ts.isFunctionDeclaration(node) && node.body) {
1835
- const markedBody = addMarkersToBlock(node.body);
1836
- const updatedNode = factory.updateFunctionDeclaration(
1837
- node,
1838
- node.modifiers,
1839
- node.asteriskToken,
1840
- node.name,
1841
- node.typeParameters,
1842
- node.parameters,
1843
- node.type,
1844
- markedBody
1845
- );
1846
- if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
1847
- return addLineMarker(updatedNode, originalSourceFile, node) as unknown as T;
1848
- }
1849
- return updatedNode as unknown as T;
1850
- }
1851
-
1852
- // Handle variable statements
1853
- if (ts.isVariableStatement(node)) {
1854
- if (!hasMarker(node) && hasOriginalPosition(node)) {
1855
- return addLineMarker(node, originalSourceFile, node);
1856
- }
1857
- return node;
1858
- }
1859
-
1860
- // Handle expression statements
1861
- if (ts.isExpressionStatement(node)) {
1862
- if (!hasMarker(node) && hasOriginalPosition(node)) {
1863
- return addLineMarker(node, originalSourceFile, node);
1864
- }
1865
- return node;
1866
- }
1867
-
1868
- // Handle return statements
1869
- if (ts.isReturnStatement(node)) {
1870
- if (!hasMarker(node) && hasOriginalPosition(node)) {
1871
- return addLineMarker(node, originalSourceFile, node);
1872
- }
1873
- return node;
1874
- }
1875
-
1876
- // Handle if statements
1877
- if (ts.isIfStatement(node)) {
1878
- let thenStmt = node.thenStatement;
1879
- let elseStmt = node.elseStatement;
1880
-
1881
- if (ts.isBlock(thenStmt)) {
1882
- thenStmt = addMarkersToBlock(thenStmt);
1883
- }
1884
- if (elseStmt && ts.isBlock(elseStmt)) {
1885
- elseStmt = addMarkersToBlock(elseStmt);
1886
- }
1887
-
1888
- const updatedNode = factory.updateIfStatement(node, node.expression, thenStmt, elseStmt);
1889
- if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
1890
- return addLineMarker(updatedNode, originalSourceFile, node) as unknown as T;
1891
- }
1892
- return updatedNode as unknown as T;
1893
- }
1894
-
1895
- // Default: just mark the node if it has original position
1896
- if (!hasMarker(node) && hasOriginalPosition(node)) {
1897
- return addLineMarker(node, originalSourceFile, node);
1898
- }
1899
-
1900
- return node;
1901
- };
1902
-
1903
- // Add markers to statements in a block
1904
- const addMarkersToBlock = (block: ts.Block): ts.Block => {
1905
- const markedStatements = block.statements.map(stmt => addMarkersToNode(stmt));
1906
- return factory.updateBlock(block, markedStatements);
1907
- };
1908
-
1909
- // Process all top-level statements
1910
- const newStatements = factory.createNodeArray(
1911
- transformedFile.statements.map(stmt => addMarkersToNode(stmt))
1912
- );
1913
-
1914
- return factory.updateSourceFile(transformedFile, newStatements);
1915
- }
1916
-
1917
- public shouldTransformFile(fileName: string): boolean {
1918
- return shouldTransformFile(fileName, this.config);
1919
- }
1920
-
1921
- /**
1922
- * Get pre-compiled ignore patterns, caching them for performance.
1923
- */
1924
- private getCompiledPatterns(): CompiledIgnorePatterns {
1925
- if (!this.compiledPatterns) {
1926
- this.compiledPatterns = getCompiledIgnorePatterns(this.config);
1927
- }
1928
- return this.compiledPatterns;
1929
- }
1930
-
1931
- /**
1932
- * Check if a TypeNode represents a type that shouldn't be validated.
1933
- * This includes:
1934
- * - any/unknown (intentional escape hatches)
1935
- * - Type parameters (generics like T)
1936
- * - Constructor types (new (...args: any[]) => T)
1937
- * - Function types ((...args) => T)
1938
- */
1939
- private isAnyOrUnknownType(typeNode: ts.TypeNode): boolean {
1940
- // any/unknown are escape hatches
1941
- if (typeNode.kind === this.ts.SyntaxKind.AnyKeyword ||
1942
- typeNode.kind === this.ts.SyntaxKind.UnknownKeyword) {
1943
- return true;
1944
- }
1945
- // Type parameters (generics) can't be validated at runtime
1946
- if (typeNode.kind === this.ts.SyntaxKind.TypeReference) {
1947
- const typeRef = typeNode as ts.TypeReferenceNode;
1948
- // Single identifier that's a type parameter
1949
- if (ts.isIdentifier(typeRef.typeName)) {
1950
- // Check if it's a type parameter by looking for it in enclosing type parameter lists
1951
- // For now, we'll check if it's a single uppercase letter or common generic names
1952
- const name = typeRef.typeName.text;
1953
- // Common type parameter names - single letters or common conventions
1954
- if (/^[A-Z]$/.test(name) || /^T[A-Z]?[a-z]*$/.test(name)) {
1955
- return true;
1956
- }
1957
- }
1958
- }
1959
- // Constructor types can't be validated by typia
1960
- if (typeNode.kind === this.ts.SyntaxKind.ConstructorType) {
1961
- return true;
1962
- }
1963
- // Function types generally shouldn't be validated
1964
- if (typeNode.kind === this.ts.SyntaxKind.FunctionType) {
1965
- return true;
1966
- }
1967
- return false;
1968
- }
1969
-
1970
- /**
1971
- * Check if a type contains any unvalidatable parts (type parameters, constructor types, etc.)
1972
- * This recursively checks intersection and union types.
1973
- */
1974
- private containsUnvalidatableType(type: ts.Type): boolean {
1975
- // Type parameters can't be validated at runtime
1976
- if ((type.flags & this.ts.TypeFlags.TypeParameter) !== 0) {
1977
- return true;
1978
- }
1979
-
1980
- // Check intersection types - if any part is unvalidatable, the whole thing is
1981
- if (type.isIntersection()) {
1982
- return type.types.some(t => this.containsUnvalidatableType(t));
1983
- }
1984
-
1985
- // Check union types
1986
- if (type.isUnion()) {
1987
- return type.types.some(t => this.containsUnvalidatableType(t));
1988
- }
1989
-
1990
- // Check for constructor signatures (like `new (...args) => T`)
1991
- const callSignatures = type.getConstructSignatures?.() ?? [];
1992
- if (callSignatures.length > 0) {
1993
- return true;
1994
- }
1995
-
1996
- return false;
1997
- }
1998
-
1999
- /**
2000
- * Check if a Type has any or unknown flags, or is a type parameter or function/constructor.
2001
- */
2002
- private isAnyOrUnknownTypeFlags(type: ts.Type): boolean {
2003
- // any/unknown
2004
- if ((type.flags & this.ts.TypeFlags.Any) !== 0 ||
2005
- (type.flags & this.ts.TypeFlags.Unknown) !== 0) {
2006
- return true;
2007
- }
2008
- // Type parameters (generics) - can't be validated at runtime
2009
- if ((type.flags & this.ts.TypeFlags.TypeParameter) !== 0) {
2010
- return true;
2011
- }
2012
- return false;
2013
- }
2014
-
2015
- /**
2016
- * Check if a type name matches any of the ignoreTypes patterns.
2017
- * Supports wildcards: "React.*" matches "React.FormEvent", "React.ChangeEvent", etc.
2018
- * Also handles union types: "Document | Element" is ignored if "Document" or "Element" is in ignoreTypes.
2019
- */
2020
- private isIgnoredType(typeName: string, typeChecker?: ts.TypeChecker, type?: ts.Type): boolean {
2021
- const compiled = this.getCompiledPatterns();
2022
- if (compiled.allPatterns.length === 0) return false;
2023
-
2024
- // For union types, check each constituent
2025
- if (type && type.isUnion()) {
2026
- const nonNullTypes = type.types.filter(t =>
2027
- !(t.flags & this.ts.TypeFlags.Null) && !(t.flags & this.ts.TypeFlags.Undefined)
2028
- );
2029
- if (nonNullTypes.length === 0) return false;
2030
- // All non-null types must be ignored
2031
- return nonNullTypes.every(t => this.isIgnoredSingleType(t, compiled.allPatterns, typeChecker));
2032
- }
2033
-
2034
- // For non-union types, check directly
2035
- if (type && typeChecker) {
2036
- return this.isIgnoredSingleType(type, compiled.allPatterns, typeChecker);
2037
- }
2038
-
2039
- // Fallback: string-based matching for union types like "Document | Element | null"
2040
- const typeParts = typeName.split(' | ').map(t => t.trim());
2041
- const nonNullParts = typeParts.filter(t => t !== 'null' && t !== 'undefined');
2042
- if (nonNullParts.length === 0) return false;
2043
-
2044
- return nonNullParts.every(part => this.matchesIgnorePatternCompiled(part, compiled.allPatterns));
2045
- }
2046
-
2047
- /**
2048
- * Check if a single type (not a union) should be ignored.
2049
- * Checks both the type name and its base classes.
2050
- * Uses Set-based cycle detection to handle recursive type hierarchies.
2051
- * @param patterns Pre-compiled RegExp patterns
2052
- * @param visited Set of type IDs already visited (for cycle detection)
2053
- */
2054
- private isIgnoredSingleType(
2055
- type: ts.Type,
2056
- patterns: RegExp[],
2057
- typeChecker?: ts.TypeChecker,
2058
- visited: Set<number> = new Set()
2059
- ): boolean {
2060
- // Use type ID for cycle detection (more precise than depth counter)
2061
- const typeId = (type as { id?: number }).id;
2062
- if (typeId !== undefined) {
2063
- if (visited.has(typeId)) {
2064
- if (process.env.DEBUG) {
2065
- console.log(`TYPICAL: Cycle detected for type "${type.symbol?.name || '?'}" (id: ${typeId}), skipping`);
2066
- }
2067
- return false; // Already visited this type, not ignored
2068
- }
2069
- visited.add(typeId);
2070
- }
2071
-
2072
- const typeName = type.symbol?.name || '';
2073
-
2074
- if (process.env.DEBUG) {
2075
- console.log(`TYPICAL: isIgnoredSingleType checking type: "${typeName}" (visited: ${visited.size})`);
2076
- }
2077
-
2078
- // Check direct name match
2079
- if (this.matchesIgnorePatternCompiled(typeName, patterns)) {
2080
- if (process.env.DEBUG) {
2081
- console.log(`TYPICAL: Type "${typeName}" matched ignore pattern directly`);
2082
- }
2083
- return true;
2084
- }
2085
-
2086
- // Check base classes (for classes extending DOM types like HTMLElement)
2087
- // This works for class types that have getBaseTypes available
2088
- const baseTypes = type.getBaseTypes?.() ?? [];
2089
- if (process.env.DEBUG && baseTypes.length > 0) {
2090
- console.log(`TYPICAL: Type "${typeName}" has ${baseTypes.length} base types: ${baseTypes.map(t => t.symbol?.name || '?').join(', ')}`);
2091
- }
2092
- for (const baseType of baseTypes) {
2093
- if (this.isIgnoredSingleType(baseType, patterns, typeChecker, visited)) {
2094
- if (process.env.DEBUG) {
2095
- console.log(`TYPICAL: Type "${typeName}" ignored because base type "${baseType.symbol?.name}" is ignored`);
2096
- }
2097
- return true;
2098
- }
2099
- }
2100
-
2101
- // Also check the declared type's symbol for heritage clauses (alternative approach)
2102
- // This handles cases where getBaseTypes doesn't return what we expect
2103
- if (type.symbol?.declarations) {
2104
- for (const decl of type.symbol.declarations) {
2105
- if (this.ts.isClassDeclaration(decl) && decl.heritageClauses) {
2106
- for (const heritage of decl.heritageClauses) {
2107
- if (heritage.token === this.ts.SyntaxKind.ExtendsKeyword) {
2108
- for (const heritageType of heritage.types) {
2109
- const baseTypeName = heritageType.expression.getText();
2110
- if (process.env.DEBUG) {
2111
- console.log(`TYPICAL: Type "${typeName}" extends "${baseTypeName}" (from heritage clause)`);
2112
- }
2113
- if (this.matchesIgnorePatternCompiled(baseTypeName, patterns)) {
2114
- if (process.env.DEBUG) {
2115
- console.log(`TYPICAL: Type "${typeName}" ignored because it extends "${baseTypeName}"`);
2116
- }
2117
- return true;
2118
- }
2119
- // Recursively check the heritage type
2120
- if (typeChecker) {
2121
- const heritageTypeObj = typeChecker.getTypeAtLocation(heritageType);
2122
- if (this.isIgnoredSingleType(heritageTypeObj, patterns, typeChecker, visited)) {
2123
- return true;
2124
- }
2125
-
2126
- // For mixin patterns like `extends VueWatcher(BaseElement)`, the expression is a CallExpression.
2127
- // We need to check the return type of the mixin function AND the arguments passed to it.
2128
- if (this.ts.isCallExpression(heritageType.expression)) {
2129
- // Check arguments to the mixin (e.g., BaseElement in VueWatcher(BaseElement))
2130
- for (const arg of heritageType.expression.arguments) {
2131
- const argType = typeChecker.getTypeAtLocation(arg);
2132
- if (process.env.DEBUG) {
2133
- console.log(`TYPICAL: Type "${typeName}" mixin arg: "${argType.symbol?.name}" (from call expression)`);
2134
- }
2135
- if (this.isIgnoredSingleType(argType, patterns, typeChecker, visited)) {
2136
- if (process.env.DEBUG) {
2137
- console.log(`TYPICAL: Type "${typeName}" ignored because mixin argument "${argType.symbol?.name}" is ignored`);
2138
- }
2139
- return true;
2140
- }
2141
- }
2142
- }
2143
- }
2144
- }
2145
- }
2146
- }
2147
- }
2148
- }
2149
- }
2150
-
2151
- return false;
2152
- }
2153
-
2154
- /**
2155
- * Check if a single type name matches any pre-compiled ignore pattern.
2156
- * @param patterns Pre-compiled RegExp patterns
2157
- */
2158
- private matchesIgnorePatternCompiled(typeName: string, patterns: RegExp[]): boolean {
2159
- return patterns.some(pattern => pattern.test(typeName));
2160
- }
2161
-
2162
- /**
2163
- * Find untransformed typia calls in the output code.
2164
- * These indicate types that typia could not process.
2165
- */
2166
- private findUntransformedTypiaCalls(code: string): Array<{ method: string; type: string }> {
2167
- const results: Array<{ method: string; type: string }> = [];
2168
-
2169
- // Match patterns like: typia.createAssert<Type>() or typia.json.createAssertParse<Type>()
2170
- // The type argument can contain nested generics like React.FormEvent<HTMLElement>
2171
- const patterns = [
2172
- /typia\.createAssert<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
2173
- /typia\.json\.createAssertParse<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
2174
- /typia\.json\.createStringify<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
2175
- ];
2176
-
2177
- for (const pattern of patterns) {
2178
- let match;
2179
- while ((match = pattern.exec(code)) !== null) {
2180
- const methodMatch = match[0].match(/typia\.([\w.]+)</);
2181
- results.push({
2182
- method: methodMatch ? methodMatch[1] : 'unknown',
2183
- type: match[1]
2184
- });
2185
- }
2186
- }
2187
-
2188
- return results;
2189
- }
2190
-
2191
- /**
2192
- * Infer type information from a JSON.stringify argument for creating a reusable stringifier.
2193
- */
2194
- private inferStringifyType(
2195
- arg: ts.Expression,
2196
- typeChecker: ts.TypeChecker,
2197
- ctx: TransformContext
2198
- ): { typeText: string; typeNode: ts.TypeNode } {
2199
- const ts = this.ts;
2200
-
2201
- // Type assertion: use the asserted type directly
2202
- if (ts.isAsExpression(arg)) {
2203
- const typeNode = arg.type;
2204
- const typeKey = this.getTypeKey(typeNode, typeChecker);
2205
- return { typeText: typeKey, typeNode };
2206
- }
2207
-
2208
- // Object literal: infer type from type checker
2209
- if (ts.isObjectLiteralExpression(arg)) {
2210
- const objectType = typeChecker.getTypeAtLocation(arg);
2211
- const typeNode = typeChecker.typeToTypeNode(objectType, arg, TYPE_NODE_FLAGS);
2212
- if (!typeNode) {
2213
- throw new Error('unknown type node for object literal: ' + arg.getText());
2214
- }
2215
- const typeKey = this.getTypeKey(typeNode, typeChecker, objectType);
2216
- return { typeText: typeKey, typeNode };
2217
- }
2218
-
2219
- // Other expressions: infer from type checker
2220
- const argType = typeChecker.getTypeAtLocation(arg);
2221
- const typeNode = typeChecker.typeToTypeNode(argType, arg, TYPE_NODE_FLAGS);
2222
- if (typeNode) {
2223
- const typeKey = this.getTypeKey(typeNode, typeChecker, argType);
2224
- return { typeText: typeKey, typeNode };
2225
- }
2226
-
2227
- // Fallback to unknown
2228
63
  return {
2229
- typeText: "unknown",
2230
- typeNode: ctx.factory.createKeywordTypeNode(ctx.ts.SyntaxKind.UnknownKeyword),
2231
- };
2232
- }
2233
-
2234
- // ============================================
2235
- // Flow Analysis Helpers
2236
- // ============================================
2237
-
2238
- /**
2239
- * Gets the root identifier from an expression.
2240
- * e.g., `user.address.city` -> "user"
2241
- */
2242
- private getRootIdentifier(expr: ts.Expression): string | undefined {
2243
- if (this.ts.isIdentifier(expr)) {
2244
- return expr.text;
2245
- }
2246
- if (this.ts.isPropertyAccessExpression(expr)) {
2247
- return this.getRootIdentifier(expr.expression);
2248
- }
2249
- return undefined;
2250
- }
2251
-
2252
- /**
2253
- * Check if a validated variable has been tainted (mutated) in the function body.
2254
- * A variable is tainted if it's reassigned, has properties modified, is passed
2255
- * to a function, has methods called on it, or if an await occurs.
2256
- */
2257
- private isTainted(varName: string, body: ts.Block): boolean {
2258
- let tainted = false;
2259
- const ts = this.ts;
2260
-
2261
- // Collect aliases (variables that reference properties of varName)
2262
- // e.g., const addr = user.address; -> addr is an alias
2263
- const aliases = new Set<string>([varName]);
2264
-
2265
- const collectAliases = (node: ts.Node): void => {
2266
- if (ts.isVariableStatement(node)) {
2267
- for (const decl of node.declarationList.declarations) {
2268
- if (ts.isIdentifier(decl.name) && decl.initializer) {
2269
- const initRoot = this.getRootIdentifier(decl.initializer);
2270
- if (initRoot && aliases.has(initRoot)) {
2271
- aliases.add(decl.name.text);
2272
- }
2273
- }
2274
- }
2275
- }
2276
- ts.forEachChild(node, collectAliases);
2277
- };
2278
- collectAliases(body);
2279
-
2280
- // Helper to check if any alias is involved
2281
- const involvesTrackedVar = (expr: ts.Expression): boolean => {
2282
- const root = this.getRootIdentifier(expr);
2283
- return root !== undefined && aliases.has(root);
2284
- };
2285
-
2286
- const checkTainting = (node: ts.Node): void => {
2287
- if (tainted) return;
2288
-
2289
- // Reassignment: trackedVar = ...
2290
- if (ts.isBinaryExpression(node) &&
2291
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
2292
- ts.isIdentifier(node.left) &&
2293
- aliases.has(node.left.text)) {
2294
- tainted = true;
2295
- return;
2296
- }
2297
-
2298
- // Property assignment: trackedVar.x = ... or alias.x = ...
2299
- if (ts.isBinaryExpression(node) &&
2300
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
2301
- ts.isPropertyAccessExpression(node.left) &&
2302
- involvesTrackedVar(node.left)) {
2303
- tainted = true;
2304
- return;
2305
- }
2306
-
2307
- // Element assignment: trackedVar[x] = ... or alias[x] = ...
2308
- if (ts.isBinaryExpression(node) &&
2309
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
2310
- ts.isElementAccessExpression(node.left) &&
2311
- involvesTrackedVar(node.left.expression)) {
2312
- tainted = true;
2313
- return;
2314
- }
2315
-
2316
- // Passed as argument to a function: fn(trackedVar) or fn(alias)
2317
- if (ts.isCallExpression(node)) {
2318
- for (const arg of node.arguments) {
2319
- let hasTrackedRef = false;
2320
- const checkRef = (n: ts.Node): void => {
2321
- if (ts.isIdentifier(n) && aliases.has(n.text)) {
2322
- hasTrackedRef = true;
2323
- }
2324
- ts.forEachChild(n, checkRef);
2325
- };
2326
- checkRef(arg);
2327
- if (hasTrackedRef) {
2328
- tainted = true;
2329
- return;
2330
- }
2331
- }
2332
- }
2333
-
2334
- // Method call on the variable: trackedVar.method() or alias.method()
2335
- if (ts.isCallExpression(node) &&
2336
- ts.isPropertyAccessExpression(node.expression) &&
2337
- involvesTrackedVar(node.expression.expression)) {
2338
- tainted = true;
2339
- return;
2340
- }
2341
-
2342
- // Await expression (async boundary - external code could run)
2343
- if (ts.isAwaitExpression(node)) {
2344
- tainted = true;
2345
- return;
2346
- }
2347
-
2348
- ts.forEachChild(node, checkTainting);
2349
- };
2350
-
2351
- checkTainting(body);
2352
- return tainted;
2353
- }
2354
-
2355
- private addTypiaImport(
2356
- sourceFile: ts.SourceFile,
2357
- ctx: TransformContext
2358
- ): ts.SourceFile {
2359
- const { factory } = ctx;
2360
-
2361
- const existingImports = sourceFile.statements.filter(
2362
- ctx.ts.isImportDeclaration
2363
- );
2364
- const hasTypiaImport = existingImports.some(
2365
- (imp) =>
2366
- imp.moduleSpecifier &&
2367
- ctx.ts.isStringLiteral(imp.moduleSpecifier) &&
2368
- imp.moduleSpecifier.text === "typia"
2369
- );
2370
-
2371
- if (!hasTypiaImport) {
2372
- const typiaImport = factory.createImportDeclaration(
2373
- undefined,
2374
- factory.createImportClause(
2375
- false,
2376
- factory.createIdentifier("typia"),
2377
- undefined
2378
- ),
2379
- factory.createStringLiteral("typia")
2380
- );
2381
-
2382
- const newSourceFile = factory.updateSourceFile(
2383
- sourceFile,
2384
- factory.createNodeArray([typiaImport, ...sourceFile.statements])
2385
- );
2386
-
2387
- return newSourceFile;
64
+ code: result.code,
65
+ map: result.sourceMap ?? null,
2388
66
  }
2389
-
2390
- return sourceFile;
2391
67
  }
2392
68
 
2393
69
  /**
2394
- * Gets type text for use as a validator map key.
2395
- * Uses getText() to preserve local aliases (e.g., "User1" vs "User2"),
2396
- * but falls back to typeToString() for synthesized nodes without source positions.
2397
- *
2398
- * @param typeNode The TypeNode to get a key for
2399
- * @param typeChecker The TypeChecker to use
2400
- * @param typeObj Optional Type object - use this for synthesized nodes since
2401
- * 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.
2402
72
  */
2403
- private getTypeKey(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker, typeObj?: ts.Type): string {
2404
- // Check if node has a real position (not synthesized)
2405
- if (typeNode.pos >= 0 && typeNode.end > typeNode.pos) {
2406
- try {
2407
- const text = typeNode.getText();
2408
- // Check for truncation patterns in source text (shouldn't happen but be safe)
2409
- if (!text.includes('...') || !text.match(/\.\.\.\d+\s+more/)) {
2410
- return text;
2411
- }
2412
- } catch {
2413
- // Fall through to typeToString
2414
- }
2415
- }
2416
- // Fallback for synthesized nodes - use the provided Type object if available,
2417
- // otherwise try to get it from the node (which may not work correctly)
2418
- const type = typeObj ?? typeChecker.getTypeFromTypeNode(typeNode);
2419
- const typeString = typeChecker.typeToString(type, undefined, this.ts.TypeFormatFlags.NoTruncation);
2420
-
2421
- // TypeScript may still truncate very large types even with NoTruncation flag.
2422
- // Detect truncation patterns like "...19 more..." and use a hash-based key instead.
2423
- if (typeString.match(/\.\.\.\d+\s+more/)) {
2424
- const hash = this.hashString(typeString);
2425
- return `__complex_type_${hash}`;
2426
- }
2427
-
2428
- return typeString;
2429
- }
2430
-
2431
- /**
2432
- * Simple string hash for creating unique identifiers from type strings.
2433
- */
2434
- private hashString(str: string): string {
2435
- let hash = 0;
2436
- for (let i = 0; i < str.length; i++) {
2437
- const char = str.charCodeAt(i);
2438
- hash = ((hash << 5) - hash) + char;
2439
- hash = hash & hash; // Convert to 32bit integer
2440
- }
2441
- return Math.abs(hash).toString(36);
2442
- }
2443
-
2444
- /**
2445
- * Format typia's error message into a cleaner list format.
2446
- * Typia outputs verbose messages like:
2447
- * "unsupported type detected\n\n- Window.ondevicemotion: unknown\n - nonsensible intersection\n\n- Window.ondeviceorientation..."
2448
- * We want to extract just the problematic types and their issues.
2449
- */
2450
- private formatTypiaError(message: string): string {
2451
- const lines = message.split('\n');
2452
- const firstLine = lines[0]; // e.g., "unsupported type detected"
2453
-
2454
- // Parse the error entries - each starts with "- " at the beginning of a line
2455
- const issues: { type: string; reasons: string[] }[] = [];
2456
- let currentIssue: { type: string; reasons: string[] } | null = null;
2457
-
2458
- for (const line of lines.slice(1)) {
2459
- if (line.startsWith('- ')) {
2460
- // New type entry
2461
- if (currentIssue) {
2462
- issues.push(currentIssue);
2463
- }
2464
- currentIssue = { type: line.slice(2), reasons: [] };
2465
- } else if (line.startsWith(' - ') && currentIssue) {
2466
- // Reason for current type
2467
- currentIssue.reasons.push(line.slice(4));
2468
- }
2469
- }
2470
- if (currentIssue) {
2471
- issues.push(currentIssue);
2472
- }
2473
-
2474
- if (issues.length === 0) {
2475
- return ` ${firstLine}`;
2476
- }
2477
-
2478
- // Limit to 5 issues, show count of remaining
2479
- const maxIssues = 5;
2480
- const displayIssues = issues.slice(0, maxIssues);
2481
- const remainingCount = issues.length - maxIssues;
2482
-
2483
- const formatted = displayIssues.map(issue => {
2484
- const reasons = issue.reasons.map(r => ` - ${r}`).join('\n');
2485
- return ` - ${issue.type}\n${reasons}`;
2486
- }).join('\n');
2487
-
2488
- const suffix = remainingCount > 0 ? `\n (and ${remainingCount} more errors)` : '';
2489
-
2490
- return ` ${firstLine}\n${formatted}${suffix}`;
2491
- }
2492
-
2493
-
2494
- /**
2495
- * Creates a readable name suffix from a type string.
2496
- * For simple identifiers like "User" or "string", returns the name directly.
2497
- * For complex types, returns a numeric index.
2498
- */
2499
- private getTypeNameSuffix(typeText: string, existingNames: Set<string>, fallbackIndex: number): string {
2500
- // Complex types from getTypeKey() - use numeric index
2501
- if (typeText.startsWith('__complex_type_')) {
2502
- return String(fallbackIndex);
2503
- }
2504
-
2505
- // Check if it's a simple identifier (letters, numbers, underscore, starting with letter/underscore)
2506
- if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(typeText)) {
2507
- // It's a simple type name like "User", "string", "MyType"
2508
- let name = typeText;
2509
- // Handle collisions by appending a number
2510
- if (existingNames.has(name)) {
2511
- let i = 2;
2512
- while (existingNames.has(`${typeText}${i}`)) {
2513
- i++;
2514
- }
2515
- name = `${typeText}${i}`;
2516
- }
2517
- return name;
2518
- }
2519
- // Complex type - use numeric index
2520
- return String(fallbackIndex);
2521
- }
2522
-
2523
- /**
2524
- * Generic method to get or create a typed function (validator, stringifier, or parser).
2525
- */
2526
- private getOrCreateTypedFunction(
2527
- kind: 'assert' | 'stringify' | 'parse',
2528
- typeText: string,
2529
- typeNode: ts.TypeNode
2530
- ): string {
2531
- const maps = {
2532
- assert: this.typeValidators,
2533
- stringify: this.typeStringifiers,
2534
- parse: this.typeParsers,
2535
- };
2536
- const prefixes = {
2537
- assert: '__typical_assert_',
2538
- stringify: '__typical_stringify_',
2539
- parse: '__typical_parse_',
2540
- };
2541
-
2542
- const map = maps[kind];
2543
- const prefix = prefixes[kind];
2544
-
2545
- if (map.has(typeText)) {
2546
- return map.get(typeText)!.name;
2547
- }
2548
-
2549
- const existingSuffixes = [...map.values()].map(v => v.name.slice(prefix.length));
2550
- const existingNames = new Set(existingSuffixes);
2551
- const numericCount = existingSuffixes.filter(s => /^\d+$/.test(s)).length;
2552
- const suffix = this.getTypeNameSuffix(typeText, existingNames, numericCount);
2553
- const name = `${prefix}${suffix}`;
2554
- map.set(typeText, { name, typeNode });
2555
- return name;
2556
- }
2557
-
2558
- private getOrCreateValidator(typeText: string, typeNode: ts.TypeNode): string {
2559
- return this.getOrCreateTypedFunction('assert', typeText, typeNode);
2560
- }
2561
-
2562
- private getOrCreateStringifier(typeText: string, typeNode: ts.TypeNode): string {
2563
- return this.getOrCreateTypedFunction('stringify', typeText, typeNode);
2564
- }
2565
-
2566
- private getOrCreateParser(typeText: string, typeNode: ts.TypeNode): string {
2567
- return this.getOrCreateTypedFunction('parse', typeText, typeNode);
2568
- }
2569
-
2570
- /**
2571
- * Creates a nested property access expression from an array of identifiers.
2572
- * e.g., ['typia', 'json', 'createStringify'] -> typia.json.createStringify
2573
- */
2574
- private createPropertyAccessChain(factory: ts.NodeFactory, parts: string[]): ts.Expression {
2575
- let expr: ts.Expression = factory.createIdentifier(parts[0]);
2576
- for (let i = 1; i < parts.length; i++) {
2577
- expr = factory.createPropertyAccessExpression(expr, parts[i]);
2578
- }
2579
- return expr;
2580
- }
2581
-
2582
- private createValidatorStatements(ctx: TransformContext): ts.Statement[] {
2583
- const { factory } = ctx;
2584
- const statements: ts.Statement[] = [];
2585
-
2586
- const configs: Array<{
2587
- map: Map<string, { name: string; typeNode: ts.TypeNode }>;
2588
- methodPath: string[];
2589
- }> = [
2590
- { map: this.typeValidators, methodPath: ['typia', 'createAssert'] },
2591
- { map: this.typeStringifiers, methodPath: ['typia', 'json', 'createStringify'] },
2592
- { map: this.typeParsers, methodPath: ['typia', 'json', 'createAssertParse'] },
2593
- ];
2594
-
2595
- for (const { map, methodPath } of configs) {
2596
- for (const [, { name, typeNode }] of map) {
2597
- const createCall = factory.createCallExpression(
2598
- this.createPropertyAccessChain(factory, methodPath),
2599
- [typeNode],
2600
- []
2601
- );
2602
-
2603
- let declaration: ts.Statement = factory.createVariableStatement(
2604
- undefined,
2605
- factory.createVariableDeclarationList(
2606
- [factory.createVariableDeclaration(name, undefined, undefined, createCall)],
2607
- ctx.ts.NodeFlags.Const
2608
- )
2609
- );
2610
-
2611
- // Add source map marker pointing to the type node that triggered this validator
2612
- // This ensures all the expanded typia validation code maps back to the original type
2613
- if (typeNode.pos >= 0) {
2614
- declaration = addSourceMapMarker(declaration, ctx.sourceFile, typeNode);
2615
- }
2616
-
2617
- statements.push(declaration);
2618
- }
2619
- }
2620
-
2621
- return statements;
73
+ async close(): Promise<void> {
74
+ this.projectHandle = null
75
+ this.initPromise = null
76
+ await this.compiler.close()
2622
77
  }
2623
78
  }