@elliots/typical 0.1.2 → 0.1.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.
@@ -104,29 +104,31 @@ export class TypicalTransformer {
104
104
  return transformedSourceFile;
105
105
  }
106
106
 
107
- // Then apply typia if we added typia calls
107
+ // Apply typia transformation
108
108
  const printer = this.ts.createPrinter();
109
109
  const transformedCode = printer.printFile(transformedSourceFile);
110
110
 
111
+ if (process.env.DEBUG) {
112
+ console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
113
+ }
114
+
111
115
  if (transformedCode.includes("typia.")) {
112
116
  try {
113
- // Apply typia transformation to files with typia calls
114
-
115
- // Create a new source file with the transformed code, preserving original filename
117
+ // Create a new source file from our transformed code
116
118
  const newSourceFile = this.ts.createSourceFile(
117
- sourceFile.fileName, // Use original filename to maintain source map references
119
+ sourceFile.fileName,
118
120
  transformedCode,
119
121
  sourceFile.languageVersion,
120
122
  true
121
123
  );
122
124
 
123
- // Create a new program with the transformed source file so typia's type checker works
125
+ // Create a new program with the transformed source file so typia can resolve types
124
126
  const compilerOptions = this.program.getCompilerOptions();
125
127
  const originalSourceFiles = new Map<string, ts.SourceFile>();
126
128
  for (const sf of this.program.getSourceFiles()) {
127
129
  originalSourceFiles.set(sf.fileName, sf);
128
130
  }
129
- // Replace the original source file with the transformed one
131
+ // Replace the original source file with our transformed one
130
132
  originalSourceFiles.set(sourceFile.fileName, newSourceFile);
131
133
 
132
134
  const customHost: ts.CompilerHost = {
@@ -141,7 +143,7 @@ export class TypicalTransformer {
141
143
  true
142
144
  );
143
145
  },
144
- getDefaultLibFileName: () => this.ts.getDefaultLibFilePath(compilerOptions),
146
+ getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
145
147
  writeFile: () => {},
146
148
  getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
147
149
  getCanonicalFileName: (fileName) =>
@@ -158,33 +160,62 @@ export class TypicalTransformer {
158
160
  customHost
159
161
  );
160
162
 
161
- // Create typia transformer with the NEW program that has the transformed source
162
- const typiaTransformer = typiaTransform(
163
+ // Get the bound source file from the new program (has proper symbol tables)
164
+ const boundSourceFile = newProgram.getSourceFile(sourceFile.fileName);
165
+ if (!boundSourceFile) {
166
+ throw new Error(`Failed to get bound source file: ${sourceFile.fileName}`);
167
+ }
168
+
169
+ // Create typia transformer with the NEW program that has our transformed source
170
+ const typiaTransformerFactory = typiaTransform(
163
171
  newProgram,
164
172
  {},
165
173
  {
166
174
  addDiagnostic(diag: ts.Diagnostic) {
167
- console.warn("Typia diagnostic:", diag);
175
+ if (process.env.DEBUG) {
176
+ console.warn("Typia diagnostic:", diag);
177
+ }
168
178
  return 0;
169
179
  },
170
180
  }
171
181
  );
172
182
 
173
- // Apply the transformer with source map preservation
174
- const transformationResult = this.ts.transform(
175
- newSourceFile,
176
- [typiaTransformer],
177
- { ...compilerOptions, sourceMap: true }
178
- );
183
+ // Apply typia's transformer to the bound source file
184
+ const typiaNodeTransformer = typiaTransformerFactory(context);
185
+ const typiaTransformed = typiaNodeTransformer(boundSourceFile);
179
186
 
180
- if (transformationResult.transformed.length > 0) {
181
- const finalTransformed = transformationResult.transformed[0];
182
- transformedSourceFile = finalTransformed;
187
+ if (process.env.DEBUG) {
188
+ const afterTypia = printer.printFile(typiaTransformed);
189
+ console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
190
+ }
183
191
 
184
- // Typia transformation completed successfully
192
+ // Return the typia-transformed source file.
193
+ // We need to recreate imports as synthetic nodes to prevent import elision,
194
+ // since the imports come from a different program context.
195
+ // Skip type-only imports as they shouldn't appear in JS output.
196
+ const syntheticStatements: ts.Statement[] = [];
197
+ for (const stmt of typiaTransformed.statements) {
198
+ if (this.ts.isImportDeclaration(stmt)) {
199
+ // Skip type-only imports (import type X from "y")
200
+ if (stmt.importClause?.isTypeOnly) {
201
+ continue;
202
+ }
203
+ syntheticStatements.push(this.recreateImportDeclaration(stmt, factory));
204
+ } else {
205
+ syntheticStatements.push(stmt);
206
+ }
185
207
  }
186
208
 
187
- transformationResult.dispose();
209
+ // Update the source file with synthetic imports
210
+ transformedSourceFile = factory.updateSourceFile(
211
+ typiaTransformed,
212
+ syntheticStatements,
213
+ typiaTransformed.isDeclarationFile,
214
+ typiaTransformed.referencedFiles,
215
+ typiaTransformed.typeReferenceDirectives,
216
+ typiaTransformed.hasNoDefaultLib,
217
+ typiaTransformed.libReferenceDirectives
218
+ );
188
219
  } catch (error) {
189
220
  console.warn("Failed to apply typia transformer:", sourceFile.fileName, error);
190
221
  }
@@ -195,6 +226,59 @@ export class TypicalTransformer {
195
226
  };
196
227
  }
197
228
 
229
+ /**
230
+ * Re-create an import declaration as a fully synthetic node.
231
+ * This prevents TypeScript from trying to look up symbol bindings
232
+ * and eliding the import as "unused".
233
+ */
234
+ private recreateImportDeclaration(
235
+ importDecl: ts.ImportDeclaration,
236
+ factory: ts.NodeFactory
237
+ ): ts.ImportDeclaration {
238
+ let importClause: ts.ImportClause | undefined;
239
+
240
+ if (importDecl.importClause) {
241
+ const clause = importDecl.importClause;
242
+ let namedBindings: ts.NamedImportBindings | undefined;
243
+
244
+ if (clause.namedBindings) {
245
+ if (this.ts.isNamespaceImport(clause.namedBindings)) {
246
+ // import * as foo from "bar"
247
+ namedBindings = factory.createNamespaceImport(
248
+ factory.createIdentifier(clause.namedBindings.name.text)
249
+ );
250
+ } else if (this.ts.isNamedImports(clause.namedBindings)) {
251
+ // 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);
260
+ }
261
+ }
262
+
263
+ importClause = factory.createImportClause(
264
+ clause.isTypeOnly,
265
+ clause.name ? factory.createIdentifier(clause.name.text) : undefined,
266
+ namedBindings
267
+ );
268
+ }
269
+
270
+ const moduleSpecifier = this.ts.isStringLiteral(importDecl.moduleSpecifier)
271
+ ? factory.createStringLiteral(importDecl.moduleSpecifier.text)
272
+ : importDecl.moduleSpecifier;
273
+
274
+ return factory.createImportDeclaration(
275
+ importDecl.modifiers,
276
+ importClause,
277
+ moduleSpecifier,
278
+ importDecl.attributes
279
+ );
280
+ }
281
+
198
282
  /**
199
283
  * Transform a single source file with TypeScript AST
200
284
  */
@@ -210,10 +294,12 @@ export class TypicalTransformer {
210
294
  return sourceFile; // Return unchanged for excluded files
211
295
  }
212
296
 
213
- // Check if this file has already been transformed by us
214
- const sourceText = sourceFile.getFullText();
215
- if (sourceText.includes('__typical_assert_') || sourceText.includes('__typical_stringify_') || sourceText.includes('__typical_parse_')) {
216
- throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
297
+ if (!sourceFile.fileName.includes('transformer.test.ts')) {
298
+ // Check if this file has already been transformed by us
299
+ const sourceText = sourceFile.getFullText();
300
+ if (sourceText.includes('__typical_' + 'assert_') || sourceText.includes('__typical_' + 'stringify_') || sourceText.includes('__typical_' + 'parse_')) {
301
+ throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
302
+ }
217
303
  }
218
304
 
219
305
  // Reset caches for each file
@@ -238,6 +324,17 @@ export class TypicalTransformer {
238
324
 
239
325
  if (propertyAccess.name.text === "stringify") {
240
326
  // For stringify, we need to infer the type from the argument
327
+ // First check if the argument type is 'any' - if so, skip transformation
328
+ if (node.arguments.length > 0) {
329
+ const arg = node.arguments[0];
330
+ 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) {
334
+ return node; // Don't transform JSON.stringify for any/unknown types
335
+ }
336
+ }
337
+
241
338
  if (this.config.reusableValidators) {
242
339
  // For JSON.stringify, try to infer the type from the argument
243
340
  let typeText = "unknown";
@@ -385,10 +482,32 @@ export class TypicalTransformer {
385
482
  funcParent = funcParent.parent;
386
483
  }
387
484
  break;
485
+ } else if (ts.isArrowFunction(parent) && parent.type) {
486
+ // Arrow function with expression body (not block)
487
+ // e.g., (s: string): User => JSON.parse(s)
488
+ targetType = parent.type;
489
+ break;
388
490
  }
389
491
  parent = parent.parent;
390
492
  }
391
493
 
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) {
501
+ // Don't transform JSON.parse for any/unknown types
502
+ return node;
503
+ }
504
+
505
+ // If we can't determine the target type and there's no explicit type argument,
506
+ // don't transform - we can't validate against an unknown type
507
+ if (!targetType && !node.typeArguments) {
508
+ return node;
509
+ }
510
+
392
511
  if (this.config.reusableValidators && targetType) {
393
512
  // Use reusable parser - use typeToString
394
513
  const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
@@ -446,25 +565,189 @@ export class TypicalTransformer {
446
565
  return ctx.ts.visitEachChild(node, visit, ctx.context);
447
566
  };
448
567
 
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
+
449
701
  const transformFunction = (
450
702
  func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration
451
703
  ): ts.Node => {
452
704
  const body = func.body;
705
+
706
+ // For arrow functions with expression bodies (not blocks),
707
+ // still visit the expression to transform JSON calls etc.
708
+ if (body && !ts.isBlock(body) && ts.isArrowFunction(func)) {
709
+ const visitedBody = ctx.ts.visitNode(body, visit) as ts.Expression;
710
+ if (visitedBody !== body) {
711
+ return ctx.factory.updateArrowFunction(
712
+ func,
713
+ func.modifiers,
714
+ func.typeParameters,
715
+ func.parameters,
716
+ func.type,
717
+ func.equalsGreaterThanToken,
718
+ visitedBody
719
+ );
720
+ }
721
+ return func;
722
+ }
723
+
453
724
  if (!body || !ts.isBlock(body)) return func;
454
725
 
726
+ // Track validated variables (params and consts with type annotations)
727
+ const validatedVariables = new Map<string, ts.Type>();
728
+
455
729
  // Add parameter validation
456
730
  const validationStatements: ts.Statement[] = [];
457
731
 
458
732
  func.parameters.forEach((param) => {
459
733
  if (param.type) {
734
+ // Skip 'any' and 'unknown' types - no point validating them
735
+ if (param.type.kind === ts.SyntaxKind.AnyKeyword ||
736
+ param.type.kind === ts.SyntaxKind.UnknownKeyword) {
737
+ return;
738
+ }
739
+
460
740
  const paramName = ts.isIdentifier(param.name)
461
741
  ? param.name.text
462
742
  : "param";
463
743
  const paramIdentifier = ctx.factory.createIdentifier(paramName);
464
744
 
745
+ // Track this parameter as validated for flow analysis
746
+ const paramType = typeChecker.getTypeFromTypeNode(param.type);
747
+ validatedVariables.set(paramName, paramType);
748
+
465
749
  if (this.config.reusableValidators) {
466
750
  // Use reusable validators - use typeToString
467
- const paramType = typeChecker.getTypeFromTypeNode(param.type);
468
751
  const typeText = typeChecker.typeToString(paramType);
469
752
  const validatorName = this.getOrCreateValidator(
470
753
  typeText,
@@ -504,31 +787,136 @@ export class TypicalTransformer {
504
787
  // First visit all child nodes (including JSON calls) before adding validation
505
788
  const visitedBody = ctx.ts.visitNode(body, visit) as ts.Block;
506
789
 
790
+ // Also track const declarations with type annotations as validated
791
+ // (the assignment will be validated, and const can't be reassigned)
792
+ const collectConstDeclarations = (node: ts.Node): void => {
793
+ if (ts.isVariableStatement(node)) {
794
+ const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
795
+ if (isConst) {
796
+ for (const decl of node.declarationList.declarations) {
797
+ if (decl.type && ts.isIdentifier(decl.name)) {
798
+ // Skip any/unknown types
799
+ if (decl.type.kind !== ts.SyntaxKind.AnyKeyword &&
800
+ decl.type.kind !== ts.SyntaxKind.UnknownKeyword) {
801
+ const constType = typeChecker.getTypeFromTypeNode(decl.type);
802
+ validatedVariables.set(decl.name.text, constType);
803
+ }
804
+ }
805
+ }
806
+ }
807
+ }
808
+ ts.forEachChild(node, collectConstDeclarations);
809
+ };
810
+ collectConstDeclarations(visitedBody);
811
+
507
812
  // Transform return statements - use explicit type or infer from type checker
508
813
  let transformedStatements = visitedBody.statements;
509
814
  let returnType = func.type;
510
815
 
816
+ // Check if this is an async function
817
+ const isAsync = func.modifiers?.some(
818
+ (mod) => mod.kind === ts.SyntaxKind.AsyncKeyword
819
+ );
820
+
511
821
  // If no explicit return type, try to infer it from the type checker
512
822
  let returnTypeForString: ts.Type | undefined;
513
823
  if (!returnType) {
514
- const signature = typeChecker.getSignatureFromDeclaration(func);
515
- if (signature) {
516
- const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
517
- returnType = typeChecker.typeToTypeNode(
518
- inferredReturnType,
519
- func,
520
- ts.NodeBuilderFlags.InTypeAlias
521
- );
522
- returnTypeForString = inferredReturnType;
824
+ try {
825
+ const signature = typeChecker.getSignatureFromDeclaration(func);
826
+ if (signature) {
827
+ const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
828
+ returnType = typeChecker.typeToTypeNode(
829
+ inferredReturnType,
830
+ func,
831
+ ts.NodeBuilderFlags.InTypeAlias
832
+ );
833
+ returnTypeForString = inferredReturnType;
834
+ }
835
+ } catch {
836
+ // Could not infer signature (e.g., untyped arrow function callback)
837
+ // Skip return type validation for this function
523
838
  }
524
839
  } else {
525
840
  // For explicit return types, get the Type from the TypeNode
526
841
  returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
527
842
  }
528
843
 
529
- if (returnType && returnTypeForString) {
844
+ // For async functions, unwrap Promise<T> to get T
845
+ // The return statement in an async function returns T, not Promise<T>
846
+ if (isAsync && returnType && returnTypeForString) {
847
+ const promiseSymbol = returnTypeForString.getSymbol();
848
+ if (promiseSymbol && promiseSymbol.getName() === "Promise") {
849
+ // Get the type argument of Promise<T>
850
+ const typeArgs = (returnTypeForString as ts.TypeReference).typeArguments;
851
+ if (typeArgs && typeArgs.length > 0) {
852
+ returnTypeForString = typeArgs[0];
853
+ // Also update the TypeNode to match
854
+ if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
855
+ returnType = returnType.typeArguments[0];
856
+ } else {
857
+ // Create a new type node from the unwrapped type
858
+ returnType = typeChecker.typeToTypeNode(
859
+ returnTypeForString,
860
+ func,
861
+ ts.NodeBuilderFlags.InTypeAlias
862
+ );
863
+ }
864
+ }
865
+ }
866
+ }
867
+
868
+ // 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) {
530
875
  const returnTransformer = (node: ts.Node): ts.Node => {
531
876
  if (ts.isReturnStatement(node) && node.expression) {
877
+ // Skip return validation if the expression already contains a __typical _parse_* call
878
+ // since typia.assertParse already validates the parsed data
879
+ const containsTypicalParse = (n: ts.Node): boolean => {
880
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
881
+ const name = n.expression.text;
882
+ if (name.startsWith("__typical" + "_parse_")) {
883
+ return true;
884
+ }
885
+ }
886
+ return ts.forEachChild(n, containsTypicalParse) || false;
887
+ };
888
+ if (containsTypicalParse(node.expression)) {
889
+ return node; // Already validated by parse, skip return validation
890
+ }
891
+
892
+ // Flow analysis: Skip return validation if returning a validated variable
893
+ // (or property of one) that hasn't been tainted
894
+ const rootVar = getRootIdentifier(node.expression);
895
+ if (rootVar && validatedVariables.has(rootVar)) {
896
+ // Check if the variable has been tainted (mutated, passed to function, etc.)
897
+ if (!isTainted(rootVar, visitedBody)) {
898
+ // Return expression is rooted at a validated, untainted variable
899
+ // For direct returns (identifier) or property access, we can skip validation
900
+ if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
901
+ return node; // Skip validation - already validated and untainted
902
+ }
903
+ }
904
+ }
905
+
906
+ // For async functions, we need to await the expression before validating
907
+ // because the return expression might be a Promise
908
+ let expressionToValidate = node.expression;
909
+
910
+ if (isAsync) {
911
+ // Check if the expression is already an await expression
912
+ const isAlreadyAwaited = ts.isAwaitExpression(node.expression);
913
+
914
+ if (!isAlreadyAwaited) {
915
+ // Wrap in await: return validate(await expr)
916
+ expressionToValidate = ctx.factory.createAwaitExpression(node.expression);
917
+ }
918
+ }
919
+
532
920
  if (this.config.reusableValidators) {
533
921
  // Use reusable validators - always use typeToString
534
922
  const returnTypeText = typeChecker.typeToString(returnTypeForString!);
@@ -540,7 +928,7 @@ export class TypicalTransformer {
540
928
  const validatorCall = ctx.factory.createCallExpression(
541
929
  ctx.factory.createIdentifier(validatorName),
542
930
  undefined,
543
- [node.expression]
931
+ [expressionToValidate]
544
932
  );
545
933
 
546
934
  return ctx.factory.updateReturnStatement(node, validatorCall);
@@ -555,7 +943,7 @@ export class TypicalTransformer {
555
943
  const callExpression = ctx.factory.createCallExpression(
556
944
  propertyAccess,
557
945
  [returnType],
558
- [node.expression]
946
+ [expressionToValidate]
559
947
  );
560
948
 
561
949
  return ctx.factory.updateReturnStatement(node, callExpression);
@@ -703,7 +1091,7 @@ export class TypicalTransformer {
703
1091
  return this.typeValidators.get(typeText)!.name;
704
1092
  }
705
1093
 
706
- const validatorName = `__typical_assert_${this.typeValidators.size}`;
1094
+ const validatorName = `__typical_` + `assert_${this.typeValidators.size}`;
707
1095
  this.typeValidators.set(typeText, { name: validatorName, typeNode });
708
1096
  return validatorName;
709
1097
  }
@@ -716,7 +1104,7 @@ export class TypicalTransformer {
716
1104
  return this.typeStringifiers.get(typeText)!.name;
717
1105
  }
718
1106
 
719
- const stringifierName = `__typical_stringify_${this.typeStringifiers.size}`;
1107
+ const stringifierName = `__typical_` + `stringify_${this.typeStringifiers.size}`;
720
1108
  this.typeStringifiers.set(typeText, { name: stringifierName, typeNode });
721
1109
  return stringifierName;
722
1110
  }
@@ -726,7 +1114,7 @@ export class TypicalTransformer {
726
1114
  return this.typeParsers.get(typeText)!.name;
727
1115
  }
728
1116
 
729
- const parserName = `__typical_parse_${this.typeParsers.size}`;
1117
+ const parserName = `__typical_` + `parse_${this.typeParsers.size}`;
730
1118
  this.typeParsers.set(typeText, { name: parserName, typeNode });
731
1119
  return parserName;
732
1120
  }
package/src/tsc-plugin.ts CHANGED
@@ -6,7 +6,7 @@ import { loadConfig } from './config.js';
6
6
  export default function (program: ts.Program, pluginConfig: PluginConfig, { ts: tsInstance }: TransformerExtras) {
7
7
  const config = loadConfig();
8
8
  const transformer = new TypicalTransformer(config, program, tsInstance);
9
-
9
+
10
10
  // Create the typical transformer with typia integration
11
11
  return transformer.getTransformer(true);
12
12
  }