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