@elliots/typical 0.1.4 → 0.1.5

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,6 +1,9 @@
1
1
  import ts from "typescript";
2
+ import fs from "fs";
3
+ import path from "path";
2
4
  import { loadConfig, TypicalConfig } from "./config.js";
3
5
  import { shouldTransformFile } from "./file-filter.js";
6
+ import { hoistRegexConstructors } from "./regex-hoister.js";
4
7
 
5
8
  import { transform as typiaTransform } from "typia/lib/transform.js";
6
9
  import { setupTsProgram } from "./setup.js";
@@ -89,6 +92,10 @@ export class TypicalTransformer {
89
92
  };
90
93
 
91
94
  return (sourceFile: ts.SourceFile) => {
95
+ // Check if this file should be transformed based on include/exclude patterns
96
+ if (!this.shouldTransformFile(sourceFile.fileName)) {
97
+ return sourceFile; // Return unchanged for excluded files
98
+ }
92
99
 
93
100
  if (process.env.DEBUG) {
94
101
  console.log("TYPICAL: processing ", sourceFile.fileName);
@@ -112,6 +119,28 @@ export class TypicalTransformer {
112
119
  console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
113
120
  }
114
121
 
122
+ // Write intermediate file if debug option is enabled
123
+ if (this.config.debug?.writeIntermediateFiles) {
124
+ const compilerOptions = this.program.getCompilerOptions();
125
+ const outDir = compilerOptions.outDir || ".";
126
+ const rootDir = compilerOptions.rootDir || ".";
127
+
128
+ // Calculate the relative path from rootDir
129
+ const relativePath = path.relative(rootDir, sourceFile.fileName);
130
+ // Change extension to .typical.ts to indicate intermediate state
131
+ const intermediateFileName = relativePath.replace(/\.tsx?$/, ".typical.ts");
132
+ const intermediateFilePath = path.join(outDir, intermediateFileName);
133
+
134
+ // Ensure directory exists
135
+ const dir = path.dirname(intermediateFilePath);
136
+ if (!fs.existsSync(dir)) {
137
+ fs.mkdirSync(dir, { recursive: true });
138
+ }
139
+
140
+ fs.writeFileSync(intermediateFilePath, transformedCode);
141
+ console.log(`TYPICAL: Wrote intermediate file: ${intermediateFilePath}`);
142
+ }
143
+
115
144
  if (transformedCode.includes("typia.")) {
116
145
  try {
117
146
  // Create a new source file from our transformed code
@@ -200,7 +229,10 @@ export class TypicalTransformer {
200
229
  if (stmt.importClause?.isTypeOnly) {
201
230
  continue;
202
231
  }
203
- syntheticStatements.push(this.recreateImportDeclaration(stmt, factory));
232
+ const recreated = this.recreateImportDeclaration(stmt, factory, newProgram.getTypeChecker());
233
+ if (recreated) {
234
+ syntheticStatements.push(recreated);
235
+ }
204
236
  } else {
205
237
  syntheticStatements.push(stmt);
206
238
  }
@@ -216,6 +248,15 @@ export class TypicalTransformer {
216
248
  typiaTransformed.hasNoDefaultLib,
217
249
  typiaTransformed.libReferenceDirectives
218
250
  );
251
+
252
+ // Hoist RegExp constructors to top-level constants for performance
253
+ if (this.config.hoistRegex !== false) {
254
+ transformedSourceFile = hoistRegexConstructors(
255
+ transformedSourceFile,
256
+ this.ts,
257
+ factory
258
+ );
259
+ }
219
260
  } catch (error) {
220
261
  console.warn("Failed to apply typia transformer:", sourceFile.fileName, error);
221
262
  }
@@ -233,8 +274,9 @@ export class TypicalTransformer {
233
274
  */
234
275
  private recreateImportDeclaration(
235
276
  importDecl: ts.ImportDeclaration,
236
- factory: ts.NodeFactory
237
- ): ts.ImportDeclaration {
277
+ factory: ts.NodeFactory,
278
+ typeChecker: ts.TypeChecker
279
+ ): ts.ImportDeclaration | undefined {
238
280
  let importClause: ts.ImportClause | undefined;
239
281
 
240
282
  if (importDecl.importClause) {
@@ -249,20 +291,54 @@ export class TypicalTransformer {
249
291
  );
250
292
  } else if (this.ts.isNamedImports(clause.namedBindings)) {
251
293
  // import { foo, bar } from "baz"
252
- const elements = clause.namedBindings.elements.map((el) =>
253
- factory.createImportSpecifier(
254
- el.isTypeOnly,
255
- el.propertyName ? factory.createIdentifier(el.propertyName.text) : undefined,
256
- factory.createIdentifier(el.name.text)
257
- )
258
- );
259
- namedBindings = factory.createNamedImports(elements);
294
+ // Filter out type-only imports (explicit or inferred from symbol)
295
+ const elements = clause.namedBindings.elements
296
+ .filter((el) => {
297
+ // Skip explicit type-only specifiers
298
+ if (el.isTypeOnly) return false;
299
+ // Check if the symbol is type-only (interface, type alias, etc.)
300
+ let symbol = typeChecker.getSymbolAtLocation(el.name);
301
+ // Follow alias to get the actual exported symbol
302
+ if (symbol && symbol.flags & this.ts.SymbolFlags.Alias) {
303
+ symbol = typeChecker.getAliasedSymbol(symbol);
304
+ }
305
+ if (symbol) {
306
+ const declarations = symbol.getDeclarations();
307
+ if (declarations && declarations.length > 0) {
308
+ // If all declarations are type-only, skip this import
309
+ const allTypeOnly = declarations.every((decl) =>
310
+ this.ts.isInterfaceDeclaration(decl) ||
311
+ this.ts.isTypeAliasDeclaration(decl) ||
312
+ this.ts.isTypeLiteralNode(decl)
313
+ );
314
+ if (allTypeOnly) return false;
315
+ }
316
+ }
317
+ return true;
318
+ })
319
+ .map((el) =>
320
+ factory.createImportSpecifier(
321
+ false,
322
+ el.propertyName ? factory.createIdentifier(el.propertyName.text) : undefined,
323
+ factory.createIdentifier(el.name.text)
324
+ )
325
+ );
326
+ // Only create named imports if there are non-type specifiers
327
+ if (elements.length > 0) {
328
+ namedBindings = factory.createNamedImports(elements);
329
+ }
260
330
  }
261
331
  }
262
332
 
333
+ // Skip import entirely if no default import and no named bindings remain
334
+ const defaultName = clause.name ? factory.createIdentifier(clause.name.text) : undefined;
335
+ if (!defaultName && !namedBindings) {
336
+ return undefined;
337
+ }
338
+
263
339
  importClause = factory.createImportClause(
264
340
  clause.isTypeOnly,
265
- clause.name ? factory.createIdentifier(clause.name.text) : undefined,
341
+ defaultName,
266
342
  namedBindings
267
343
  );
268
344
  }
@@ -289,11 +365,6 @@ export class TypicalTransformer {
289
365
  ): ts.SourceFile {
290
366
  const { ts } = ctx;
291
367
 
292
- // Check if this file should be transformed
293
- if (!this.shouldTransformFile(sourceFile.fileName)) {
294
- return sourceFile; // Return unchanged for excluded files
295
- }
296
-
297
368
  if (!sourceFile.fileName.includes('transformer.test.ts')) {
298
369
  // Check if this file has already been transformed by us
299
370
  const sourceText = sourceFile.getFullText();
@@ -328,115 +399,22 @@ export class TypicalTransformer {
328
399
  if (node.arguments.length > 0) {
329
400
  const arg = node.arguments[0];
330
401
  const argType = typeChecker.getTypeAtLocation(arg);
331
- const typeFlags = argType.flags;
332
- // Skip if type is any (1) or unknown (2)
333
- if (typeFlags & ts.TypeFlags.Any || typeFlags & ts.TypeFlags.Unknown) {
402
+ if (this.isAnyOrUnknownTypeFlags(argType)) {
334
403
  return node; // Don't transform JSON.stringify for any/unknown types
335
404
  }
336
405
  }
337
406
 
338
407
  if (this.config.reusableValidators) {
339
- // For JSON.stringify, try to infer the type from the argument
340
- let typeText = "unknown";
341
- let typeNodeForCache: ts.TypeNode | undefined;
342
-
343
- if (node.arguments.length > 0) {
344
- const arg = node.arguments[0];
345
-
346
- // Check if it's a type assertion
347
- if (ts.isAsExpression(arg)) {
348
- // For type assertions, use the asserted type directly
349
- const assertedType = arg.type;
350
- const objectType =
351
- typeChecker.getTypeFromTypeNode(assertedType);
352
-
353
- const typeNode = assertedType;
354
-
355
- if (typeNode) {
356
- const typeString = typeChecker.typeToString(objectType);
357
- typeText = `Asserted_${typeString.replace(
358
- /[^a-zA-Z0-9_]/g,
359
- "_"
360
- )}`;
361
- typeNodeForCache = typeNode;
362
- } else {
363
- typeText = "unknown";
364
- typeNodeForCache = ctx.factory.createKeywordTypeNode(
365
- ctx.ts.SyntaxKind.UnknownKeyword
366
- );
367
- }
368
- } else if (ts.isObjectLiteralExpression(arg)) {
369
- // For object literals, use the type checker to get the actual type
370
- const objectType = typeChecker.getTypeAtLocation(arg);
371
-
372
- const typeNode = typeChecker.typeToTypeNode(
373
- objectType,
374
- arg,
375
- ts.NodeBuilderFlags.InTypeAlias
376
- );
377
-
378
- if (typeNode) {
379
- const propNames = arg.properties
380
- .map((prop) => {
381
- if (ts.isShorthandPropertyAssignment(prop)) {
382
- return prop.name.text;
383
- } else if (
384
- ts.isPropertyAssignment(prop) &&
385
- ts.isIdentifier(prop.name)
386
- ) {
387
- return prop.name.text;
388
- }
389
- return "unknown";
390
- })
391
- .sort()
392
- .join("_");
393
-
394
- typeText = `ObjectLiteral_${propNames}`;
395
- typeNodeForCache = typeNode;
396
- } else {
397
- // typeText = "unknown";
398
- // typeNodeForCache = ctx.factory.createKeywordTypeNode(
399
- // ctx.ts.SyntaxKind.UnknownKeyword
400
- // );
401
- throw new Error('unknown type node for object literal: ' + arg.getText());
402
- }
403
- } else {
404
- // For other expressions, try to get the type from the type checker
405
- const argType = typeChecker.getTypeAtLocation(arg);
406
-
407
- const typeNode = typeChecker.typeToTypeNode(
408
- argType,
409
- arg,
410
- ts.NodeBuilderFlags.InTypeAlias
411
- );
412
- if (typeNode) {
413
- const typeString = typeChecker.typeToString(argType);
414
- typeText = `Expression_${typeString.replace(
415
- /[^a-zA-Z0-9_]/g,
416
- "_"
417
- )}`;
418
- typeNodeForCache = typeNode;
419
- } else {
420
- typeText = "unknown";
421
- typeNodeForCache = ctx.factory.createKeywordTypeNode(
422
- ctx.ts.SyntaxKind.UnknownKeyword
423
- )
424
- }
425
- }
426
- }
427
-
428
- const stringifierName = this.getOrCreateStringifier(
429
- typeText,
430
- typeNodeForCache!
431
- );
408
+ // Infer type from argument
409
+ const arg = node.arguments[0];
410
+ const { typeText, typeNode } = this.inferStringifyType(arg, typeChecker, ctx);
432
411
 
433
- const newCall = ctx.factory.createCallExpression(
412
+ const stringifierName = this.getOrCreateStringifier(typeText, typeNode);
413
+ return ctx.factory.createCallExpression(
434
414
  ctx.factory.createIdentifier(stringifierName),
435
415
  undefined,
436
416
  node.arguments
437
417
  );
438
-
439
- return newCall;
440
418
  } else {
441
419
  // Use inline typia.json.stringify
442
420
  return ctx.factory.updateCallExpression(
@@ -491,13 +469,7 @@ export class TypicalTransformer {
491
469
  parent = parent.parent;
492
470
  }
493
471
 
494
- // Skip transformation if target type is any or unknown
495
- const isAnyOrUnknown = targetType && (
496
- targetType.kind === ts.SyntaxKind.AnyKeyword ||
497
- targetType.kind === ts.SyntaxKind.UnknownKeyword
498
- );
499
-
500
- if (isAnyOrUnknown) {
472
+ if (targetType && this.isAnyOrUnknownType(targetType)) {
501
473
  // Don't transform JSON.parse for any/unknown types
502
474
  return node;
503
475
  }
@@ -509,9 +481,8 @@ export class TypicalTransformer {
509
481
  }
510
482
 
511
483
  if (this.config.reusableValidators && targetType) {
512
- // Use reusable parser - use typeToString
513
- const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
514
- const typeText = typeChecker.typeToString(targetTypeObj);
484
+ // Use reusable parser - use typeNode text to preserve local aliases
485
+ const typeText = this.getTypeKey(targetType, typeChecker);
515
486
  const parserName = this.getOrCreateParser(typeText, targetType);
516
487
 
517
488
  const newCall = ctx.factory.createCallExpression(
@@ -544,6 +515,52 @@ export class TypicalTransformer {
544
515
  }
545
516
  }
546
517
 
518
+ // Transform type assertions (as expressions) when validateCasts is enabled
519
+ // e.g., `obj as User` becomes `__typical_assert_N(obj)`
520
+ if (this.config.validateCasts && ts.isAsExpression(node)) {
521
+ const targetType = node.type;
522
+
523
+ // Skip 'as any' and 'as unknown' casts - these are intentional escapes
524
+ if (this.isAnyOrUnknownType(targetType)) {
525
+ return ctx.ts.visitEachChild(node, visit, ctx.context);
526
+ }
527
+
528
+ // Skip primitive types - no runtime validation needed
529
+ if (targetType.kind === ts.SyntaxKind.StringKeyword ||
530
+ targetType.kind === ts.SyntaxKind.NumberKeyword ||
531
+ targetType.kind === ts.SyntaxKind.BooleanKeyword) {
532
+ return ctx.ts.visitEachChild(node, visit, ctx.context);
533
+ }
534
+
535
+ needsTypiaImport = true;
536
+
537
+ // Visit the expression first to transform any nested casts
538
+ const visitedExpression = ctx.ts.visitNode(node.expression, visit) as ts.Expression;
539
+
540
+ if (this.config.reusableValidators) {
541
+ // Use typeNode text to preserve local aliases
542
+ const typeText = this.getTypeKey(targetType, typeChecker);
543
+ const validatorName = this.getOrCreateValidator(typeText, targetType);
544
+
545
+ // Replace `expr as Type` with `__typical_assert_N(expr)`
546
+ return ctx.factory.createCallExpression(
547
+ ctx.factory.createIdentifier(validatorName),
548
+ undefined,
549
+ [visitedExpression]
550
+ );
551
+ } else {
552
+ // Inline validator: typia.assert<Type>(expr)
553
+ return ctx.factory.createCallExpression(
554
+ ctx.factory.createPropertyAccessExpression(
555
+ ctx.factory.createIdentifier("typia"),
556
+ "assert"
557
+ ),
558
+ [targetType],
559
+ [visitedExpression]
560
+ );
561
+ }
562
+ }
563
+
547
564
  // Transform function declarations
548
565
  if (ts.isFunctionDeclaration(node)) {
549
566
  needsTypiaImport = true;
@@ -565,139 +582,6 @@ export class TypicalTransformer {
565
582
  return ctx.ts.visitEachChild(node, visit, ctx.context);
566
583
  };
567
584
 
568
- // Helper functions for flow analysis
569
- const getRootIdentifier = (expr: ts.Expression): string | undefined => {
570
- if (ts.isIdentifier(expr)) {
571
- return expr.text;
572
- }
573
- if (ts.isPropertyAccessExpression(expr)) {
574
- return getRootIdentifier(expr.expression);
575
- }
576
- return undefined;
577
- };
578
-
579
- const containsReference = (expr: ts.Expression, name: string): boolean => {
580
- if (ts.isIdentifier(expr) && expr.text === name) {
581
- return true;
582
- }
583
- if (ts.isPropertyAccessExpression(expr)) {
584
- return containsReference(expr.expression, name);
585
- }
586
- if (ts.isElementAccessExpression(expr)) {
587
- return containsReference(expr.expression, name) ||
588
- containsReference(expr.argumentExpression as ts.Expression, name);
589
- }
590
- // Check all children
591
- let found = false;
592
- ts.forEachChild(expr, (child) => {
593
- if (ts.isExpression(child) && containsReference(child, name)) {
594
- found = true;
595
- }
596
- });
597
- return found;
598
- };
599
-
600
- // Check if a validated variable has been tainted in the function body
601
- const isTainted = (varName: string, body: ts.Block): boolean => {
602
- let tainted = false;
603
-
604
- // First pass: collect aliases (variables that reference properties of varName)
605
- // e.g., const addr = user.address; -> addr is an alias
606
- const aliases = new Set<string>([varName]);
607
-
608
- const collectAliases = (node: ts.Node): void => {
609
- // Look for: const/let x = varName.property or const/let x = varName
610
- if (ts.isVariableStatement(node)) {
611
- for (const decl of node.declarationList.declarations) {
612
- if (ts.isIdentifier(decl.name) && decl.initializer) {
613
- // Check if initializer is rooted at our tracked variable or any existing alias
614
- const initRoot = getRootIdentifier(decl.initializer);
615
- if (initRoot && aliases.has(initRoot)) {
616
- aliases.add(decl.name.text);
617
- }
618
- }
619
- }
620
- }
621
- ts.forEachChild(node, collectAliases);
622
- };
623
- collectAliases(body);
624
-
625
- // Helper to check if any alias is involved
626
- const involvesTrackedVar = (expr: ts.Expression): boolean => {
627
- const root = getRootIdentifier(expr);
628
- return root !== undefined && aliases.has(root);
629
- };
630
-
631
- const checkTainting = (node: ts.Node): void => {
632
- if (tainted) return; // Early exit if already tainted
633
-
634
- // Reassignment: trackedVar = ...
635
- if (ts.isBinaryExpression(node) &&
636
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
637
- ts.isIdentifier(node.left) &&
638
- aliases.has(node.left.text)) {
639
- tainted = true;
640
- return;
641
- }
642
-
643
- // Property assignment: trackedVar.x = ... or alias.x = ...
644
- if (ts.isBinaryExpression(node) &&
645
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
646
- ts.isPropertyAccessExpression(node.left) &&
647
- involvesTrackedVar(node.left)) {
648
- tainted = true;
649
- return;
650
- }
651
-
652
- // Element assignment: trackedVar[x] = ... or alias[x] = ...
653
- if (ts.isBinaryExpression(node) &&
654
- node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
655
- ts.isElementAccessExpression(node.left) &&
656
- involvesTrackedVar(node.left.expression)) {
657
- tainted = true;
658
- return;
659
- }
660
-
661
- // Passed as argument to a function: fn(trackedVar) or fn(alias)
662
- if (ts.isCallExpression(node)) {
663
- for (const arg of node.arguments) {
664
- // Check if any tracked variable or alias appears in the argument
665
- let hasTrackedRef = false;
666
- const checkRef = (n: ts.Node): void => {
667
- if (ts.isIdentifier(n) && aliases.has(n.text)) {
668
- hasTrackedRef = true;
669
- }
670
- ts.forEachChild(n, checkRef);
671
- };
672
- checkRef(arg);
673
- if (hasTrackedRef) {
674
- tainted = true;
675
- return;
676
- }
677
- }
678
- }
679
-
680
- // Method call on the variable: trackedVar.method() or alias.method()
681
- if (ts.isCallExpression(node) &&
682
- ts.isPropertyAccessExpression(node.expression) &&
683
- involvesTrackedVar(node.expression.expression)) {
684
- tainted = true;
685
- return;
686
- }
687
-
688
- // Await expression (async boundary - external code could run)
689
- if (ts.isAwaitExpression(node)) {
690
- tainted = true;
691
- return;
692
- }
693
-
694
- ts.forEachChild(node, checkTainting);
695
- };
696
-
697
- checkTainting(body);
698
- return tainted;
699
- };
700
-
701
585
  const transformFunction = (
702
586
  func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration
703
587
  ): ts.Node => {
@@ -732,8 +616,7 @@ export class TypicalTransformer {
732
616
  func.parameters.forEach((param) => {
733
617
  if (param.type) {
734
618
  // Skip 'any' and 'unknown' types - no point validating them
735
- if (param.type.kind === ts.SyntaxKind.AnyKeyword ||
736
- param.type.kind === ts.SyntaxKind.UnknownKeyword) {
619
+ if (this.isAnyOrUnknownType(param.type)) {
737
620
  return;
738
621
  }
739
622
 
@@ -747,8 +630,8 @@ export class TypicalTransformer {
747
630
  validatedVariables.set(paramName, paramType);
748
631
 
749
632
  if (this.config.reusableValidators) {
750
- // Use reusable validators - use typeToString
751
- const typeText = typeChecker.typeToString(paramType);
633
+ // Use reusable validators - use typeNode text to preserve local aliases
634
+ const typeText = this.getTypeKey(param.type, typeChecker);
752
635
  const validatorName = this.getOrCreateValidator(
753
636
  typeText,
754
637
  param.type
@@ -796,8 +679,7 @@ export class TypicalTransformer {
796
679
  for (const decl of node.declarationList.declarations) {
797
680
  if (decl.type && ts.isIdentifier(decl.name)) {
798
681
  // Skip any/unknown types
799
- if (decl.type.kind !== ts.SyntaxKind.AnyKeyword &&
800
- decl.type.kind !== ts.SyntaxKind.UnknownKeyword) {
682
+ if (!this.isAnyOrUnknownType(decl.type)) {
801
683
  const constType = typeChecker.getTypeFromTypeNode(decl.type);
802
684
  validatedVariables.set(decl.name.text, constType);
803
685
  }
@@ -866,12 +748,7 @@ export class TypicalTransformer {
866
748
  }
867
749
 
868
750
  // Skip 'any' and 'unknown' return types - no point validating them
869
- const isAnyOrUnknownReturn = returnType && (
870
- returnType.kind === ts.SyntaxKind.AnyKeyword ||
871
- returnType.kind === ts.SyntaxKind.UnknownKeyword
872
- );
873
-
874
- if (returnType && returnTypeForString && !isAnyOrUnknownReturn) {
751
+ if (returnType && returnTypeForString && !this.isAnyOrUnknownType(returnType)) {
875
752
  const returnTransformer = (node: ts.Node): ts.Node => {
876
753
  if (ts.isReturnStatement(node) && node.expression) {
877
754
  // Skip return validation if the expression already contains a __typical _parse_* call
@@ -891,10 +768,10 @@ export class TypicalTransformer {
891
768
 
892
769
  // Flow analysis: Skip return validation if returning a validated variable
893
770
  // (or property of one) that hasn't been tainted
894
- const rootVar = getRootIdentifier(node.expression);
771
+ const rootVar = this.getRootIdentifier(node.expression);
895
772
  if (rootVar && validatedVariables.has(rootVar)) {
896
773
  // Check if the variable has been tainted (mutated, passed to function, etc.)
897
- if (!isTainted(rootVar, visitedBody)) {
774
+ if (!this.isTainted(rootVar, visitedBody)) {
898
775
  // Return expression is rooted at a validated, untainted variable
899
776
  // For direct returns (identifier) or property access, we can skip validation
900
777
  if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
@@ -918,8 +795,9 @@ export class TypicalTransformer {
918
795
  }
919
796
 
920
797
  if (this.config.reusableValidators) {
921
- // Use reusable validators - always use typeToString
922
- const returnTypeText = typeChecker.typeToString(returnTypeForString!);
798
+ // Use reusable validators - use typeNode text to preserve local aliases
799
+ // Pass returnTypeForString for synthesized nodes (inferred return types)
800
+ const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
923
801
  const validatorName = this.getOrCreateValidator(
924
802
  returnTypeText,
925
803
  returnType
@@ -1045,6 +923,200 @@ export class TypicalTransformer {
1045
923
  return shouldTransformFile(fileName, this.config);
1046
924
  }
1047
925
 
926
+ /**
927
+ * Check if a TypeNode represents any or unknown type.
928
+ * These types are intentional escape hatches and shouldn't be validated.
929
+ */
930
+ private isAnyOrUnknownType(typeNode: ts.TypeNode): boolean {
931
+ return typeNode.kind === this.ts.SyntaxKind.AnyKeyword ||
932
+ typeNode.kind === this.ts.SyntaxKind.UnknownKeyword;
933
+ }
934
+
935
+ /**
936
+ * Check if a Type has any or unknown flags.
937
+ */
938
+ private isAnyOrUnknownTypeFlags(type: ts.Type): boolean {
939
+ return (type.flags & this.ts.TypeFlags.Any) !== 0 ||
940
+ (type.flags & this.ts.TypeFlags.Unknown) !== 0;
941
+ }
942
+
943
+ /**
944
+ * Infer type information from a JSON.stringify argument for creating a reusable stringifier.
945
+ */
946
+ private inferStringifyType(
947
+ arg: ts.Expression,
948
+ typeChecker: ts.TypeChecker,
949
+ ctx: TransformContext
950
+ ): { typeText: string; typeNode: ts.TypeNode } {
951
+ const ts = this.ts;
952
+
953
+ // Type assertion: use the asserted type directly
954
+ if (ts.isAsExpression(arg)) {
955
+ const typeNode = arg.type;
956
+ const typeString = typeChecker.typeToString(typeChecker.getTypeFromTypeNode(typeNode));
957
+ return {
958
+ typeText: `Asserted_${typeString.replace(/[^a-zA-Z0-9_]/g, "_")}`,
959
+ typeNode,
960
+ };
961
+ }
962
+
963
+ // Object literal: use property names for the key
964
+ if (ts.isObjectLiteralExpression(arg)) {
965
+ const objectType = typeChecker.getTypeAtLocation(arg);
966
+ const typeNode = typeChecker.typeToTypeNode(objectType, arg, ts.NodeBuilderFlags.InTypeAlias);
967
+ if (!typeNode) {
968
+ throw new Error('unknown type node for object literal: ' + arg.getText());
969
+ }
970
+ const propNames = arg.properties
971
+ .map((prop) => {
972
+ if (ts.isShorthandPropertyAssignment(prop)) return prop.name.text;
973
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) return prop.name.text;
974
+ return "unknown";
975
+ })
976
+ .sort()
977
+ .join("_");
978
+ return { typeText: `ObjectLiteral_${propNames}`, typeNode };
979
+ }
980
+
981
+ // Other expressions: infer from type checker
982
+ const argType = typeChecker.getTypeAtLocation(arg);
983
+ const typeNode = typeChecker.typeToTypeNode(argType, arg, ts.NodeBuilderFlags.InTypeAlias);
984
+ if (typeNode) {
985
+ const typeString = typeChecker.typeToString(argType);
986
+ return {
987
+ typeText: `Expression_${typeString.replace(/[^a-zA-Z0-9_]/g, "_")}`,
988
+ typeNode,
989
+ };
990
+ }
991
+
992
+ // Fallback to unknown
993
+ return {
994
+ typeText: "unknown",
995
+ typeNode: ctx.factory.createKeywordTypeNode(ctx.ts.SyntaxKind.UnknownKeyword),
996
+ };
997
+ }
998
+
999
+ // ============================================
1000
+ // Flow Analysis Helpers
1001
+ // ============================================
1002
+
1003
+ /**
1004
+ * Gets the root identifier from an expression.
1005
+ * e.g., `user.address.city` -> "user"
1006
+ */
1007
+ private getRootIdentifier(expr: ts.Expression): string | undefined {
1008
+ if (this.ts.isIdentifier(expr)) {
1009
+ return expr.text;
1010
+ }
1011
+ if (this.ts.isPropertyAccessExpression(expr)) {
1012
+ return this.getRootIdentifier(expr.expression);
1013
+ }
1014
+ return undefined;
1015
+ }
1016
+
1017
+ /**
1018
+ * Check if a validated variable has been tainted (mutated) in the function body.
1019
+ * A variable is tainted if it's reassigned, has properties modified, is passed
1020
+ * to a function, has methods called on it, or if an await occurs.
1021
+ */
1022
+ private isTainted(varName: string, body: ts.Block): boolean {
1023
+ let tainted = false;
1024
+ const ts = this.ts;
1025
+
1026
+ // Collect aliases (variables that reference properties of varName)
1027
+ // e.g., const addr = user.address; -> addr is an alias
1028
+ const aliases = new Set<string>([varName]);
1029
+
1030
+ const collectAliases = (node: ts.Node): void => {
1031
+ if (ts.isVariableStatement(node)) {
1032
+ for (const decl of node.declarationList.declarations) {
1033
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
1034
+ const initRoot = this.getRootIdentifier(decl.initializer);
1035
+ if (initRoot && aliases.has(initRoot)) {
1036
+ aliases.add(decl.name.text);
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ ts.forEachChild(node, collectAliases);
1042
+ };
1043
+ collectAliases(body);
1044
+
1045
+ // Helper to check if any alias is involved
1046
+ const involvesTrackedVar = (expr: ts.Expression): boolean => {
1047
+ const root = this.getRootIdentifier(expr);
1048
+ return root !== undefined && aliases.has(root);
1049
+ };
1050
+
1051
+ const checkTainting = (node: ts.Node): void => {
1052
+ if (tainted) return;
1053
+
1054
+ // Reassignment: trackedVar = ...
1055
+ if (ts.isBinaryExpression(node) &&
1056
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1057
+ ts.isIdentifier(node.left) &&
1058
+ aliases.has(node.left.text)) {
1059
+ tainted = true;
1060
+ return;
1061
+ }
1062
+
1063
+ // Property assignment: trackedVar.x = ... or alias.x = ...
1064
+ if (ts.isBinaryExpression(node) &&
1065
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1066
+ ts.isPropertyAccessExpression(node.left) &&
1067
+ involvesTrackedVar(node.left)) {
1068
+ tainted = true;
1069
+ return;
1070
+ }
1071
+
1072
+ // Element assignment: trackedVar[x] = ... or alias[x] = ...
1073
+ if (ts.isBinaryExpression(node) &&
1074
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1075
+ ts.isElementAccessExpression(node.left) &&
1076
+ involvesTrackedVar(node.left.expression)) {
1077
+ tainted = true;
1078
+ return;
1079
+ }
1080
+
1081
+ // Passed as argument to a function: fn(trackedVar) or fn(alias)
1082
+ if (ts.isCallExpression(node)) {
1083
+ for (const arg of node.arguments) {
1084
+ let hasTrackedRef = false;
1085
+ const checkRef = (n: ts.Node): void => {
1086
+ if (ts.isIdentifier(n) && aliases.has(n.text)) {
1087
+ hasTrackedRef = true;
1088
+ }
1089
+ ts.forEachChild(n, checkRef);
1090
+ };
1091
+ checkRef(arg);
1092
+ if (hasTrackedRef) {
1093
+ tainted = true;
1094
+ return;
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ // Method call on the variable: trackedVar.method() or alias.method()
1100
+ if (ts.isCallExpression(node) &&
1101
+ ts.isPropertyAccessExpression(node.expression) &&
1102
+ involvesTrackedVar(node.expression.expression)) {
1103
+ tainted = true;
1104
+ return;
1105
+ }
1106
+
1107
+ // Await expression (async boundary - external code could run)
1108
+ if (ts.isAwaitExpression(node)) {
1109
+ tainted = true;
1110
+ return;
1111
+ }
1112
+
1113
+ ts.forEachChild(node, checkTainting);
1114
+ };
1115
+
1116
+ checkTainting(body);
1117
+ return tainted;
1118
+ }
1119
+
1048
1120
  private addTypiaImport(
1049
1121
  sourceFile: ts.SourceFile,
1050
1122
  ctx: TransformContext
@@ -1083,135 +1155,153 @@ export class TypicalTransformer {
1083
1155
  return sourceFile;
1084
1156
  }
1085
1157
 
1086
- private getOrCreateValidator(
1087
- typeText: string,
1088
- typeNode: ts.TypeNode
1089
- ): string {
1090
- if (this.typeValidators.has(typeText)) {
1091
- return this.typeValidators.get(typeText)!.name;
1158
+ /**
1159
+ * Gets type text for use as a validator map key.
1160
+ * Uses getText() to preserve local aliases (e.g., "User1" vs "User2"),
1161
+ * but falls back to typeToString() for synthesized nodes without source positions.
1162
+ *
1163
+ * @param typeNode The TypeNode to get a key for
1164
+ * @param typeChecker The TypeChecker to use
1165
+ * @param typeObj Optional Type object - use this for synthesized nodes since
1166
+ * getTypeFromTypeNode doesn't work correctly on them
1167
+ */
1168
+ private getTypeKey(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker, typeObj?: ts.Type): string {
1169
+ // Check if node has a real position (not synthesized)
1170
+ if (typeNode.pos >= 0 && typeNode.end > typeNode.pos) {
1171
+ try {
1172
+ return typeNode.getText();
1173
+ } catch {
1174
+ // Fall through to typeToString
1175
+ }
1092
1176
  }
1177
+ // Fallback for synthesized nodes - use the provided Type object if available,
1178
+ // otherwise try to get it from the node (which may not work correctly)
1179
+ const type = typeObj ?? typeChecker.getTypeFromTypeNode(typeNode);
1180
+ return typeChecker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
1181
+ }
1093
1182
 
1094
- const validatorName = `__typical_` + `assert_${this.typeValidators.size}`;
1095
- this.typeValidators.set(typeText, { name: validatorName, typeNode });
1096
- return validatorName;
1183
+ /**
1184
+ * Creates a readable name suffix from a type string.
1185
+ * For simple identifiers like "User" or "string", returns the name directly.
1186
+ * For complex types, returns a numeric index.
1187
+ */
1188
+ private getTypeNameSuffix(typeText: string, existingNames: Set<string>, fallbackIndex: number): string {
1189
+ // Strip known prefixes that wrap the actual type name
1190
+ let normalizedTypeText = typeText;
1191
+ if (typeText.startsWith('Expression_')) {
1192
+ normalizedTypeText = typeText.slice('Expression_'.length);
1193
+ } else if (typeText.startsWith('ObjectLiteral_')) {
1194
+ // Object literals use property names, fall back to numeric
1195
+ return String(fallbackIndex);
1196
+ }
1197
+
1198
+ // Check if it's a simple identifier (letters, numbers, underscore, starting with letter/underscore)
1199
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(normalizedTypeText)) {
1200
+ // It's a simple type name like "User", "string", "MyType"
1201
+ let name = normalizedTypeText;
1202
+ // Handle collisions by appending a number
1203
+ if (existingNames.has(name)) {
1204
+ let i = 2;
1205
+ while (existingNames.has(`${normalizedTypeText}${i}`)) {
1206
+ i++;
1207
+ }
1208
+ name = `${normalizedTypeText}${i}`;
1209
+ }
1210
+ return name;
1211
+ }
1212
+ // Complex type - use numeric index
1213
+ return String(fallbackIndex);
1097
1214
  }
1098
1215
 
1099
- private getOrCreateStringifier(
1216
+ /**
1217
+ * Generic method to get or create a typed function (validator, stringifier, or parser).
1218
+ */
1219
+ private getOrCreateTypedFunction(
1220
+ kind: 'assert' | 'stringify' | 'parse',
1100
1221
  typeText: string,
1101
1222
  typeNode: ts.TypeNode
1102
1223
  ): string {
1103
- if (this.typeStringifiers.has(typeText)) {
1104
- return this.typeStringifiers.get(typeText)!.name;
1224
+ const maps = {
1225
+ assert: this.typeValidators,
1226
+ stringify: this.typeStringifiers,
1227
+ parse: this.typeParsers,
1228
+ };
1229
+ const prefixes = {
1230
+ assert: '__typical_assert_',
1231
+ stringify: '__typical_stringify_',
1232
+ parse: '__typical_parse_',
1233
+ };
1234
+
1235
+ const map = maps[kind];
1236
+ const prefix = prefixes[kind];
1237
+
1238
+ if (map.has(typeText)) {
1239
+ return map.get(typeText)!.name;
1105
1240
  }
1106
1241
 
1107
- const stringifierName = `__typical_` + `stringify_${this.typeStringifiers.size}`;
1108
- this.typeStringifiers.set(typeText, { name: stringifierName, typeNode });
1109
- return stringifierName;
1242
+ const existingSuffixes = [...map.values()].map(v => v.name.slice(prefix.length));
1243
+ const existingNames = new Set(existingSuffixes);
1244
+ const numericCount = existingSuffixes.filter(s => /^\d+$/.test(s)).length;
1245
+ const suffix = this.getTypeNameSuffix(typeText, existingNames, numericCount);
1246
+ const name = `${prefix}${suffix}`;
1247
+ map.set(typeText, { name, typeNode });
1248
+ return name;
1249
+ }
1250
+
1251
+ private getOrCreateValidator(typeText: string, typeNode: ts.TypeNode): string {
1252
+ return this.getOrCreateTypedFunction('assert', typeText, typeNode);
1253
+ }
1254
+
1255
+ private getOrCreateStringifier(typeText: string, typeNode: ts.TypeNode): string {
1256
+ return this.getOrCreateTypedFunction('stringify', typeText, typeNode);
1110
1257
  }
1111
1258
 
1112
1259
  private getOrCreateParser(typeText: string, typeNode: ts.TypeNode): string {
1113
- if (this.typeParsers.has(typeText)) {
1114
- return this.typeParsers.get(typeText)!.name;
1115
- }
1260
+ return this.getOrCreateTypedFunction('parse', typeText, typeNode);
1261
+ }
1116
1262
 
1117
- const parserName = `__typical_` + `parse_${this.typeParsers.size}`;
1118
- this.typeParsers.set(typeText, { name: parserName, typeNode });
1119
- return parserName;
1263
+ /**
1264
+ * Creates a nested property access expression from an array of identifiers.
1265
+ * e.g., ['typia', 'json', 'createStringify'] -> typia.json.createStringify
1266
+ */
1267
+ private createPropertyAccessChain(factory: ts.NodeFactory, parts: string[]): ts.Expression {
1268
+ let expr: ts.Expression = factory.createIdentifier(parts[0]);
1269
+ for (let i = 1; i < parts.length; i++) {
1270
+ expr = factory.createPropertyAccessExpression(expr, parts[i]);
1271
+ }
1272
+ return expr;
1120
1273
  }
1121
1274
 
1122
1275
  private createValidatorStatements(ctx: TransformContext): ts.Statement[] {
1123
1276
  const { factory } = ctx;
1124
1277
  const statements: ts.Statement[] = [];
1125
1278
 
1126
- // Create assert validators
1127
- for (const [, { name: validatorName, typeNode }] of this.typeValidators) {
1128
- const createAssertCall = factory.createCallExpression(
1129
- factory.createPropertyAccessExpression(
1130
- factory.createIdentifier("typia"),
1131
- "createAssert"
1132
- ),
1133
- [typeNode],
1134
- []
1135
- );
1136
-
1137
- const validatorDeclaration = factory.createVariableStatement(
1138
- undefined,
1139
- factory.createVariableDeclarationList(
1140
- [
1141
- factory.createVariableDeclaration(
1142
- validatorName,
1143
- undefined,
1144
- undefined,
1145
- createAssertCall
1146
- ),
1147
- ],
1148
- ctx.ts.NodeFlags.Const
1149
- )
1150
- );
1151
- statements.push(validatorDeclaration);
1152
- }
1153
-
1154
- // Create stringifiers
1155
- for (const [, { name: stringifierName, typeNode }] of this
1156
- .typeStringifiers) {
1157
- const createStringifyCall = factory.createCallExpression(
1158
- factory.createPropertyAccessExpression(
1159
- factory.createPropertyAccessExpression(
1160
- factory.createIdentifier("typia"),
1161
- "json"
1162
- ),
1163
- "createStringify"
1164
- ),
1165
- [typeNode],
1166
- []
1167
- );
1168
-
1169
- const stringifierDeclaration = factory.createVariableStatement(
1170
- undefined,
1171
- factory.createVariableDeclarationList(
1172
- [
1173
- factory.createVariableDeclaration(
1174
- stringifierName,
1175
- undefined,
1176
- undefined,
1177
- createStringifyCall
1178
- ),
1179
- ],
1180
- ctx.ts.NodeFlags.Const
1181
- )
1182
- );
1183
- statements.push(stringifierDeclaration);
1184
- }
1185
-
1186
- // Create parsers
1187
- for (const [, { name: parserName, typeNode }] of this.typeParsers) {
1188
- const createParseCall = factory.createCallExpression(
1189
- factory.createPropertyAccessExpression(
1190
- factory.createPropertyAccessExpression(
1191
- factory.createIdentifier("typia"),
1192
- "json"
1193
- ),
1194
- "createAssertParse"
1195
- ),
1196
- [typeNode],
1197
- []
1198
- );
1279
+ const configs: Array<{
1280
+ map: Map<string, { name: string; typeNode: ts.TypeNode }>;
1281
+ methodPath: string[];
1282
+ }> = [
1283
+ { map: this.typeValidators, methodPath: ['typia', 'createAssert'] },
1284
+ { map: this.typeStringifiers, methodPath: ['typia', 'json', 'createStringify'] },
1285
+ { map: this.typeParsers, methodPath: ['typia', 'json', 'createAssertParse'] },
1286
+ ];
1287
+
1288
+ for (const { map, methodPath } of configs) {
1289
+ for (const [, { name, typeNode }] of map) {
1290
+ const createCall = factory.createCallExpression(
1291
+ this.createPropertyAccessChain(factory, methodPath),
1292
+ [typeNode],
1293
+ []
1294
+ );
1199
1295
 
1200
- const parserDeclaration = factory.createVariableStatement(
1201
- undefined,
1202
- factory.createVariableDeclarationList(
1203
- [
1204
- factory.createVariableDeclaration(
1205
- parserName,
1206
- undefined,
1207
- undefined,
1208
- createParseCall
1209
- ),
1210
- ],
1211
- ctx.ts.NodeFlags.Const
1212
- )
1213
- );
1214
- statements.push(parserDeclaration);
1296
+ const declaration = factory.createVariableStatement(
1297
+ undefined,
1298
+ factory.createVariableDeclarationList(
1299
+ [factory.createVariableDeclaration(name, undefined, undefined, createCall)],
1300
+ ctx.ts.NodeFlags.Const
1301
+ )
1302
+ );
1303
+ statements.push(declaration);
1304
+ }
1215
1305
  }
1216
1306
 
1217
1307
  return statements;