@elliots/typical 0.2.3 → 0.2.4

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