@elliots/typical 0.1.4 → 0.1.6
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/dist/src/config.d.ts +12 -0
- package/dist/src/config.js +5 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/esm-loader.d.ts +4 -0
- package/dist/src/esm-loader.js +23 -1
- package/dist/src/esm-loader.js.map +1 -1
- package/dist/src/file-filter.js +4 -0
- package/dist/src/file-filter.js.map +1 -1
- package/dist/src/regex-hoister.d.ts +11 -0
- package/dist/src/regex-hoister.js +156 -0
- package/dist/src/regex-hoister.js.map +1 -0
- package/dist/src/setup.js +6 -0
- package/dist/src/setup.js.map +1 -1
- package/dist/src/transformer.d.ts +60 -0
- package/dist/src/transformer.js +471 -260
- package/dist/src/transformer.js.map +1 -1
- package/package.json +17 -17
- package/src/config.ts +18 -0
- package/src/esm-loader.ts +26 -1
- package/src/file-filter.ts +8 -3
- package/src/regex-hoister.ts +203 -0
- package/src/setup.ts +10 -0
- package/src/transformer.ts +557 -384
package/src/transformer.ts
CHANGED
|
@@ -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(x) to indicate intermediate state, preserving tsx
|
|
131
|
+
const intermediateFileName = relativePath.replace(/\.(tsx?)$/, ".typical.$1");
|
|
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
|
-
|
|
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,7 +248,34 @@ 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
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for untransformed typia calls - these indicate types typia couldn't process
|
|
262
|
+
const finalCode = printer.printFile(transformedSourceFile);
|
|
263
|
+
const untransformedCalls = this.findUntransformedTypiaCalls(finalCode);
|
|
264
|
+
if (untransformedCalls.length > 0) {
|
|
265
|
+
const failedTypes = untransformedCalls.map(c => c.type).filter((v, i, a) => a.indexOf(v) === i);
|
|
266
|
+
throw new Error(
|
|
267
|
+
`TYPICAL: Failed to transform the following types (typia cannot process them):\n` +
|
|
268
|
+
failedTypes.map(t => ` - ${t}`).join('\n') +
|
|
269
|
+
`\n\nTo skip validation for these types, add to ignoreTypes in typical.json:\n` +
|
|
270
|
+
` "ignoreTypes": [${failedTypes.map(t => `"${t}"`).join(', ')}]` +
|
|
271
|
+
`\n\nFile: ${sourceFile.fileName}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
219
274
|
} catch (error) {
|
|
275
|
+
// Re-throw our own errors, only catch typia transform failures
|
|
276
|
+
if (error instanceof Error && error.message.startsWith('TYPICAL:')) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
220
279
|
console.warn("Failed to apply typia transformer:", sourceFile.fileName, error);
|
|
221
280
|
}
|
|
222
281
|
}
|
|
@@ -233,8 +292,9 @@ export class TypicalTransformer {
|
|
|
233
292
|
*/
|
|
234
293
|
private recreateImportDeclaration(
|
|
235
294
|
importDecl: ts.ImportDeclaration,
|
|
236
|
-
factory: ts.NodeFactory
|
|
237
|
-
|
|
295
|
+
factory: ts.NodeFactory,
|
|
296
|
+
typeChecker: ts.TypeChecker
|
|
297
|
+
): ts.ImportDeclaration | undefined {
|
|
238
298
|
let importClause: ts.ImportClause | undefined;
|
|
239
299
|
|
|
240
300
|
if (importDecl.importClause) {
|
|
@@ -249,20 +309,54 @@ export class TypicalTransformer {
|
|
|
249
309
|
);
|
|
250
310
|
} else if (this.ts.isNamedImports(clause.namedBindings)) {
|
|
251
311
|
// import { foo, bar } from "baz"
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
312
|
+
// Filter out type-only imports (explicit or inferred from symbol)
|
|
313
|
+
const elements = clause.namedBindings.elements
|
|
314
|
+
.filter((el) => {
|
|
315
|
+
// Skip explicit type-only specifiers
|
|
316
|
+
if (el.isTypeOnly) return false;
|
|
317
|
+
// Check if the symbol is type-only (interface, type alias, etc.)
|
|
318
|
+
let symbol = typeChecker.getSymbolAtLocation(el.name);
|
|
319
|
+
// Follow alias to get the actual exported symbol
|
|
320
|
+
if (symbol && symbol.flags & this.ts.SymbolFlags.Alias) {
|
|
321
|
+
symbol = typeChecker.getAliasedSymbol(symbol);
|
|
322
|
+
}
|
|
323
|
+
if (symbol) {
|
|
324
|
+
const declarations = symbol.getDeclarations();
|
|
325
|
+
if (declarations && declarations.length > 0) {
|
|
326
|
+
// If all declarations are type-only, skip this import
|
|
327
|
+
const allTypeOnly = declarations.every((decl) =>
|
|
328
|
+
this.ts.isInterfaceDeclaration(decl) ||
|
|
329
|
+
this.ts.isTypeAliasDeclaration(decl) ||
|
|
330
|
+
this.ts.isTypeLiteralNode(decl)
|
|
331
|
+
);
|
|
332
|
+
if (allTypeOnly) return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
})
|
|
337
|
+
.map((el) =>
|
|
338
|
+
factory.createImportSpecifier(
|
|
339
|
+
false,
|
|
340
|
+
el.propertyName ? factory.createIdentifier(el.propertyName.text) : undefined,
|
|
341
|
+
factory.createIdentifier(el.name.text)
|
|
342
|
+
)
|
|
343
|
+
);
|
|
344
|
+
// Only create named imports if there are non-type specifiers
|
|
345
|
+
if (elements.length > 0) {
|
|
346
|
+
namedBindings = factory.createNamedImports(elements);
|
|
347
|
+
}
|
|
260
348
|
}
|
|
261
349
|
}
|
|
262
350
|
|
|
351
|
+
// Skip import entirely if no default import and no named bindings remain
|
|
352
|
+
const defaultName = clause.name ? factory.createIdentifier(clause.name.text) : undefined;
|
|
353
|
+
if (!defaultName && !namedBindings) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
263
357
|
importClause = factory.createImportClause(
|
|
264
358
|
clause.isTypeOnly,
|
|
265
|
-
|
|
359
|
+
defaultName,
|
|
266
360
|
namedBindings
|
|
267
361
|
);
|
|
268
362
|
}
|
|
@@ -289,11 +383,6 @@ export class TypicalTransformer {
|
|
|
289
383
|
): ts.SourceFile {
|
|
290
384
|
const { ts } = ctx;
|
|
291
385
|
|
|
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
386
|
if (!sourceFile.fileName.includes('transformer.test.ts')) {
|
|
298
387
|
// Check if this file has already been transformed by us
|
|
299
388
|
const sourceText = sourceFile.getFullText();
|
|
@@ -328,115 +417,22 @@ export class TypicalTransformer {
|
|
|
328
417
|
if (node.arguments.length > 0) {
|
|
329
418
|
const arg = node.arguments[0];
|
|
330
419
|
const argType = typeChecker.getTypeAtLocation(arg);
|
|
331
|
-
|
|
332
|
-
// Skip if type is any (1) or unknown (2)
|
|
333
|
-
if (typeFlags & ts.TypeFlags.Any || typeFlags & ts.TypeFlags.Unknown) {
|
|
420
|
+
if (this.isAnyOrUnknownTypeFlags(argType)) {
|
|
334
421
|
return node; // Don't transform JSON.stringify for any/unknown types
|
|
335
422
|
}
|
|
336
423
|
}
|
|
337
424
|
|
|
338
425
|
if (this.config.reusableValidators) {
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
);
|
|
426
|
+
// Infer type from argument
|
|
427
|
+
const arg = node.arguments[0];
|
|
428
|
+
const { typeText, typeNode } = this.inferStringifyType(arg, typeChecker, ctx);
|
|
432
429
|
|
|
433
|
-
const
|
|
430
|
+
const stringifierName = this.getOrCreateStringifier(typeText, typeNode);
|
|
431
|
+
return ctx.factory.createCallExpression(
|
|
434
432
|
ctx.factory.createIdentifier(stringifierName),
|
|
435
433
|
undefined,
|
|
436
434
|
node.arguments
|
|
437
435
|
);
|
|
438
|
-
|
|
439
|
-
return newCall;
|
|
440
436
|
} else {
|
|
441
437
|
// Use inline typia.json.stringify
|
|
442
438
|
return ctx.factory.updateCallExpression(
|
|
@@ -491,13 +487,7 @@ export class TypicalTransformer {
|
|
|
491
487
|
parent = parent.parent;
|
|
492
488
|
}
|
|
493
489
|
|
|
494
|
-
|
|
495
|
-
const isAnyOrUnknown = targetType && (
|
|
496
|
-
targetType.kind === ts.SyntaxKind.AnyKeyword ||
|
|
497
|
-
targetType.kind === ts.SyntaxKind.UnknownKeyword
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
if (isAnyOrUnknown) {
|
|
490
|
+
if (targetType && this.isAnyOrUnknownType(targetType)) {
|
|
501
491
|
// Don't transform JSON.parse for any/unknown types
|
|
502
492
|
return node;
|
|
503
493
|
}
|
|
@@ -509,9 +499,8 @@ export class TypicalTransformer {
|
|
|
509
499
|
}
|
|
510
500
|
|
|
511
501
|
if (this.config.reusableValidators && targetType) {
|
|
512
|
-
// Use reusable parser - use
|
|
513
|
-
const
|
|
514
|
-
const typeText = typeChecker.typeToString(targetTypeObj);
|
|
502
|
+
// Use reusable parser - use typeNode text to preserve local aliases
|
|
503
|
+
const typeText = this.getTypeKey(targetType, typeChecker);
|
|
515
504
|
const parserName = this.getOrCreateParser(typeText, targetType);
|
|
516
505
|
|
|
517
506
|
const newCall = ctx.factory.createCallExpression(
|
|
@@ -544,6 +533,52 @@ export class TypicalTransformer {
|
|
|
544
533
|
}
|
|
545
534
|
}
|
|
546
535
|
|
|
536
|
+
// Transform type assertions (as expressions) when validateCasts is enabled
|
|
537
|
+
// e.g., `obj as User` becomes `__typical_assert_N(obj)`
|
|
538
|
+
if (this.config.validateCasts && ts.isAsExpression(node)) {
|
|
539
|
+
const targetType = node.type;
|
|
540
|
+
|
|
541
|
+
// Skip 'as any' and 'as unknown' casts - these are intentional escapes
|
|
542
|
+
if (this.isAnyOrUnknownType(targetType)) {
|
|
543
|
+
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Skip primitive types - no runtime validation needed
|
|
547
|
+
if (targetType.kind === ts.SyntaxKind.StringKeyword ||
|
|
548
|
+
targetType.kind === ts.SyntaxKind.NumberKeyword ||
|
|
549
|
+
targetType.kind === ts.SyntaxKind.BooleanKeyword) {
|
|
550
|
+
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
needsTypiaImport = true;
|
|
554
|
+
|
|
555
|
+
// Visit the expression first to transform any nested casts
|
|
556
|
+
const visitedExpression = ctx.ts.visitNode(node.expression, visit) as ts.Expression;
|
|
557
|
+
|
|
558
|
+
if (this.config.reusableValidators) {
|
|
559
|
+
// Use typeNode text to preserve local aliases
|
|
560
|
+
const typeText = this.getTypeKey(targetType, typeChecker);
|
|
561
|
+
const validatorName = this.getOrCreateValidator(typeText, targetType);
|
|
562
|
+
|
|
563
|
+
// Replace `expr as Type` with `__typical_assert_N(expr)`
|
|
564
|
+
return ctx.factory.createCallExpression(
|
|
565
|
+
ctx.factory.createIdentifier(validatorName),
|
|
566
|
+
undefined,
|
|
567
|
+
[visitedExpression]
|
|
568
|
+
);
|
|
569
|
+
} else {
|
|
570
|
+
// Inline validator: typia.assert<Type>(expr)
|
|
571
|
+
return ctx.factory.createCallExpression(
|
|
572
|
+
ctx.factory.createPropertyAccessExpression(
|
|
573
|
+
ctx.factory.createIdentifier("typia"),
|
|
574
|
+
"assert"
|
|
575
|
+
),
|
|
576
|
+
[targetType],
|
|
577
|
+
[visitedExpression]
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
547
582
|
// Transform function declarations
|
|
548
583
|
if (ts.isFunctionDeclaration(node)) {
|
|
549
584
|
needsTypiaImport = true;
|
|
@@ -565,139 +600,6 @@ export class TypicalTransformer {
|
|
|
565
600
|
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
566
601
|
};
|
|
567
602
|
|
|
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
603
|
const transformFunction = (
|
|
702
604
|
func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration
|
|
703
605
|
): ts.Node => {
|
|
@@ -732,8 +634,19 @@ export class TypicalTransformer {
|
|
|
732
634
|
func.parameters.forEach((param) => {
|
|
733
635
|
if (param.type) {
|
|
734
636
|
// Skip 'any' and 'unknown' types - no point validating them
|
|
735
|
-
if (param.type
|
|
736
|
-
|
|
637
|
+
if (this.isAnyOrUnknownType(param.type)) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Skip types matching ignoreTypes patterns
|
|
642
|
+
const typeText = this.getTypeKey(param.type, typeChecker);
|
|
643
|
+
if (process.env.DEBUG) {
|
|
644
|
+
console.log(`TYPICAL: Processing parameter type: ${typeText}`);
|
|
645
|
+
}
|
|
646
|
+
if (this.isIgnoredType(typeText)) {
|
|
647
|
+
if (process.env.DEBUG) {
|
|
648
|
+
console.log(`TYPICAL: Skipping ignored type for parameter: ${typeText}`);
|
|
649
|
+
}
|
|
737
650
|
return;
|
|
738
651
|
}
|
|
739
652
|
|
|
@@ -747,8 +660,7 @@ export class TypicalTransformer {
|
|
|
747
660
|
validatedVariables.set(paramName, paramType);
|
|
748
661
|
|
|
749
662
|
if (this.config.reusableValidators) {
|
|
750
|
-
// Use reusable validators - use
|
|
751
|
-
const typeText = typeChecker.typeToString(paramType);
|
|
663
|
+
// Use reusable validators - use typeNode text to preserve local aliases
|
|
752
664
|
const validatorName = this.getOrCreateValidator(
|
|
753
665
|
typeText,
|
|
754
666
|
param.type
|
|
@@ -796,8 +708,7 @@ export class TypicalTransformer {
|
|
|
796
708
|
for (const decl of node.declarationList.declarations) {
|
|
797
709
|
if (decl.type && ts.isIdentifier(decl.name)) {
|
|
798
710
|
// Skip any/unknown types
|
|
799
|
-
if (decl.type
|
|
800
|
-
decl.type.kind !== ts.SyntaxKind.UnknownKeyword) {
|
|
711
|
+
if (!this.isAnyOrUnknownType(decl.type)) {
|
|
801
712
|
const constType = typeChecker.getTypeFromTypeNode(decl.type);
|
|
802
713
|
validatedVariables.set(decl.name.text, constType);
|
|
803
714
|
}
|
|
@@ -866,12 +777,15 @@ export class TypicalTransformer {
|
|
|
866
777
|
}
|
|
867
778
|
|
|
868
779
|
// Skip 'any' and 'unknown' return types - no point validating them
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
returnType
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (
|
|
780
|
+
// Also skip types matching ignoreTypes patterns
|
|
781
|
+
const returnTypeText = returnType && returnTypeForString
|
|
782
|
+
? this.getTypeKey(returnType, typeChecker, returnTypeForString)
|
|
783
|
+
: null;
|
|
784
|
+
const isIgnoredReturnType = returnTypeText && this.isIgnoredType(returnTypeText);
|
|
785
|
+
if (isIgnoredReturnType && process.env.DEBUG) {
|
|
786
|
+
console.log(`TYPICAL: Skipping ignored type for return: ${returnTypeText}`);
|
|
787
|
+
}
|
|
788
|
+
if (returnType && returnTypeForString && !this.isAnyOrUnknownType(returnType) && !isIgnoredReturnType) {
|
|
875
789
|
const returnTransformer = (node: ts.Node): ts.Node => {
|
|
876
790
|
if (ts.isReturnStatement(node) && node.expression) {
|
|
877
791
|
// Skip return validation if the expression already contains a __typical _parse_* call
|
|
@@ -891,10 +805,10 @@ export class TypicalTransformer {
|
|
|
891
805
|
|
|
892
806
|
// Flow analysis: Skip return validation if returning a validated variable
|
|
893
807
|
// (or property of one) that hasn't been tainted
|
|
894
|
-
const rootVar = getRootIdentifier(node.expression);
|
|
808
|
+
const rootVar = this.getRootIdentifier(node.expression);
|
|
895
809
|
if (rootVar && validatedVariables.has(rootVar)) {
|
|
896
810
|
// Check if the variable has been tainted (mutated, passed to function, etc.)
|
|
897
|
-
if (!isTainted(rootVar, visitedBody)) {
|
|
811
|
+
if (!this.isTainted(rootVar, visitedBody)) {
|
|
898
812
|
// Return expression is rooted at a validated, untainted variable
|
|
899
813
|
// For direct returns (identifier) or property access, we can skip validation
|
|
900
814
|
if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
|
|
@@ -918,8 +832,9 @@ export class TypicalTransformer {
|
|
|
918
832
|
}
|
|
919
833
|
|
|
920
834
|
if (this.config.reusableValidators) {
|
|
921
|
-
// Use reusable validators -
|
|
922
|
-
|
|
835
|
+
// Use reusable validators - use typeNode text to preserve local aliases
|
|
836
|
+
// Pass returnTypeForString for synthesized nodes (inferred return types)
|
|
837
|
+
const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
|
|
923
838
|
const validatorName = this.getOrCreateValidator(
|
|
924
839
|
returnTypeText,
|
|
925
840
|
returnType
|
|
@@ -1045,6 +960,246 @@ export class TypicalTransformer {
|
|
|
1045
960
|
return shouldTransformFile(fileName, this.config);
|
|
1046
961
|
}
|
|
1047
962
|
|
|
963
|
+
/**
|
|
964
|
+
* Check if a TypeNode represents any or unknown type.
|
|
965
|
+
* These types are intentional escape hatches and shouldn't be validated.
|
|
966
|
+
*/
|
|
967
|
+
private isAnyOrUnknownType(typeNode: ts.TypeNode): boolean {
|
|
968
|
+
return typeNode.kind === this.ts.SyntaxKind.AnyKeyword ||
|
|
969
|
+
typeNode.kind === this.ts.SyntaxKind.UnknownKeyword;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Check if a Type has any or unknown flags.
|
|
974
|
+
*/
|
|
975
|
+
private isAnyOrUnknownTypeFlags(type: ts.Type): boolean {
|
|
976
|
+
return (type.flags & this.ts.TypeFlags.Any) !== 0 ||
|
|
977
|
+
(type.flags & this.ts.TypeFlags.Unknown) !== 0;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Check if a type name matches any of the ignoreTypes patterns.
|
|
982
|
+
* Supports wildcards: "React.*" matches "React.FormEvent", "React.ChangeEvent", etc.
|
|
983
|
+
*/
|
|
984
|
+
private isIgnoredType(typeName: string): boolean {
|
|
985
|
+
const patterns = this.config.ignoreTypes ?? [];
|
|
986
|
+
if (patterns.length === 0) return false;
|
|
987
|
+
|
|
988
|
+
return patterns.some(pattern => {
|
|
989
|
+
// Convert glob pattern to regex: "React.*" -> /^React\..*$/
|
|
990
|
+
const regexStr = '^' + pattern
|
|
991
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except *
|
|
992
|
+
.replace(/\*/g, '.*') + '$';
|
|
993
|
+
return new RegExp(regexStr).test(typeName);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Find untransformed typia calls in the output code.
|
|
999
|
+
* These indicate types that typia could not process.
|
|
1000
|
+
*/
|
|
1001
|
+
private findUntransformedTypiaCalls(code: string): Array<{ method: string; type: string }> {
|
|
1002
|
+
const results: Array<{ method: string; type: string }> = [];
|
|
1003
|
+
|
|
1004
|
+
// Match patterns like: typia.createAssert<Type>() or typia.json.createAssertParse<Type>()
|
|
1005
|
+
// The type argument can contain nested generics like React.FormEvent<HTMLElement>
|
|
1006
|
+
const patterns = [
|
|
1007
|
+
/typia\.createAssert<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
|
|
1008
|
+
/typia\.json\.createAssertParse<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
|
|
1009
|
+
/typia\.json\.createStringify<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
|
|
1010
|
+
];
|
|
1011
|
+
|
|
1012
|
+
for (const pattern of patterns) {
|
|
1013
|
+
let match;
|
|
1014
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
1015
|
+
const methodMatch = match[0].match(/typia\.([\w.]+)</);
|
|
1016
|
+
results.push({
|
|
1017
|
+
method: methodMatch ? methodMatch[1] : 'unknown',
|
|
1018
|
+
type: match[1]
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return results;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Infer type information from a JSON.stringify argument for creating a reusable stringifier.
|
|
1028
|
+
*/
|
|
1029
|
+
private inferStringifyType(
|
|
1030
|
+
arg: ts.Expression,
|
|
1031
|
+
typeChecker: ts.TypeChecker,
|
|
1032
|
+
ctx: TransformContext
|
|
1033
|
+
): { typeText: string; typeNode: ts.TypeNode } {
|
|
1034
|
+
const ts = this.ts;
|
|
1035
|
+
|
|
1036
|
+
// Type assertion: use the asserted type directly
|
|
1037
|
+
if (ts.isAsExpression(arg)) {
|
|
1038
|
+
const typeNode = arg.type;
|
|
1039
|
+
const typeString = typeChecker.typeToString(typeChecker.getTypeFromTypeNode(typeNode));
|
|
1040
|
+
return {
|
|
1041
|
+
typeText: `Asserted_${typeString.replace(/[^a-zA-Z0-9_]/g, "_")}`,
|
|
1042
|
+
typeNode,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Object literal: use property names for the key
|
|
1047
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
1048
|
+
const objectType = typeChecker.getTypeAtLocation(arg);
|
|
1049
|
+
const typeNode = typeChecker.typeToTypeNode(objectType, arg, ts.NodeBuilderFlags.InTypeAlias);
|
|
1050
|
+
if (!typeNode) {
|
|
1051
|
+
throw new Error('unknown type node for object literal: ' + arg.getText());
|
|
1052
|
+
}
|
|
1053
|
+
const propNames = arg.properties
|
|
1054
|
+
.map((prop) => {
|
|
1055
|
+
if (ts.isShorthandPropertyAssignment(prop)) return prop.name.text;
|
|
1056
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) return prop.name.text;
|
|
1057
|
+
return "unknown";
|
|
1058
|
+
})
|
|
1059
|
+
.sort()
|
|
1060
|
+
.join("_");
|
|
1061
|
+
return { typeText: `ObjectLiteral_${propNames}`, typeNode };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Other expressions: infer from type checker
|
|
1065
|
+
const argType = typeChecker.getTypeAtLocation(arg);
|
|
1066
|
+
const typeNode = typeChecker.typeToTypeNode(argType, arg, ts.NodeBuilderFlags.InTypeAlias);
|
|
1067
|
+
if (typeNode) {
|
|
1068
|
+
const typeString = typeChecker.typeToString(argType);
|
|
1069
|
+
return {
|
|
1070
|
+
typeText: `Expression_${typeString.replace(/[^a-zA-Z0-9_]/g, "_")}`,
|
|
1071
|
+
typeNode,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Fallback to unknown
|
|
1076
|
+
return {
|
|
1077
|
+
typeText: "unknown",
|
|
1078
|
+
typeNode: ctx.factory.createKeywordTypeNode(ctx.ts.SyntaxKind.UnknownKeyword),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ============================================
|
|
1083
|
+
// Flow Analysis Helpers
|
|
1084
|
+
// ============================================
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Gets the root identifier from an expression.
|
|
1088
|
+
* e.g., `user.address.city` -> "user"
|
|
1089
|
+
*/
|
|
1090
|
+
private getRootIdentifier(expr: ts.Expression): string | undefined {
|
|
1091
|
+
if (this.ts.isIdentifier(expr)) {
|
|
1092
|
+
return expr.text;
|
|
1093
|
+
}
|
|
1094
|
+
if (this.ts.isPropertyAccessExpression(expr)) {
|
|
1095
|
+
return this.getRootIdentifier(expr.expression);
|
|
1096
|
+
}
|
|
1097
|
+
return undefined;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Check if a validated variable has been tainted (mutated) in the function body.
|
|
1102
|
+
* A variable is tainted if it's reassigned, has properties modified, is passed
|
|
1103
|
+
* to a function, has methods called on it, or if an await occurs.
|
|
1104
|
+
*/
|
|
1105
|
+
private isTainted(varName: string, body: ts.Block): boolean {
|
|
1106
|
+
let tainted = false;
|
|
1107
|
+
const ts = this.ts;
|
|
1108
|
+
|
|
1109
|
+
// Collect aliases (variables that reference properties of varName)
|
|
1110
|
+
// e.g., const addr = user.address; -> addr is an alias
|
|
1111
|
+
const aliases = new Set<string>([varName]);
|
|
1112
|
+
|
|
1113
|
+
const collectAliases = (node: ts.Node): void => {
|
|
1114
|
+
if (ts.isVariableStatement(node)) {
|
|
1115
|
+
for (const decl of node.declarationList.declarations) {
|
|
1116
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
1117
|
+
const initRoot = this.getRootIdentifier(decl.initializer);
|
|
1118
|
+
if (initRoot && aliases.has(initRoot)) {
|
|
1119
|
+
aliases.add(decl.name.text);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
ts.forEachChild(node, collectAliases);
|
|
1125
|
+
};
|
|
1126
|
+
collectAliases(body);
|
|
1127
|
+
|
|
1128
|
+
// Helper to check if any alias is involved
|
|
1129
|
+
const involvesTrackedVar = (expr: ts.Expression): boolean => {
|
|
1130
|
+
const root = this.getRootIdentifier(expr);
|
|
1131
|
+
return root !== undefined && aliases.has(root);
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const checkTainting = (node: ts.Node): void => {
|
|
1135
|
+
if (tainted) return;
|
|
1136
|
+
|
|
1137
|
+
// Reassignment: trackedVar = ...
|
|
1138
|
+
if (ts.isBinaryExpression(node) &&
|
|
1139
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1140
|
+
ts.isIdentifier(node.left) &&
|
|
1141
|
+
aliases.has(node.left.text)) {
|
|
1142
|
+
tainted = true;
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Property assignment: trackedVar.x = ... or alias.x = ...
|
|
1147
|
+
if (ts.isBinaryExpression(node) &&
|
|
1148
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1149
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
1150
|
+
involvesTrackedVar(node.left)) {
|
|
1151
|
+
tainted = true;
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Element assignment: trackedVar[x] = ... or alias[x] = ...
|
|
1156
|
+
if (ts.isBinaryExpression(node) &&
|
|
1157
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1158
|
+
ts.isElementAccessExpression(node.left) &&
|
|
1159
|
+
involvesTrackedVar(node.left.expression)) {
|
|
1160
|
+
tainted = true;
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Passed as argument to a function: fn(trackedVar) or fn(alias)
|
|
1165
|
+
if (ts.isCallExpression(node)) {
|
|
1166
|
+
for (const arg of node.arguments) {
|
|
1167
|
+
let hasTrackedRef = false;
|
|
1168
|
+
const checkRef = (n: ts.Node): void => {
|
|
1169
|
+
if (ts.isIdentifier(n) && aliases.has(n.text)) {
|
|
1170
|
+
hasTrackedRef = true;
|
|
1171
|
+
}
|
|
1172
|
+
ts.forEachChild(n, checkRef);
|
|
1173
|
+
};
|
|
1174
|
+
checkRef(arg);
|
|
1175
|
+
if (hasTrackedRef) {
|
|
1176
|
+
tainted = true;
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Method call on the variable: trackedVar.method() or alias.method()
|
|
1183
|
+
if (ts.isCallExpression(node) &&
|
|
1184
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
1185
|
+
involvesTrackedVar(node.expression.expression)) {
|
|
1186
|
+
tainted = true;
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Await expression (async boundary - external code could run)
|
|
1191
|
+
if (ts.isAwaitExpression(node)) {
|
|
1192
|
+
tainted = true;
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
ts.forEachChild(node, checkTainting);
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
checkTainting(body);
|
|
1200
|
+
return tainted;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1048
1203
|
private addTypiaImport(
|
|
1049
1204
|
sourceFile: ts.SourceFile,
|
|
1050
1205
|
ctx: TransformContext
|
|
@@ -1083,135 +1238,153 @@ export class TypicalTransformer {
|
|
|
1083
1238
|
return sourceFile;
|
|
1084
1239
|
}
|
|
1085
1240
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1241
|
+
/**
|
|
1242
|
+
* Gets type text for use as a validator map key.
|
|
1243
|
+
* Uses getText() to preserve local aliases (e.g., "User1" vs "User2"),
|
|
1244
|
+
* but falls back to typeToString() for synthesized nodes without source positions.
|
|
1245
|
+
*
|
|
1246
|
+
* @param typeNode The TypeNode to get a key for
|
|
1247
|
+
* @param typeChecker The TypeChecker to use
|
|
1248
|
+
* @param typeObj Optional Type object - use this for synthesized nodes since
|
|
1249
|
+
* getTypeFromTypeNode doesn't work correctly on them
|
|
1250
|
+
*/
|
|
1251
|
+
private getTypeKey(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker, typeObj?: ts.Type): string {
|
|
1252
|
+
// Check if node has a real position (not synthesized)
|
|
1253
|
+
if (typeNode.pos >= 0 && typeNode.end > typeNode.pos) {
|
|
1254
|
+
try {
|
|
1255
|
+
return typeNode.getText();
|
|
1256
|
+
} catch {
|
|
1257
|
+
// Fall through to typeToString
|
|
1258
|
+
}
|
|
1092
1259
|
}
|
|
1260
|
+
// Fallback for synthesized nodes - use the provided Type object if available,
|
|
1261
|
+
// otherwise try to get it from the node (which may not work correctly)
|
|
1262
|
+
const type = typeObj ?? typeChecker.getTypeFromTypeNode(typeNode);
|
|
1263
|
+
return typeChecker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
|
|
1264
|
+
}
|
|
1093
1265
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1266
|
+
/**
|
|
1267
|
+
* Creates a readable name suffix from a type string.
|
|
1268
|
+
* For simple identifiers like "User" or "string", returns the name directly.
|
|
1269
|
+
* For complex types, returns a numeric index.
|
|
1270
|
+
*/
|
|
1271
|
+
private getTypeNameSuffix(typeText: string, existingNames: Set<string>, fallbackIndex: number): string {
|
|
1272
|
+
// Strip known prefixes that wrap the actual type name
|
|
1273
|
+
let normalizedTypeText = typeText;
|
|
1274
|
+
if (typeText.startsWith('Expression_')) {
|
|
1275
|
+
normalizedTypeText = typeText.slice('Expression_'.length);
|
|
1276
|
+
} else if (typeText.startsWith('ObjectLiteral_')) {
|
|
1277
|
+
// Object literals use property names, fall back to numeric
|
|
1278
|
+
return String(fallbackIndex);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Check if it's a simple identifier (letters, numbers, underscore, starting with letter/underscore)
|
|
1282
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(normalizedTypeText)) {
|
|
1283
|
+
// It's a simple type name like "User", "string", "MyType"
|
|
1284
|
+
let name = normalizedTypeText;
|
|
1285
|
+
// Handle collisions by appending a number
|
|
1286
|
+
if (existingNames.has(name)) {
|
|
1287
|
+
let i = 2;
|
|
1288
|
+
while (existingNames.has(`${normalizedTypeText}${i}`)) {
|
|
1289
|
+
i++;
|
|
1290
|
+
}
|
|
1291
|
+
name = `${normalizedTypeText}${i}`;
|
|
1292
|
+
}
|
|
1293
|
+
return name;
|
|
1294
|
+
}
|
|
1295
|
+
// Complex type - use numeric index
|
|
1296
|
+
return String(fallbackIndex);
|
|
1097
1297
|
}
|
|
1098
1298
|
|
|
1099
|
-
|
|
1299
|
+
/**
|
|
1300
|
+
* Generic method to get or create a typed function (validator, stringifier, or parser).
|
|
1301
|
+
*/
|
|
1302
|
+
private getOrCreateTypedFunction(
|
|
1303
|
+
kind: 'assert' | 'stringify' | 'parse',
|
|
1100
1304
|
typeText: string,
|
|
1101
1305
|
typeNode: ts.TypeNode
|
|
1102
1306
|
): string {
|
|
1103
|
-
|
|
1104
|
-
|
|
1307
|
+
const maps = {
|
|
1308
|
+
assert: this.typeValidators,
|
|
1309
|
+
stringify: this.typeStringifiers,
|
|
1310
|
+
parse: this.typeParsers,
|
|
1311
|
+
};
|
|
1312
|
+
const prefixes = {
|
|
1313
|
+
assert: '__typical_assert_',
|
|
1314
|
+
stringify: '__typical_stringify_',
|
|
1315
|
+
parse: '__typical_parse_',
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
const map = maps[kind];
|
|
1319
|
+
const prefix = prefixes[kind];
|
|
1320
|
+
|
|
1321
|
+
if (map.has(typeText)) {
|
|
1322
|
+
return map.get(typeText)!.name;
|
|
1105
1323
|
}
|
|
1106
1324
|
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1325
|
+
const existingSuffixes = [...map.values()].map(v => v.name.slice(prefix.length));
|
|
1326
|
+
const existingNames = new Set(existingSuffixes);
|
|
1327
|
+
const numericCount = existingSuffixes.filter(s => /^\d+$/.test(s)).length;
|
|
1328
|
+
const suffix = this.getTypeNameSuffix(typeText, existingNames, numericCount);
|
|
1329
|
+
const name = `${prefix}${suffix}`;
|
|
1330
|
+
map.set(typeText, { name, typeNode });
|
|
1331
|
+
return name;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
private getOrCreateValidator(typeText: string, typeNode: ts.TypeNode): string {
|
|
1335
|
+
return this.getOrCreateTypedFunction('assert', typeText, typeNode);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
private getOrCreateStringifier(typeText: string, typeNode: ts.TypeNode): string {
|
|
1339
|
+
return this.getOrCreateTypedFunction('stringify', typeText, typeNode);
|
|
1110
1340
|
}
|
|
1111
1341
|
|
|
1112
1342
|
private getOrCreateParser(typeText: string, typeNode: ts.TypeNode): string {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1343
|
+
return this.getOrCreateTypedFunction('parse', typeText, typeNode);
|
|
1344
|
+
}
|
|
1116
1345
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1346
|
+
/**
|
|
1347
|
+
* Creates a nested property access expression from an array of identifiers.
|
|
1348
|
+
* e.g., ['typia', 'json', 'createStringify'] -> typia.json.createStringify
|
|
1349
|
+
*/
|
|
1350
|
+
private createPropertyAccessChain(factory: ts.NodeFactory, parts: string[]): ts.Expression {
|
|
1351
|
+
let expr: ts.Expression = factory.createIdentifier(parts[0]);
|
|
1352
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1353
|
+
expr = factory.createPropertyAccessExpression(expr, parts[i]);
|
|
1354
|
+
}
|
|
1355
|
+
return expr;
|
|
1120
1356
|
}
|
|
1121
1357
|
|
|
1122
1358
|
private createValidatorStatements(ctx: TransformContext): ts.Statement[] {
|
|
1123
1359
|
const { factory } = ctx;
|
|
1124
1360
|
const statements: ts.Statement[] = [];
|
|
1125
1361
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
[
|
|
1141
|
-
|
|
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
|
-
);
|
|
1362
|
+
const configs: Array<{
|
|
1363
|
+
map: Map<string, { name: string; typeNode: ts.TypeNode }>;
|
|
1364
|
+
methodPath: string[];
|
|
1365
|
+
}> = [
|
|
1366
|
+
{ map: this.typeValidators, methodPath: ['typia', 'createAssert'] },
|
|
1367
|
+
{ map: this.typeStringifiers, methodPath: ['typia', 'json', 'createStringify'] },
|
|
1368
|
+
{ map: this.typeParsers, methodPath: ['typia', 'json', 'createAssertParse'] },
|
|
1369
|
+
];
|
|
1370
|
+
|
|
1371
|
+
for (const { map, methodPath } of configs) {
|
|
1372
|
+
for (const [, { name, typeNode }] of map) {
|
|
1373
|
+
const createCall = factory.createCallExpression(
|
|
1374
|
+
this.createPropertyAccessChain(factory, methodPath),
|
|
1375
|
+
[typeNode],
|
|
1376
|
+
[]
|
|
1377
|
+
);
|
|
1199
1378
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
),
|
|
1210
|
-
],
|
|
1211
|
-
ctx.ts.NodeFlags.Const
|
|
1212
|
-
)
|
|
1213
|
-
);
|
|
1214
|
-
statements.push(parserDeclaration);
|
|
1379
|
+
const declaration = factory.createVariableStatement(
|
|
1380
|
+
undefined,
|
|
1381
|
+
factory.createVariableDeclarationList(
|
|
1382
|
+
[factory.createVariableDeclaration(name, undefined, undefined, createCall)],
|
|
1383
|
+
ctx.ts.NodeFlags.Const
|
|
1384
|
+
)
|
|
1385
|
+
);
|
|
1386
|
+
statements.push(declaration);
|
|
1387
|
+
}
|
|
1215
1388
|
}
|
|
1216
1389
|
|
|
1217
1390
|
return statements;
|