@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.
- package/README.md +3 -0
- package/dist/src/transformer.d.ts +6 -0
- package/dist/src/transformer.js +345 -34
- package/dist/src/transformer.js.map +1 -1
- package/package.json +5 -3
- package/src/transformer.ts +430 -42
- package/src/tsc-plugin.ts +1 -1
package/README.md
CHANGED
|
@@ -87,3 +87,6 @@ But basically you shouldn't need to care about how it works internally, it makes
|
|
|
87
87
|
|
|
88
88
|
## Credits
|
|
89
89
|
The actual validation work is done by [typia](https://github.com/samchon/typia). This package just generates the necessary code to call typia's functions based on your TypeScript types.
|
|
90
|
+
|
|
91
|
+
> NOTE: The whole package was all mostly LLM. Feel free to improve it without care for the author's feelings.
|
|
92
|
+
|
|
@@ -16,6 +16,12 @@ export declare class TypicalTransformer {
|
|
|
16
16
|
createSourceFile(fileName: string, content: string): ts.SourceFile;
|
|
17
17
|
transform(sourceFile: ts.SourceFile | string, mode: "basic" | "typia" | "js"): string;
|
|
18
18
|
getTransformer(withTypia: boolean): ts.TransformerFactory<ts.SourceFile>;
|
|
19
|
+
/**
|
|
20
|
+
* Re-create an import declaration as a fully synthetic node.
|
|
21
|
+
* This prevents TypeScript from trying to look up symbol bindings
|
|
22
|
+
* and eliding the import as "unused".
|
|
23
|
+
*/
|
|
24
|
+
private recreateImportDeclaration;
|
|
19
25
|
/**
|
|
20
26
|
* Transform a single source file with TypeScript AST
|
|
21
27
|
*/
|
package/dist/src/transformer.js
CHANGED
|
@@ -57,22 +57,23 @@ export class TypicalTransformer {
|
|
|
57
57
|
if (!withTypia) {
|
|
58
58
|
return transformedSourceFile;
|
|
59
59
|
}
|
|
60
|
-
//
|
|
60
|
+
// Apply typia transformation
|
|
61
61
|
const printer = this.ts.createPrinter();
|
|
62
62
|
const transformedCode = printer.printFile(transformedSourceFile);
|
|
63
|
+
if (process.env.DEBUG) {
|
|
64
|
+
console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
|
|
65
|
+
}
|
|
63
66
|
if (transformedCode.includes("typia.")) {
|
|
64
67
|
try {
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
transformedCode, sourceFile.languageVersion, true);
|
|
69
|
-
// Create a new program with the transformed source file so typia's type checker works
|
|
68
|
+
// Create a new source file from our transformed code
|
|
69
|
+
const newSourceFile = this.ts.createSourceFile(sourceFile.fileName, transformedCode, sourceFile.languageVersion, true);
|
|
70
|
+
// Create a new program with the transformed source file so typia can resolve types
|
|
70
71
|
const compilerOptions = this.program.getCompilerOptions();
|
|
71
72
|
const originalSourceFiles = new Map();
|
|
72
73
|
for (const sf of this.program.getSourceFiles()) {
|
|
73
74
|
originalSourceFiles.set(sf.fileName, sf);
|
|
74
75
|
}
|
|
75
|
-
// Replace the original source file with
|
|
76
|
+
// Replace the original source file with our transformed one
|
|
76
77
|
originalSourceFiles.set(sourceFile.fileName, newSourceFile);
|
|
77
78
|
const customHost = {
|
|
78
79
|
getSourceFile: (fileName, languageVersion) => {
|
|
@@ -81,7 +82,7 @@ export class TypicalTransformer {
|
|
|
81
82
|
}
|
|
82
83
|
return this.ts.createSourceFile(fileName, this.ts.sys.readFile(fileName) || "", languageVersion, true);
|
|
83
84
|
},
|
|
84
|
-
getDefaultLibFileName: () => this.ts.getDefaultLibFilePath(
|
|
85
|
+
getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
|
|
85
86
|
writeFile: () => { },
|
|
86
87
|
getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
|
|
87
88
|
getCanonicalFileName: (fileName) => this.ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
|
|
@@ -91,21 +92,46 @@ export class TypicalTransformer {
|
|
|
91
92
|
readFile: (fileName) => this.ts.sys.readFile(fileName),
|
|
92
93
|
};
|
|
93
94
|
const newProgram = this.ts.createProgram(Array.from(originalSourceFiles.keys()), compilerOptions, customHost);
|
|
94
|
-
//
|
|
95
|
-
const
|
|
95
|
+
// Get the bound source file from the new program (has proper symbol tables)
|
|
96
|
+
const boundSourceFile = newProgram.getSourceFile(sourceFile.fileName);
|
|
97
|
+
if (!boundSourceFile) {
|
|
98
|
+
throw new Error(`Failed to get bound source file: ${sourceFile.fileName}`);
|
|
99
|
+
}
|
|
100
|
+
// Create typia transformer with the NEW program that has our transformed source
|
|
101
|
+
const typiaTransformerFactory = typiaTransform(newProgram, {}, {
|
|
96
102
|
addDiagnostic(diag) {
|
|
97
|
-
|
|
103
|
+
if (process.env.DEBUG) {
|
|
104
|
+
console.warn("Typia diagnostic:", diag);
|
|
105
|
+
}
|
|
98
106
|
return 0;
|
|
99
107
|
},
|
|
100
108
|
});
|
|
101
|
-
// Apply
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
// Apply typia's transformer to the bound source file
|
|
110
|
+
const typiaNodeTransformer = typiaTransformerFactory(context);
|
|
111
|
+
const typiaTransformed = typiaNodeTransformer(boundSourceFile);
|
|
112
|
+
if (process.env.DEBUG) {
|
|
113
|
+
const afterTypia = printer.printFile(typiaTransformed);
|
|
114
|
+
console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
|
|
115
|
+
}
|
|
116
|
+
// Return the typia-transformed source file.
|
|
117
|
+
// We need to recreate imports as synthetic nodes to prevent import elision,
|
|
118
|
+
// since the imports come from a different program context.
|
|
119
|
+
// Skip type-only imports as they shouldn't appear in JS output.
|
|
120
|
+
const syntheticStatements = [];
|
|
121
|
+
for (const stmt of typiaTransformed.statements) {
|
|
122
|
+
if (this.ts.isImportDeclaration(stmt)) {
|
|
123
|
+
// Skip type-only imports (import type X from "y")
|
|
124
|
+
if (stmt.importClause?.isTypeOnly) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
syntheticStatements.push(this.recreateImportDeclaration(stmt, factory));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
syntheticStatements.push(stmt);
|
|
131
|
+
}
|
|
107
132
|
}
|
|
108
|
-
|
|
133
|
+
// Update the source file with synthetic imports
|
|
134
|
+
transformedSourceFile = factory.updateSourceFile(typiaTransformed, syntheticStatements, typiaTransformed.isDeclarationFile, typiaTransformed.referencedFiles, typiaTransformed.typeReferenceDirectives, typiaTransformed.hasNoDefaultLib, typiaTransformed.libReferenceDirectives);
|
|
109
135
|
}
|
|
110
136
|
catch (error) {
|
|
111
137
|
console.warn("Failed to apply typia transformer:", sourceFile.fileName, error);
|
|
@@ -115,6 +141,34 @@ export class TypicalTransformer {
|
|
|
115
141
|
};
|
|
116
142
|
};
|
|
117
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Re-create an import declaration as a fully synthetic node.
|
|
146
|
+
* This prevents TypeScript from trying to look up symbol bindings
|
|
147
|
+
* and eliding the import as "unused".
|
|
148
|
+
*/
|
|
149
|
+
recreateImportDeclaration(importDecl, factory) {
|
|
150
|
+
let importClause;
|
|
151
|
+
if (importDecl.importClause) {
|
|
152
|
+
const clause = importDecl.importClause;
|
|
153
|
+
let namedBindings;
|
|
154
|
+
if (clause.namedBindings) {
|
|
155
|
+
if (this.ts.isNamespaceImport(clause.namedBindings)) {
|
|
156
|
+
// import * as foo from "bar"
|
|
157
|
+
namedBindings = factory.createNamespaceImport(factory.createIdentifier(clause.namedBindings.name.text));
|
|
158
|
+
}
|
|
159
|
+
else if (this.ts.isNamedImports(clause.namedBindings)) {
|
|
160
|
+
// import { foo, bar } from "baz"
|
|
161
|
+
const elements = clause.namedBindings.elements.map((el) => factory.createImportSpecifier(el.isTypeOnly, el.propertyName ? factory.createIdentifier(el.propertyName.text) : undefined, factory.createIdentifier(el.name.text)));
|
|
162
|
+
namedBindings = factory.createNamedImports(elements);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
importClause = factory.createImportClause(clause.isTypeOnly, clause.name ? factory.createIdentifier(clause.name.text) : undefined, namedBindings);
|
|
166
|
+
}
|
|
167
|
+
const moduleSpecifier = this.ts.isStringLiteral(importDecl.moduleSpecifier)
|
|
168
|
+
? factory.createStringLiteral(importDecl.moduleSpecifier.text)
|
|
169
|
+
: importDecl.moduleSpecifier;
|
|
170
|
+
return factory.createImportDeclaration(importDecl.modifiers, importClause, moduleSpecifier, importDecl.attributes);
|
|
171
|
+
}
|
|
118
172
|
/**
|
|
119
173
|
* Transform a single source file with TypeScript AST
|
|
120
174
|
*/
|
|
@@ -124,10 +178,12 @@ export class TypicalTransformer {
|
|
|
124
178
|
if (!this.shouldTransformFile(sourceFile.fileName)) {
|
|
125
179
|
return sourceFile; // Return unchanged for excluded files
|
|
126
180
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
if (!sourceFile.fileName.includes('transformer.test.ts')) {
|
|
182
|
+
// Check if this file has already been transformed by us
|
|
183
|
+
const sourceText = sourceFile.getFullText();
|
|
184
|
+
if (sourceText.includes('__typical_' + 'assert_') || sourceText.includes('__typical_' + 'stringify_') || sourceText.includes('__typical_' + 'parse_')) {
|
|
185
|
+
throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
|
|
186
|
+
}
|
|
131
187
|
}
|
|
132
188
|
// Reset caches for each file
|
|
133
189
|
this.typeValidators.clear();
|
|
@@ -144,6 +200,16 @@ export class TypicalTransformer {
|
|
|
144
200
|
needsTypiaImport = true;
|
|
145
201
|
if (propertyAccess.name.text === "stringify") {
|
|
146
202
|
// For stringify, we need to infer the type from the argument
|
|
203
|
+
// First check if the argument type is 'any' - if so, skip transformation
|
|
204
|
+
if (node.arguments.length > 0) {
|
|
205
|
+
const arg = node.arguments[0];
|
|
206
|
+
const argType = typeChecker.getTypeAtLocation(arg);
|
|
207
|
+
const typeFlags = argType.flags;
|
|
208
|
+
// Skip if type is any (1) or unknown (2)
|
|
209
|
+
if (typeFlags & ts.TypeFlags.Any || typeFlags & ts.TypeFlags.Unknown) {
|
|
210
|
+
return node; // Don't transform JSON.stringify for any/unknown types
|
|
211
|
+
}
|
|
212
|
+
}
|
|
147
213
|
if (this.config.reusableValidators) {
|
|
148
214
|
// For JSON.stringify, try to infer the type from the argument
|
|
149
215
|
let typeText = "unknown";
|
|
@@ -249,8 +315,26 @@ export class TypicalTransformer {
|
|
|
249
315
|
}
|
|
250
316
|
break;
|
|
251
317
|
}
|
|
318
|
+
else if (ts.isArrowFunction(parent) && parent.type) {
|
|
319
|
+
// Arrow function with expression body (not block)
|
|
320
|
+
// e.g., (s: string): User => JSON.parse(s)
|
|
321
|
+
targetType = parent.type;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
252
324
|
parent = parent.parent;
|
|
253
325
|
}
|
|
326
|
+
// Skip transformation if target type is any or unknown
|
|
327
|
+
const isAnyOrUnknown = targetType && (targetType.kind === ts.SyntaxKind.AnyKeyword ||
|
|
328
|
+
targetType.kind === ts.SyntaxKind.UnknownKeyword);
|
|
329
|
+
if (isAnyOrUnknown) {
|
|
330
|
+
// Don't transform JSON.parse for any/unknown types
|
|
331
|
+
return node;
|
|
332
|
+
}
|
|
333
|
+
// If we can't determine the target type and there's no explicit type argument,
|
|
334
|
+
// don't transform - we can't validate against an unknown type
|
|
335
|
+
if (!targetType && !node.typeArguments) {
|
|
336
|
+
return node;
|
|
337
|
+
}
|
|
254
338
|
if (this.config.reusableValidators && targetType) {
|
|
255
339
|
// Use reusable parser - use typeToString
|
|
256
340
|
const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
|
|
@@ -286,21 +370,158 @@ export class TypicalTransformer {
|
|
|
286
370
|
}
|
|
287
371
|
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
288
372
|
};
|
|
373
|
+
// Helper functions for flow analysis
|
|
374
|
+
const getRootIdentifier = (expr) => {
|
|
375
|
+
if (ts.isIdentifier(expr)) {
|
|
376
|
+
return expr.text;
|
|
377
|
+
}
|
|
378
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
379
|
+
return getRootIdentifier(expr.expression);
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
};
|
|
383
|
+
const containsReference = (expr, name) => {
|
|
384
|
+
if (ts.isIdentifier(expr) && expr.text === name) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
388
|
+
return containsReference(expr.expression, name);
|
|
389
|
+
}
|
|
390
|
+
if (ts.isElementAccessExpression(expr)) {
|
|
391
|
+
return containsReference(expr.expression, name) ||
|
|
392
|
+
containsReference(expr.argumentExpression, name);
|
|
393
|
+
}
|
|
394
|
+
// Check all children
|
|
395
|
+
let found = false;
|
|
396
|
+
ts.forEachChild(expr, (child) => {
|
|
397
|
+
if (ts.isExpression(child) && containsReference(child, name)) {
|
|
398
|
+
found = true;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return found;
|
|
402
|
+
};
|
|
403
|
+
// Check if a validated variable has been tainted in the function body
|
|
404
|
+
const isTainted = (varName, body) => {
|
|
405
|
+
let tainted = false;
|
|
406
|
+
// First pass: collect aliases (variables that reference properties of varName)
|
|
407
|
+
// e.g., const addr = user.address; -> addr is an alias
|
|
408
|
+
const aliases = new Set([varName]);
|
|
409
|
+
const collectAliases = (node) => {
|
|
410
|
+
// Look for: const/let x = varName.property or const/let x = varName
|
|
411
|
+
if (ts.isVariableStatement(node)) {
|
|
412
|
+
for (const decl of node.declarationList.declarations) {
|
|
413
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
414
|
+
// Check if initializer is rooted at our tracked variable or any existing alias
|
|
415
|
+
const initRoot = getRootIdentifier(decl.initializer);
|
|
416
|
+
if (initRoot && aliases.has(initRoot)) {
|
|
417
|
+
aliases.add(decl.name.text);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
ts.forEachChild(node, collectAliases);
|
|
423
|
+
};
|
|
424
|
+
collectAliases(body);
|
|
425
|
+
// Helper to check if any alias is involved
|
|
426
|
+
const involvesTrackedVar = (expr) => {
|
|
427
|
+
const root = getRootIdentifier(expr);
|
|
428
|
+
return root !== undefined && aliases.has(root);
|
|
429
|
+
};
|
|
430
|
+
const checkTainting = (node) => {
|
|
431
|
+
if (tainted)
|
|
432
|
+
return; // Early exit if already tainted
|
|
433
|
+
// Reassignment: trackedVar = ...
|
|
434
|
+
if (ts.isBinaryExpression(node) &&
|
|
435
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
436
|
+
ts.isIdentifier(node.left) &&
|
|
437
|
+
aliases.has(node.left.text)) {
|
|
438
|
+
tainted = true;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// Property assignment: trackedVar.x = ... or alias.x = ...
|
|
442
|
+
if (ts.isBinaryExpression(node) &&
|
|
443
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
444
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
445
|
+
involvesTrackedVar(node.left)) {
|
|
446
|
+
tainted = true;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Element assignment: trackedVar[x] = ... or alias[x] = ...
|
|
450
|
+
if (ts.isBinaryExpression(node) &&
|
|
451
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
452
|
+
ts.isElementAccessExpression(node.left) &&
|
|
453
|
+
involvesTrackedVar(node.left.expression)) {
|
|
454
|
+
tainted = true;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Passed as argument to a function: fn(trackedVar) or fn(alias)
|
|
458
|
+
if (ts.isCallExpression(node)) {
|
|
459
|
+
for (const arg of node.arguments) {
|
|
460
|
+
// Check if any tracked variable or alias appears in the argument
|
|
461
|
+
let hasTrackedRef = false;
|
|
462
|
+
const checkRef = (n) => {
|
|
463
|
+
if (ts.isIdentifier(n) && aliases.has(n.text)) {
|
|
464
|
+
hasTrackedRef = true;
|
|
465
|
+
}
|
|
466
|
+
ts.forEachChild(n, checkRef);
|
|
467
|
+
};
|
|
468
|
+
checkRef(arg);
|
|
469
|
+
if (hasTrackedRef) {
|
|
470
|
+
tainted = true;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Method call on the variable: trackedVar.method() or alias.method()
|
|
476
|
+
if (ts.isCallExpression(node) &&
|
|
477
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
478
|
+
involvesTrackedVar(node.expression.expression)) {
|
|
479
|
+
tainted = true;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// Await expression (async boundary - external code could run)
|
|
483
|
+
if (ts.isAwaitExpression(node)) {
|
|
484
|
+
tainted = true;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
ts.forEachChild(node, checkTainting);
|
|
488
|
+
};
|
|
489
|
+
checkTainting(body);
|
|
490
|
+
return tainted;
|
|
491
|
+
};
|
|
289
492
|
const transformFunction = (func) => {
|
|
290
493
|
const body = func.body;
|
|
494
|
+
// For arrow functions with expression bodies (not blocks),
|
|
495
|
+
// still visit the expression to transform JSON calls etc.
|
|
496
|
+
if (body && !ts.isBlock(body) && ts.isArrowFunction(func)) {
|
|
497
|
+
const visitedBody = ctx.ts.visitNode(body, visit);
|
|
498
|
+
if (visitedBody !== body) {
|
|
499
|
+
return ctx.factory.updateArrowFunction(func, func.modifiers, func.typeParameters, func.parameters, func.type, func.equalsGreaterThanToken, visitedBody);
|
|
500
|
+
}
|
|
501
|
+
return func;
|
|
502
|
+
}
|
|
291
503
|
if (!body || !ts.isBlock(body))
|
|
292
504
|
return func;
|
|
505
|
+
// Track validated variables (params and consts with type annotations)
|
|
506
|
+
const validatedVariables = new Map();
|
|
293
507
|
// Add parameter validation
|
|
294
508
|
const validationStatements = [];
|
|
295
509
|
func.parameters.forEach((param) => {
|
|
296
510
|
if (param.type) {
|
|
511
|
+
// Skip 'any' and 'unknown' types - no point validating them
|
|
512
|
+
if (param.type.kind === ts.SyntaxKind.AnyKeyword ||
|
|
513
|
+
param.type.kind === ts.SyntaxKind.UnknownKeyword) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
297
516
|
const paramName = ts.isIdentifier(param.name)
|
|
298
517
|
? param.name.text
|
|
299
518
|
: "param";
|
|
300
519
|
const paramIdentifier = ctx.factory.createIdentifier(paramName);
|
|
520
|
+
// Track this parameter as validated for flow analysis
|
|
521
|
+
const paramType = typeChecker.getTypeFromTypeNode(param.type);
|
|
522
|
+
validatedVariables.set(paramName, paramType);
|
|
301
523
|
if (this.config.reusableValidators) {
|
|
302
524
|
// Use reusable validators - use typeToString
|
|
303
|
-
const paramType = typeChecker.getTypeFromTypeNode(param.type);
|
|
304
525
|
const typeText = typeChecker.typeToString(paramType);
|
|
305
526
|
const validatorName = this.getOrCreateValidator(typeText, param.type);
|
|
306
527
|
const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [paramIdentifier]);
|
|
@@ -320,31 +541,121 @@ export class TypicalTransformer {
|
|
|
320
541
|
});
|
|
321
542
|
// First visit all child nodes (including JSON calls) before adding validation
|
|
322
543
|
const visitedBody = ctx.ts.visitNode(body, visit);
|
|
544
|
+
// Also track const declarations with type annotations as validated
|
|
545
|
+
// (the assignment will be validated, and const can't be reassigned)
|
|
546
|
+
const collectConstDeclarations = (node) => {
|
|
547
|
+
if (ts.isVariableStatement(node)) {
|
|
548
|
+
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
549
|
+
if (isConst) {
|
|
550
|
+
for (const decl of node.declarationList.declarations) {
|
|
551
|
+
if (decl.type && ts.isIdentifier(decl.name)) {
|
|
552
|
+
// Skip any/unknown types
|
|
553
|
+
if (decl.type.kind !== ts.SyntaxKind.AnyKeyword &&
|
|
554
|
+
decl.type.kind !== ts.SyntaxKind.UnknownKeyword) {
|
|
555
|
+
const constType = typeChecker.getTypeFromTypeNode(decl.type);
|
|
556
|
+
validatedVariables.set(decl.name.text, constType);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
ts.forEachChild(node, collectConstDeclarations);
|
|
563
|
+
};
|
|
564
|
+
collectConstDeclarations(visitedBody);
|
|
323
565
|
// Transform return statements - use explicit type or infer from type checker
|
|
324
566
|
let transformedStatements = visitedBody.statements;
|
|
325
567
|
let returnType = func.type;
|
|
568
|
+
// Check if this is an async function
|
|
569
|
+
const isAsync = func.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.AsyncKeyword);
|
|
326
570
|
// If no explicit return type, try to infer it from the type checker
|
|
327
571
|
let returnTypeForString;
|
|
328
572
|
if (!returnType) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
573
|
+
try {
|
|
574
|
+
const signature = typeChecker.getSignatureFromDeclaration(func);
|
|
575
|
+
if (signature) {
|
|
576
|
+
const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
|
|
577
|
+
returnType = typeChecker.typeToTypeNode(inferredReturnType, func, ts.NodeBuilderFlags.InTypeAlias);
|
|
578
|
+
returnTypeForString = inferredReturnType;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// Could not infer signature (e.g., untyped arrow function callback)
|
|
583
|
+
// Skip return type validation for this function
|
|
334
584
|
}
|
|
335
585
|
}
|
|
336
586
|
else {
|
|
337
587
|
// For explicit return types, get the Type from the TypeNode
|
|
338
588
|
returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
|
|
339
589
|
}
|
|
340
|
-
|
|
590
|
+
// For async functions, unwrap Promise<T> to get T
|
|
591
|
+
// The return statement in an async function returns T, not Promise<T>
|
|
592
|
+
if (isAsync && returnType && returnTypeForString) {
|
|
593
|
+
const promiseSymbol = returnTypeForString.getSymbol();
|
|
594
|
+
if (promiseSymbol && promiseSymbol.getName() === "Promise") {
|
|
595
|
+
// Get the type argument of Promise<T>
|
|
596
|
+
const typeArgs = returnTypeForString.typeArguments;
|
|
597
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
598
|
+
returnTypeForString = typeArgs[0];
|
|
599
|
+
// Also update the TypeNode to match
|
|
600
|
+
if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
|
|
601
|
+
returnType = returnType.typeArguments[0];
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
// Create a new type node from the unwrapped type
|
|
605
|
+
returnType = typeChecker.typeToTypeNode(returnTypeForString, func, ts.NodeBuilderFlags.InTypeAlias);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Skip 'any' and 'unknown' return types - no point validating them
|
|
611
|
+
const isAnyOrUnknownReturn = returnType && (returnType.kind === ts.SyntaxKind.AnyKeyword ||
|
|
612
|
+
returnType.kind === ts.SyntaxKind.UnknownKeyword);
|
|
613
|
+
if (returnType && returnTypeForString && !isAnyOrUnknownReturn) {
|
|
341
614
|
const returnTransformer = (node) => {
|
|
342
615
|
if (ts.isReturnStatement(node) && node.expression) {
|
|
616
|
+
// Skip return validation if the expression already contains a __typical _parse_* call
|
|
617
|
+
// since typia.assertParse already validates the parsed data
|
|
618
|
+
const containsTypicalParse = (n) => {
|
|
619
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
620
|
+
const name = n.expression.text;
|
|
621
|
+
if (name.startsWith("__typical" + "_parse_")) {
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return ts.forEachChild(n, containsTypicalParse) || false;
|
|
626
|
+
};
|
|
627
|
+
if (containsTypicalParse(node.expression)) {
|
|
628
|
+
return node; // Already validated by parse, skip return validation
|
|
629
|
+
}
|
|
630
|
+
// Flow analysis: Skip return validation if returning a validated variable
|
|
631
|
+
// (or property of one) that hasn't been tainted
|
|
632
|
+
const rootVar = getRootIdentifier(node.expression);
|
|
633
|
+
if (rootVar && validatedVariables.has(rootVar)) {
|
|
634
|
+
// Check if the variable has been tainted (mutated, passed to function, etc.)
|
|
635
|
+
if (!isTainted(rootVar, visitedBody)) {
|
|
636
|
+
// Return expression is rooted at a validated, untainted variable
|
|
637
|
+
// For direct returns (identifier) or property access, we can skip validation
|
|
638
|
+
if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
|
|
639
|
+
return node; // Skip validation - already validated and untainted
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// For async functions, we need to await the expression before validating
|
|
644
|
+
// because the return expression might be a Promise
|
|
645
|
+
let expressionToValidate = node.expression;
|
|
646
|
+
if (isAsync) {
|
|
647
|
+
// Check if the expression is already an await expression
|
|
648
|
+
const isAlreadyAwaited = ts.isAwaitExpression(node.expression);
|
|
649
|
+
if (!isAlreadyAwaited) {
|
|
650
|
+
// Wrap in await: return validate(await expr)
|
|
651
|
+
expressionToValidate = ctx.factory.createAwaitExpression(node.expression);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
343
654
|
if (this.config.reusableValidators) {
|
|
344
655
|
// Use reusable validators - always use typeToString
|
|
345
656
|
const returnTypeText = typeChecker.typeToString(returnTypeForString);
|
|
346
657
|
const validatorName = this.getOrCreateValidator(returnTypeText, returnType);
|
|
347
|
-
const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [
|
|
658
|
+
const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [expressionToValidate]);
|
|
348
659
|
return ctx.factory.updateReturnStatement(node, validatorCall);
|
|
349
660
|
}
|
|
350
661
|
else {
|
|
@@ -352,7 +663,7 @@ export class TypicalTransformer {
|
|
|
352
663
|
const typiaIdentifier = ctx.factory.createIdentifier("typia");
|
|
353
664
|
const assertIdentifier = ctx.factory.createIdentifier("assert");
|
|
354
665
|
const propertyAccess = ctx.factory.createPropertyAccessExpression(typiaIdentifier, assertIdentifier);
|
|
355
|
-
const callExpression = ctx.factory.createCallExpression(propertyAccess, [returnType], [
|
|
666
|
+
const callExpression = ctx.factory.createCallExpression(propertyAccess, [returnType], [expressionToValidate]);
|
|
356
667
|
return ctx.factory.updateReturnStatement(node, callExpression);
|
|
357
668
|
}
|
|
358
669
|
}
|
|
@@ -418,7 +729,7 @@ export class TypicalTransformer {
|
|
|
418
729
|
if (this.typeValidators.has(typeText)) {
|
|
419
730
|
return this.typeValidators.get(typeText).name;
|
|
420
731
|
}
|
|
421
|
-
const validatorName = `
|
|
732
|
+
const validatorName = `__typical_` + `assert_${this.typeValidators.size}`;
|
|
422
733
|
this.typeValidators.set(typeText, { name: validatorName, typeNode });
|
|
423
734
|
return validatorName;
|
|
424
735
|
}
|
|
@@ -426,7 +737,7 @@ export class TypicalTransformer {
|
|
|
426
737
|
if (this.typeStringifiers.has(typeText)) {
|
|
427
738
|
return this.typeStringifiers.get(typeText).name;
|
|
428
739
|
}
|
|
429
|
-
const stringifierName = `
|
|
740
|
+
const stringifierName = `__typical_` + `stringify_${this.typeStringifiers.size}`;
|
|
430
741
|
this.typeStringifiers.set(typeText, { name: stringifierName, typeNode });
|
|
431
742
|
return stringifierName;
|
|
432
743
|
}
|
|
@@ -434,7 +745,7 @@ export class TypicalTransformer {
|
|
|
434
745
|
if (this.typeParsers.has(typeText)) {
|
|
435
746
|
return this.typeParsers.get(typeText).name;
|
|
436
747
|
}
|
|
437
|
-
const parserName = `
|
|
748
|
+
const parserName = `__typical_` + `parse_${this.typeParsers.size}`;
|
|
438
749
|
this.typeParsers.set(typeText, { name: parserName, typeNode });
|
|
439
750
|
return parserName;
|
|
440
751
|
}
|