@elliots/typical 0.1.10 → 0.2.0
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 +234 -200
- package/dist/src/cli.js +12 -85
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cli.typical.ts +136 -0
- package/dist/src/config.d.ts +12 -0
- package/dist/src/config.js +40 -38
- package/dist/src/config.js.map +1 -1
- package/dist/src/config.typical.ts +287 -0
- package/dist/src/esm-loader-register.js.map +1 -1
- package/dist/src/esm-loader.d.ts +1 -1
- package/dist/src/esm-loader.js +30 -17
- package/dist/src/esm-loader.js.map +1 -1
- package/dist/src/file-filter.d.ts +1 -1
- package/dist/src/file-filter.js.map +1 -1
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/program-manager.d.ts +27 -0
- package/dist/src/program-manager.js +121 -0
- package/dist/src/program-manager.js.map +1 -0
- package/dist/src/regex-hoister.d.ts +1 -1
- package/dist/src/regex-hoister.js +13 -19
- package/dist/src/regex-hoister.js.map +1 -1
- package/dist/src/setup.d.ts +1 -1
- package/dist/src/setup.js +3 -3
- package/dist/src/setup.js.map +1 -1
- package/dist/src/source-map.d.ts +1 -1
- package/dist/src/source-map.js +1 -1
- package/dist/src/source-map.js.map +1 -1
- package/dist/src/source-map.typical.ts +216 -0
- package/dist/src/timing.d.ts +19 -0
- package/dist/src/timing.js +65 -0
- package/dist/src/timing.js.map +1 -0
- package/dist/src/transformer.d.ts +28 -193
- package/dist/src/transformer.js +41 -1917
- package/dist/src/transformer.js.map +1 -1
- package/dist/src/transformer.typical.ts +2552 -0
- package/dist/src/tsc-plugin.d.ts +8 -1
- package/dist/src/tsc-plugin.js +11 -7
- package/dist/src/tsc-plugin.js.map +1 -1
- package/package.json +51 -47
- package/src/cli.ts +41 -128
- package/src/config.ts +106 -91
- package/src/esm-loader-register.ts +2 -2
- package/src/esm-loader.ts +44 -29
- package/src/index.ts +5 -10
- package/src/patch-fs.cjs +14 -14
- package/src/timing.ts +74 -0
- package/src/transformer.ts +47 -2592
- package/bin/ttsc +0 -12
- package/src/file-filter.ts +0 -49
- package/src/patch-tsconfig.cjs +0 -52
- package/src/regex-hoister.ts +0 -203
- package/src/setup.ts +0 -39
- package/src/source-map.ts +0 -202
- package/src/tsc-plugin.ts +0 -12
package/dist/src/transformer.js
CHANGED
|
@@ -1,1940 +1,64 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { loadConfig, getCompiledIgnorePatterns } from "./config.js";
|
|
5
|
-
import { shouldTransformFile } from "./file-filter.js";
|
|
6
|
-
import { hoistRegexConstructors } from "./regex-hoister.js";
|
|
7
|
-
import { composeSourceMaps, } from "./source-map.js";
|
|
8
|
-
import { transform as typiaTransform } from "typia/lib/transform.js";
|
|
9
|
-
import { setupTsProgram } from "./setup.js";
|
|
10
|
-
// Flags for typeToTypeNode to prefer type aliases over import() syntax
|
|
11
|
-
const TYPE_NODE_FLAGS = ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
|
|
12
|
-
// Source map markers:
|
|
13
|
-
// - @T:line:col - Type annotation marker (maps generated code to source type annotation)
|
|
14
|
-
// - @L:line - Line marker (identity mapping - maps output line to source line)
|
|
15
|
-
//
|
|
16
|
-
// Lines with @T markers map to the specified type annotation position
|
|
17
|
-
// Lines with @L markers establish identity mapping (output line N maps to source line N)
|
|
18
|
-
// Lines without markers inherit from the most recent marker above
|
|
19
|
-
//
|
|
20
|
-
// Match single-line comment markers: //@T:line:col or //@L:line
|
|
21
|
-
const TYPE_MARKER_REGEX = /\/\/@T:(\d+):(\d+)/g;
|
|
22
|
-
const LINE_MARKER_REGEX = /\/\/@L:(\d+)/g;
|
|
23
|
-
// Strip all markers
|
|
24
|
-
const ALL_MARKERS_REGEX = /\/\/@[TL]:\d+(?::\d+)?\n?/g;
|
|
25
1
|
/**
|
|
26
|
-
*
|
|
27
|
-
* The marker encodes the original line:column position of the type annotation
|
|
28
|
-
* so validation errors can be traced back to the source.
|
|
2
|
+
* TypicalTransformer - Thin wrapper around the Go compiler.
|
|
29
3
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
4
|
+
* The Go compiler (compiler) handles all TypeScript analysis
|
|
5
|
+
* and validation code generation. This class just manages the lifecycle
|
|
6
|
+
* and communication with the Go process.
|
|
32
7
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Use 1-based line numbers for source maps
|
|
37
|
-
// Single-line comment forces a newline after it
|
|
38
|
-
const marker = `@T:${line + 1}:${character}`;
|
|
39
|
-
if (process.env.DEBUG) {
|
|
40
|
-
console.log(`TYPICAL: Adding source map marker //${marker}`);
|
|
41
|
-
}
|
|
42
|
-
const result = ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, marker, true // trailing newline
|
|
43
|
-
);
|
|
44
|
-
if (process.env.DEBUG) {
|
|
45
|
-
const comments = ts.getSyntheticLeadingComments(result);
|
|
46
|
-
console.log(`TYPICAL: Synthetic comments after addSourceMapMarker:`, comments?.length);
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Add a line marker comment to a node for identity mapping.
|
|
52
|
-
* The marker encodes the original line number so the output line maps to itself.
|
|
53
|
-
*
|
|
54
|
-
* Uses a single-line comment (//) which forces a newline after it.
|
|
55
|
-
*/
|
|
56
|
-
function addLineMarker(node, sourceFile, originalNode) {
|
|
57
|
-
const pos = originalNode.getStart(sourceFile);
|
|
58
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
59
|
-
// Use 1-based line numbers for source maps
|
|
60
|
-
const marker = `@L:${line + 1}`;
|
|
61
|
-
if (process.env.DEBUG) {
|
|
62
|
-
console.log(`TYPICAL: Adding line marker //${marker}`);
|
|
63
|
-
}
|
|
64
|
-
return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, marker, true // trailing newline
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Parse source map markers from code and build a source map.
|
|
69
|
-
* Markers are single-line comments on their own line:
|
|
70
|
-
* - //@T:line:col - Type annotation marker (maps to specific source position)
|
|
71
|
-
* - //@L:line - Line marker (identity mapping to source line, col 0)
|
|
72
|
-
*
|
|
73
|
-
* The marker applies to the NEXT line (the actual code statement).
|
|
74
|
-
* Lines without markers inherit from the most recent marker above.
|
|
75
|
-
* Returns the code with markers stripped and the generated source map.
|
|
76
|
-
*/
|
|
77
|
-
function parseMarkersAndBuildSourceMap(code, fileName, originalSource, includeContent) {
|
|
78
|
-
const lines = code.split('\n');
|
|
79
|
-
// Current mapping position (inherited by unmarked lines)
|
|
80
|
-
let currentOrigLine = 1;
|
|
81
|
-
let currentOrigCol = 0;
|
|
82
|
-
let pendingMarker = null;
|
|
83
|
-
const mappings = [];
|
|
84
|
-
let outputLineNum = 0; // 0-indexed output line counter (after stripping markers)
|
|
85
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
86
|
-
const line = lines[lineIdx];
|
|
87
|
-
// Check for type annotation marker (@T:line:col)
|
|
88
|
-
TYPE_MARKER_REGEX.lastIndex = 0;
|
|
89
|
-
const typeMatch = TYPE_MARKER_REGEX.exec(line);
|
|
90
|
-
if (typeMatch) {
|
|
91
|
-
// This is a @T marker line - store the position for the next line
|
|
92
|
-
pendingMarker = {
|
|
93
|
-
line: parseInt(typeMatch[1], 10),
|
|
94
|
-
col: parseInt(typeMatch[2], 10),
|
|
95
|
-
};
|
|
96
|
-
// Don't output this line or create a mapping for it
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
// Check for line marker (@L:line)
|
|
100
|
-
LINE_MARKER_REGEX.lastIndex = 0;
|
|
101
|
-
const lineMatch = LINE_MARKER_REGEX.exec(line);
|
|
102
|
-
if (lineMatch) {
|
|
103
|
-
// This is a @L marker line - identity mapping (col 0)
|
|
104
|
-
pendingMarker = {
|
|
105
|
-
line: parseInt(lineMatch[1], 10),
|
|
106
|
-
col: 0,
|
|
107
|
-
};
|
|
108
|
-
// Don't output this line or create a mapping for it
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// This is a code line - apply pending marker if any
|
|
112
|
-
if (pendingMarker) {
|
|
113
|
-
currentOrigLine = pendingMarker.line;
|
|
114
|
-
currentOrigCol = pendingMarker.col;
|
|
115
|
-
pendingMarker = null;
|
|
116
|
-
}
|
|
117
|
-
outputLineNum++;
|
|
118
|
-
// Create mapping for this line
|
|
119
|
-
mappings.push({
|
|
120
|
-
generatedLine: outputLineNum,
|
|
121
|
-
generatedCol: 0,
|
|
122
|
-
originalLine: currentOrigLine,
|
|
123
|
-
originalCol: currentOrigCol,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
// Strip all markers from the code
|
|
127
|
-
const cleanCode = code.replace(ALL_MARKERS_REGEX, '');
|
|
128
|
-
// Build VLQ-encoded source map
|
|
129
|
-
const map = buildSourceMapFromMappings(mappings, fileName, originalSource, includeContent);
|
|
130
|
-
return { code: cleanCode, map };
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Build a source map from a list of position mappings.
|
|
134
|
-
*/
|
|
135
|
-
function buildSourceMapFromMappings(mappings, fileName, originalSource, includeContent) {
|
|
136
|
-
// Group mappings by generated line
|
|
137
|
-
const lineMap = new Map();
|
|
138
|
-
for (const m of mappings) {
|
|
139
|
-
if (!lineMap.has(m.generatedLine)) {
|
|
140
|
-
lineMap.set(m.generatedLine, []);
|
|
141
|
-
}
|
|
142
|
-
lineMap.get(m.generatedLine).push({
|
|
143
|
-
generatedCol: m.generatedCol,
|
|
144
|
-
originalLine: m.originalLine,
|
|
145
|
-
originalCol: m.originalCol,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
// Build VLQ-encoded mappings string
|
|
149
|
-
const maxLine = Math.max(...mappings.map(m => m.generatedLine), 0);
|
|
150
|
-
const mappingLines = [];
|
|
151
|
-
let prevGenCol = 0;
|
|
152
|
-
let prevOrigLine = 0;
|
|
153
|
-
let prevOrigCol = 0;
|
|
154
|
-
for (let line = 1; line <= maxLine; line++) {
|
|
155
|
-
const lineMappings = lineMap.get(line);
|
|
156
|
-
if (!lineMappings || lineMappings.length === 0) {
|
|
157
|
-
mappingLines.push('');
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
// Sort by generated column
|
|
161
|
-
lineMappings.sort((a, b) => a.generatedCol - b.generatedCol);
|
|
162
|
-
const segments = [];
|
|
163
|
-
prevGenCol = 0; // Reset for each line
|
|
164
|
-
for (const m of lineMappings) {
|
|
165
|
-
// VLQ encode: [genCol, sourceIdx=0, origLine, origCol]
|
|
166
|
-
const segment = vlqEncode([
|
|
167
|
-
m.generatedCol - prevGenCol,
|
|
168
|
-
0, // source index (we only have one source)
|
|
169
|
-
(m.originalLine - 1) - prevOrigLine, // 0-based, relative
|
|
170
|
-
m.originalCol - prevOrigCol,
|
|
171
|
-
]);
|
|
172
|
-
segments.push(segment);
|
|
173
|
-
prevGenCol = m.generatedCol;
|
|
174
|
-
prevOrigLine = m.originalLine - 1;
|
|
175
|
-
prevOrigCol = m.originalCol;
|
|
176
|
-
}
|
|
177
|
-
mappingLines.push(segments.join(','));
|
|
178
|
-
}
|
|
179
|
-
const map = {
|
|
180
|
-
version: 3,
|
|
181
|
-
file: fileName,
|
|
182
|
-
sources: [fileName],
|
|
183
|
-
names: [],
|
|
184
|
-
mappings: mappingLines.join(';'),
|
|
185
|
-
};
|
|
186
|
-
if (includeContent) {
|
|
187
|
-
map.sourcesContent = [originalSource];
|
|
188
|
-
}
|
|
189
|
-
return map;
|
|
190
|
-
}
|
|
191
|
-
// VLQ encoding for source maps
|
|
192
|
-
const VLQ_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
193
|
-
const VLQ_BASE = 32;
|
|
194
|
-
const VLQ_CONTINUATION_BIT = 32;
|
|
195
|
-
function vlqEncode(values) {
|
|
196
|
-
return values.map(vlqEncodeInteger).join('');
|
|
197
|
-
}
|
|
198
|
-
function vlqEncodeInteger(value) {
|
|
199
|
-
let result = '';
|
|
200
|
-
let vlq = value < 0 ? ((-value) << 1) | 1 : value << 1;
|
|
201
|
-
do {
|
|
202
|
-
let digit = vlq & 31;
|
|
203
|
-
vlq >>>= 5;
|
|
204
|
-
if (vlq > 0) {
|
|
205
|
-
digit |= VLQ_CONTINUATION_BIT;
|
|
206
|
-
}
|
|
207
|
-
result += VLQ_BASE64[digit];
|
|
208
|
-
} while (vlq > 0);
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import { TypicalCompiler } from '@elliots/typical-compiler';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
211
11
|
export class TypicalTransformer {
|
|
212
12
|
config;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
typeParsers = new Map(); // type -> { parser variable name, type node }
|
|
219
|
-
constructor(config, program, tsInstance) {
|
|
13
|
+
compiler;
|
|
14
|
+
projectHandle = null;
|
|
15
|
+
initPromise = null;
|
|
16
|
+
configFile;
|
|
17
|
+
constructor(config, configFile = 'tsconfig.json') {
|
|
220
18
|
this.config = config ?? loadConfig();
|
|
221
|
-
this.
|
|
222
|
-
this.
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Create a new TypeScript program with transformed source code.
|
|
226
|
-
* This is needed so typia can resolve types from our generated typia.createAssert<T>() calls.
|
|
227
|
-
*/
|
|
228
|
-
createTypiaProgram(fileName, transformedCode, languageVersion = this.ts.ScriptTarget.ES2020) {
|
|
229
|
-
// Create a new source file from the transformed code
|
|
230
|
-
const newSourceFile = this.ts.createSourceFile(fileName, transformedCode, languageVersion, true);
|
|
231
|
-
// Build map of all source files, replacing the transformed one
|
|
232
|
-
const compilerOptions = this.program.getCompilerOptions();
|
|
233
|
-
const originalSourceFiles = new Map();
|
|
234
|
-
for (const sf of this.program.getSourceFiles()) {
|
|
235
|
-
originalSourceFiles.set(sf.fileName, sf);
|
|
236
|
-
}
|
|
237
|
-
originalSourceFiles.set(fileName, newSourceFile);
|
|
238
|
-
// Create custom compiler host that serves our transformed file
|
|
239
|
-
const customHost = {
|
|
240
|
-
getSourceFile: (hostFileName, langVersion) => {
|
|
241
|
-
if (originalSourceFiles.has(hostFileName)) {
|
|
242
|
-
return originalSourceFiles.get(hostFileName);
|
|
243
|
-
}
|
|
244
|
-
return this.ts.createSourceFile(hostFileName, this.ts.sys.readFile(hostFileName) || "", langVersion, true);
|
|
245
|
-
},
|
|
246
|
-
getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
|
|
247
|
-
writeFile: () => { },
|
|
248
|
-
getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
|
|
249
|
-
getCanonicalFileName: (fn) => this.ts.sys.useCaseSensitiveFileNames ? fn : fn.toLowerCase(),
|
|
250
|
-
useCaseSensitiveFileNames: () => this.ts.sys.useCaseSensitiveFileNames,
|
|
251
|
-
getNewLine: () => this.ts.sys.newLine,
|
|
252
|
-
fileExists: (fn) => originalSourceFiles.has(fn) || this.ts.sys.fileExists(fn),
|
|
253
|
-
readFile: (fn) => this.ts.sys.readFile(fn),
|
|
254
|
-
};
|
|
255
|
-
// Create new program, passing oldProgram to reuse dependency context
|
|
256
|
-
const newProgram = this.ts.createProgram(Array.from(originalSourceFiles.keys()), compilerOptions, customHost, this.program);
|
|
257
|
-
// Get the bound source file from the new program (has proper symbol tables)
|
|
258
|
-
const boundSourceFile = newProgram.getSourceFile(fileName);
|
|
259
|
-
if (!boundSourceFile) {
|
|
260
|
-
throw new Error(`Failed to get bound source file: ${fileName}`);
|
|
261
|
-
}
|
|
262
|
-
return { newProgram, boundSourceFile };
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Write intermediate file for debugging purposes.
|
|
266
|
-
* Creates a .typical.ts file showing the code after typical's transformations
|
|
267
|
-
* but before typia processes it.
|
|
268
|
-
*/
|
|
269
|
-
writeIntermediateFile(fileName, code) {
|
|
270
|
-
if (!this.config.debug?.writeIntermediateFiles) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const compilerOptions = this.program.getCompilerOptions();
|
|
274
|
-
const outDir = compilerOptions.outDir || ".";
|
|
275
|
-
const rootDir = compilerOptions.rootDir || ".";
|
|
276
|
-
const relativePath = path.relative(rootDir, fileName);
|
|
277
|
-
const intermediateFileName = relativePath.replace(/\.(tsx?)$/, ".typical.$1");
|
|
278
|
-
const intermediateFilePath = path.join(outDir, intermediateFileName);
|
|
279
|
-
const dir = path.dirname(intermediateFilePath);
|
|
280
|
-
if (!fs.existsSync(dir)) {
|
|
281
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
282
|
-
}
|
|
283
|
-
fs.writeFileSync(intermediateFilePath, code);
|
|
284
|
-
console.log(`TYPICAL: Wrote intermediate file: ${intermediateFilePath}`);
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Format typia diagnostic errors into readable error messages.
|
|
288
|
-
*/
|
|
289
|
-
formatTypiaErrors(errors) {
|
|
290
|
-
return errors.map(d => {
|
|
291
|
-
const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
|
|
292
|
-
if (d.file && d.start !== undefined && d.length !== undefined) {
|
|
293
|
-
const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
|
|
294
|
-
// Extract the actual source code that caused the error
|
|
295
|
-
const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
|
|
296
|
-
// Truncate long snippets
|
|
297
|
-
const snippet = sourceSnippet.length > 100
|
|
298
|
-
? sourceSnippet.substring(0, 100) + '...'
|
|
299
|
-
: sourceSnippet;
|
|
300
|
-
// Format the error message - extract type issues from typia's verbose output
|
|
301
|
-
const formattedIssues = this.formatTypiaError(fullMessage);
|
|
302
|
-
return `${d.file.fileName}:${line + 1}:${character + 1}\n` +
|
|
303
|
-
` Code: ${snippet}\n` +
|
|
304
|
-
formattedIssues;
|
|
305
|
-
}
|
|
306
|
-
return this.formatTypiaError(fullMessage);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Check for untransformed typia calls and throw an error if found.
|
|
311
|
-
* This is a fallback in case typia silently fails without reporting a diagnostic.
|
|
312
|
-
*/
|
|
313
|
-
checkUntransformedTypiaCalls(code, fileName) {
|
|
314
|
-
const untransformedCalls = this.findUntransformedTypiaCalls(code);
|
|
315
|
-
if (untransformedCalls.length > 0) {
|
|
316
|
-
const failedTypes = untransformedCalls.map(c => c.type).filter((v, i, a) => a.indexOf(v) === i);
|
|
317
|
-
throw new Error(`TYPICAL: Failed to transform the following types (typia cannot process them):\n` +
|
|
318
|
-
failedTypes.map(t => ` - ${t}`).join('\n') +
|
|
319
|
-
`\n\nTo skip validation for these types, add to ignoreTypes in typical.json:\n` +
|
|
320
|
-
` "ignoreTypes": [${failedTypes.map(t => `"${t}"`).join(', ')}]` +
|
|
321
|
-
`\n\nFile: ${fileName}`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
createSourceFile(fileName, content) {
|
|
325
|
-
return this.ts.createSourceFile(fileName, content, this.ts.ScriptTarget.ES2020, true);
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Transform options for controlling source map generation.
|
|
329
|
-
*/
|
|
330
|
-
transform(sourceFile, mode, options = {}) {
|
|
331
|
-
const { sourceMap = false, skippedTypes = new Set() } = options;
|
|
332
|
-
if (typeof sourceFile === "string") {
|
|
333
|
-
const file = this.program.getSourceFile(sourceFile);
|
|
334
|
-
if (!file) {
|
|
335
|
-
throw new Error(`Source file not found in program: ${sourceFile}`);
|
|
336
|
-
}
|
|
337
|
-
sourceFile = file;
|
|
338
|
-
}
|
|
339
|
-
const fileName = sourceFile.fileName;
|
|
340
|
-
const originalSource = sourceFile.getFullText();
|
|
341
|
-
const printer = this.ts.createPrinter();
|
|
342
|
-
const includeContent = this.config.sourceMap?.includeContent ?? true;
|
|
343
|
-
// Phase 1: typical's own transformations (adds source map markers as comments)
|
|
344
|
-
const typicalTransformer = this.getTypicalOnlyTransformer(skippedTypes);
|
|
345
|
-
const phase1Result = this.ts.transform(sourceFile, [typicalTransformer]);
|
|
346
|
-
let transformedCode = printer.printFile(phase1Result.transformed[0]);
|
|
347
|
-
if (process.env.DEBUG) {
|
|
348
|
-
console.log("TYPICAL: After phase1 print (first 500):", transformedCode.substring(0, 500));
|
|
349
|
-
console.log("TYPICAL: Contains //@T:", transformedCode.includes("//@T:"));
|
|
350
|
-
}
|
|
351
|
-
phase1Result.dispose();
|
|
352
|
-
if (mode === "basic") {
|
|
353
|
-
// For basic mode, parse markers and build source map, then strip markers
|
|
354
|
-
if (sourceMap) {
|
|
355
|
-
const { code, map } = parseMarkersAndBuildSourceMap(transformedCode, fileName, originalSource, includeContent);
|
|
356
|
-
return { code, map };
|
|
357
|
-
}
|
|
358
|
-
// No source map requested - just strip markers
|
|
359
|
-
return {
|
|
360
|
-
code: transformedCode.replace(ALL_MARKERS_REGEX, ''),
|
|
361
|
-
map: null,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
// Phase 2: if code has typia calls, run typia transformer in its own context
|
|
365
|
-
// The markers survive through typia since they're comments
|
|
366
|
-
if (transformedCode.includes("typia.")) {
|
|
367
|
-
const result = this.applyTypiaTransform(sourceFile.fileName, transformedCode, printer);
|
|
368
|
-
if (typeof result === 'object' && 'retry' in result && result.retry) {
|
|
369
|
-
// Typia failed on a type - add to skipped and retry the whole transform
|
|
370
|
-
skippedTypes.add(result.failedType);
|
|
371
|
-
// Clear validator caches since we're retrying
|
|
372
|
-
this.typeValidators.clear();
|
|
373
|
-
this.typeStringifiers.clear();
|
|
374
|
-
this.typeParsers.clear();
|
|
375
|
-
return this.transform(sourceFile, mode, { sourceMap, skippedTypes });
|
|
376
|
-
}
|
|
377
|
-
transformedCode = result.code;
|
|
378
|
-
}
|
|
379
|
-
if (mode === "typia") {
|
|
380
|
-
// For typia mode, parse markers and build source map, then strip markers
|
|
381
|
-
if (sourceMap) {
|
|
382
|
-
const { code, map } = parseMarkersAndBuildSourceMap(transformedCode, fileName, originalSource, includeContent);
|
|
383
|
-
return { code, map };
|
|
384
|
-
}
|
|
385
|
-
// No source map requested - just strip markers
|
|
386
|
-
return {
|
|
387
|
-
code: transformedCode.replace(ALL_MARKERS_REGEX, ''),
|
|
388
|
-
map: null,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
// Mode "js" - first parse markers to build our source map, then transpile
|
|
392
|
-
let typicalMap = null;
|
|
393
|
-
if (sourceMap) {
|
|
394
|
-
const parsed = parseMarkersAndBuildSourceMap(transformedCode, fileName, originalSource, includeContent);
|
|
395
|
-
transformedCode = parsed.code;
|
|
396
|
-
typicalMap = parsed.map;
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
// Strip markers even without source map
|
|
400
|
-
transformedCode = transformedCode.replace(ALL_MARKERS_REGEX, '');
|
|
401
|
-
}
|
|
402
|
-
// Transpile to JavaScript with source map support
|
|
403
|
-
const compilerOptions = {
|
|
404
|
-
...this.program.getCompilerOptions(),
|
|
405
|
-
sourceMap: sourceMap,
|
|
406
|
-
inlineSourceMap: false,
|
|
407
|
-
inlineSources: false,
|
|
408
|
-
};
|
|
409
|
-
const compileResult = ts.transpileModule(transformedCode, {
|
|
410
|
-
compilerOptions,
|
|
411
|
-
fileName,
|
|
412
|
-
});
|
|
413
|
-
// Compose the two source maps: typical -> original AND js -> typical
|
|
414
|
-
if (sourceMap && typicalMap) {
|
|
415
|
-
let jsMap = null;
|
|
416
|
-
if (compileResult.sourceMapText) {
|
|
417
|
-
try {
|
|
418
|
-
jsMap = JSON.parse(compileResult.sourceMapText);
|
|
419
|
-
jsMap.sources = [fileName];
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
// Failed to parse, continue without
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// Compose maps: jsMap traces JS->TS, typicalMap traces TS->original
|
|
426
|
-
// Result traces JS->original
|
|
427
|
-
const composedMap = composeSourceMaps([typicalMap, jsMap], fileName);
|
|
428
|
-
if (composedMap && includeContent) {
|
|
429
|
-
composedMap.sourcesContent = [originalSource];
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
code: compileResult.outputText,
|
|
433
|
-
map: composedMap,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
code: compileResult.outputText,
|
|
438
|
-
map: null,
|
|
439
|
-
};
|
|
19
|
+
this.configFile = configFile;
|
|
20
|
+
this.compiler = new TypicalCompiler({ cwd: process.cwd() });
|
|
440
21
|
}
|
|
441
22
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
23
|
+
* Ensure the Go compiler is started and project is loaded.
|
|
24
|
+
* Uses lazy initialization - only starts on first transform.
|
|
444
25
|
*/
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
* Returns either the transformed code, or a retry signal with the failed type.
|
|
452
|
-
* Source map markers in the code are preserved through the typia transformation.
|
|
453
|
-
*/
|
|
454
|
-
applyTypiaTransform(fileName, code, printer) {
|
|
455
|
-
this.writeIntermediateFile(fileName, code);
|
|
456
|
-
if (process.env.DEBUG) {
|
|
457
|
-
console.log("TYPICAL: Before typia transform (first 500 chars):", code.substring(0, 500));
|
|
458
|
-
}
|
|
459
|
-
// Create a new program with the transformed source file so typia can resolve types
|
|
460
|
-
const { newProgram, boundSourceFile } = this.createTypiaProgram(fileName, code);
|
|
461
|
-
// Collect typia diagnostics to detect transformation failures
|
|
462
|
-
const diagnostics = [];
|
|
463
|
-
// Create typia transformer with the new program
|
|
464
|
-
const typiaTransformerFactory = typiaTransform(newProgram, {}, {
|
|
465
|
-
addDiagnostic(diag) {
|
|
466
|
-
diagnostics.push(diag);
|
|
467
|
-
if (process.env.DEBUG) {
|
|
468
|
-
console.warn("Typia diagnostic:", diag);
|
|
469
|
-
}
|
|
470
|
-
return diagnostics.length - 1;
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
// Run typia's transformer in its own ts.transform() call
|
|
474
|
-
const typiaResult = this.ts.transform(boundSourceFile, [typiaTransformerFactory]);
|
|
475
|
-
let typiaTransformed = typiaResult.transformed[0];
|
|
476
|
-
typiaResult.dispose();
|
|
477
|
-
if (process.env.DEBUG) {
|
|
478
|
-
const afterTypia = printer.printFile(typiaTransformed);
|
|
479
|
-
console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
|
|
26
|
+
async ensureInitialized() {
|
|
27
|
+
if (!this.initPromise) {
|
|
28
|
+
this.initPromise = (async () => {
|
|
29
|
+
await this.compiler.start();
|
|
30
|
+
this.projectHandle = await this.compiler.loadProject(this.configFile);
|
|
31
|
+
})();
|
|
480
32
|
}
|
|
481
|
-
|
|
482
|
-
const errors = diagnostics.filter(d => d.category === this.ts.DiagnosticCategory.Error);
|
|
483
|
-
if (errors.length > 0) {
|
|
484
|
-
// Check if any error is due to Window/globalThis intersection (DOM types)
|
|
485
|
-
for (const d of errors) {
|
|
486
|
-
const fullMessage = typeof d.messageText === 'string' ? d.messageText : d.messageText.messageText;
|
|
487
|
-
if (fullMessage.includes('Window & typeof globalThis') || fullMessage.includes('typeof globalThis')) {
|
|
488
|
-
// Find the validator that failed - look for the type in the error
|
|
489
|
-
// Error format: "Code: typia.createAssert<{ value: number; table: Table; }>()"
|
|
490
|
-
if (d.file && d.start !== undefined && d.length !== undefined) {
|
|
491
|
-
const sourceSnippet = d.file.text.substring(d.start, d.start + d.length);
|
|
492
|
-
// Extract the type from typia.createAssert<TYPE>()
|
|
493
|
-
const typeMatch = sourceSnippet.match(/typia\.\w+<([^>]+(?:<[^>]*>)*)>\(\)/);
|
|
494
|
-
if (typeMatch) {
|
|
495
|
-
const failedType = typeMatch[1].trim();
|
|
496
|
-
console.warn(`TYPICAL: Skipping validation for type due to Window/globalThis (typia cannot process DOM types): ${failedType.substring(0, 100)}...`);
|
|
497
|
-
// Add to ignored types and signal retry needed
|
|
498
|
-
return { retry: true, failedType };
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
// No retryable errors, throw the original error
|
|
504
|
-
const errorMessages = this.formatTypiaErrors(errors);
|
|
505
|
-
throw new Error(`TYPICAL: Typia transformation failed:\n\n${errorMessages.join('\n\n')}`);
|
|
506
|
-
}
|
|
507
|
-
// Hoist RegExp constructors to top-level constants for performance
|
|
508
|
-
if (this.config.hoistRegex !== false) {
|
|
509
|
-
// Need to run hoisting in a transform context
|
|
510
|
-
const hoistResult = this.ts.transform(typiaTransformed, [
|
|
511
|
-
(context) => (sf) => hoistRegexConstructors(sf, this.ts, context.factory)
|
|
512
|
-
]);
|
|
513
|
-
typiaTransformed = hoistResult.transformed[0];
|
|
514
|
-
hoistResult.dispose();
|
|
515
|
-
}
|
|
516
|
-
const finalCode = printer.printFile(typiaTransformed);
|
|
517
|
-
// Check for untransformed typia calls as a fallback
|
|
518
|
-
this.checkUntransformedTypiaCalls(finalCode, fileName);
|
|
519
|
-
// Source map markers (@T:line:col) are preserved through typia transformation
|
|
520
|
-
// and will be parsed later in the transform() method
|
|
521
|
-
return { code: finalCode };
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* Get a transformer that only applies typical's transformations (no typia).
|
|
525
|
-
* @param skippedTypes Set of type strings to skip validation for (used for retry after typia errors)
|
|
526
|
-
*/
|
|
527
|
-
getTypicalOnlyTransformer(skippedTypes = new Set()) {
|
|
528
|
-
return (context) => {
|
|
529
|
-
const factory = context.factory;
|
|
530
|
-
const typeChecker = this.program.getTypeChecker();
|
|
531
|
-
return (sourceFile) => {
|
|
532
|
-
// Check if this file should be transformed based on include/exclude patterns
|
|
533
|
-
if (!this.shouldTransformFile(sourceFile.fileName)) {
|
|
534
|
-
return sourceFile;
|
|
535
|
-
}
|
|
536
|
-
if (process.env.DEBUG) {
|
|
537
|
-
console.log("TYPICAL: processing ", sourceFile.fileName);
|
|
538
|
-
}
|
|
539
|
-
const transformContext = {
|
|
540
|
-
ts: this.ts,
|
|
541
|
-
factory,
|
|
542
|
-
context,
|
|
543
|
-
sourceFile,
|
|
544
|
-
};
|
|
545
|
-
return this.transformSourceFile(sourceFile, transformContext, typeChecker, skippedTypes);
|
|
546
|
-
};
|
|
547
|
-
};
|
|
33
|
+
await this.initPromise;
|
|
548
34
|
}
|
|
549
35
|
/**
|
|
550
|
-
*
|
|
551
|
-
* This is used by the TSC plugin where we need a single transformer factory.
|
|
36
|
+
* Transform a TypeScript file by adding runtime validation.
|
|
552
37
|
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
38
|
+
* @param fileName - Path to the TypeScript file
|
|
39
|
+
* @param mode - Output mode: 'ts' returns TypeScript, 'js' would transpile (not yet supported)
|
|
40
|
+
* @returns Transformed code with validation
|
|
555
41
|
*/
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const typeChecker = this.program.getTypeChecker();
|
|
560
|
-
return (sourceFile) => {
|
|
561
|
-
// Check if this file should be transformed based on include/exclude patterns
|
|
562
|
-
if (!this.shouldTransformFile(sourceFile.fileName)) {
|
|
563
|
-
return sourceFile;
|
|
564
|
-
}
|
|
565
|
-
if (process.env.DEBUG) {
|
|
566
|
-
console.log("TYPICAL: processing ", sourceFile.fileName);
|
|
567
|
-
}
|
|
568
|
-
const transformContext = {
|
|
569
|
-
ts: this.ts,
|
|
570
|
-
factory,
|
|
571
|
-
context,
|
|
572
|
-
sourceFile,
|
|
573
|
-
};
|
|
574
|
-
// Apply typical's transformations
|
|
575
|
-
let transformedSourceFile = this.transformSourceFile(sourceFile, transformContext, typeChecker);
|
|
576
|
-
if (!withTypia) {
|
|
577
|
-
return transformedSourceFile;
|
|
578
|
-
}
|
|
579
|
-
// Print the transformed code to check for typia calls
|
|
580
|
-
const printer = this.ts.createPrinter();
|
|
581
|
-
const transformedCode = printer.printFile(transformedSourceFile);
|
|
582
|
-
if (!transformedCode.includes("typia.")) {
|
|
583
|
-
return transformedSourceFile;
|
|
584
|
-
}
|
|
585
|
-
this.writeIntermediateFile(sourceFile.fileName, transformedCode);
|
|
586
|
-
if (process.env.DEBUG) {
|
|
587
|
-
console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
|
|
588
|
-
}
|
|
589
|
-
// Create a new program with the transformed source file so typia can resolve types
|
|
590
|
-
const { newProgram, boundSourceFile } = this.createTypiaProgram(sourceFile.fileName, transformedCode, sourceFile.languageVersion);
|
|
591
|
-
// Collect typia diagnostics to detect transformation failures
|
|
592
|
-
const diagnostics = [];
|
|
593
|
-
// Create typia transformer with the new program
|
|
594
|
-
const typiaTransformerFactory = typiaTransform(newProgram, {}, {
|
|
595
|
-
addDiagnostic(diag) {
|
|
596
|
-
diagnostics.push(diag);
|
|
597
|
-
if (process.env.DEBUG) {
|
|
598
|
-
console.warn("Typia diagnostic:", diag);
|
|
599
|
-
}
|
|
600
|
-
return diagnostics.length - 1;
|
|
601
|
-
},
|
|
602
|
-
});
|
|
603
|
-
// Apply typia's transformer to the bound source file
|
|
604
|
-
const typiaNodeTransformer = typiaTransformerFactory(context);
|
|
605
|
-
transformedSourceFile = typiaNodeTransformer(boundSourceFile);
|
|
606
|
-
if (process.env.DEBUG) {
|
|
607
|
-
const afterTypia = printer.printFile(transformedSourceFile);
|
|
608
|
-
console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
|
|
609
|
-
}
|
|
610
|
-
// Check for typia errors via diagnostics
|
|
611
|
-
const errors = diagnostics.filter(d => d.category === this.ts.DiagnosticCategory.Error);
|
|
612
|
-
if (errors.length > 0) {
|
|
613
|
-
const errorMessages = this.formatTypiaErrors(errors);
|
|
614
|
-
throw new Error(`TYPICAL: Typia transformation failed:\n\n${errorMessages.join('\n\n')}`);
|
|
615
|
-
}
|
|
616
|
-
// Hoist RegExp constructors to top-level constants for performance
|
|
617
|
-
if (this.config.hoistRegex !== false) {
|
|
618
|
-
transformedSourceFile = hoistRegexConstructors(transformedSourceFile, this.ts, factory);
|
|
619
|
-
}
|
|
620
|
-
// Check for untransformed typia calls as a fallback
|
|
621
|
-
const finalCode = printer.printFile(transformedSourceFile);
|
|
622
|
-
this.checkUntransformedTypiaCalls(finalCode, sourceFile.fileName);
|
|
623
|
-
return transformedSourceFile;
|
|
624
|
-
};
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Transform JSON.stringify or JSON.parse calls to use typia's validated versions.
|
|
629
|
-
* Returns the transformed node if applicable, or undefined to indicate no transformation.
|
|
630
|
-
*/
|
|
631
|
-
transformJSONCall(node, ctx, typeChecker, shouldSkipType) {
|
|
632
|
-
const { ts, factory } = ctx;
|
|
633
|
-
const propertyAccess = node.expression;
|
|
634
|
-
if (propertyAccess.name.text === "stringify") {
|
|
635
|
-
// For stringify, we need to infer the type from the argument
|
|
636
|
-
// First check if the argument type is 'any' - if so, skip transformation
|
|
637
|
-
if (node.arguments.length > 0) {
|
|
638
|
-
const arg = node.arguments[0];
|
|
639
|
-
const argType = typeChecker.getTypeAtLocation(arg);
|
|
640
|
-
if (this.isAnyOrUnknownTypeFlags(argType)) {
|
|
641
|
-
return undefined; // Don't transform JSON.stringify for any/unknown types
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
if (this.config.reusableValidators) {
|
|
645
|
-
// Infer type from argument
|
|
646
|
-
const arg = node.arguments[0];
|
|
647
|
-
const { typeText, typeNode } = this.inferStringifyType(arg, typeChecker, ctx);
|
|
648
|
-
const stringifierName = this.getOrCreateStringifier(typeText, typeNode);
|
|
649
|
-
return factory.createCallExpression(factory.createIdentifier(stringifierName), undefined, node.arguments);
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
// Use inline typia.json.stringify
|
|
653
|
-
return factory.updateCallExpression(node, factory.createPropertyAccessExpression(factory.createPropertyAccessExpression(factory.createIdentifier("typia"), "json"), "stringify"), node.typeArguments, node.arguments);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
else if (propertyAccess.name.text === "parse") {
|
|
657
|
-
// For JSON.parse, we need to infer the expected type from context
|
|
658
|
-
// Check if this is part of a variable declaration or type assertion
|
|
659
|
-
let targetType;
|
|
660
|
-
// Look for type annotations in parent nodes
|
|
661
|
-
let parent = node.parent;
|
|
662
|
-
while (parent) {
|
|
663
|
-
if (ts.isVariableDeclaration(parent) && parent.type) {
|
|
664
|
-
targetType = parent.type;
|
|
665
|
-
break;
|
|
666
|
-
}
|
|
667
|
-
else if (ts.isAsExpression(parent)) {
|
|
668
|
-
targetType = parent.type;
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
else if (ts.isReturnStatement(parent)) {
|
|
672
|
-
// Look for function return type
|
|
673
|
-
let funcParent = parent.parent;
|
|
674
|
-
while (funcParent) {
|
|
675
|
-
if ((ts.isFunctionDeclaration(funcParent) ||
|
|
676
|
-
ts.isArrowFunction(funcParent) ||
|
|
677
|
-
ts.isMethodDeclaration(funcParent)) &&
|
|
678
|
-
funcParent.type) {
|
|
679
|
-
targetType = funcParent.type;
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
funcParent = funcParent.parent;
|
|
683
|
-
}
|
|
684
|
-
break;
|
|
685
|
-
}
|
|
686
|
-
else if (ts.isArrowFunction(parent) && parent.type) {
|
|
687
|
-
// Arrow function with expression body (not block)
|
|
688
|
-
// e.g., (s: string): User => JSON.parse(s)
|
|
689
|
-
targetType = parent.type;
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
parent = parent.parent;
|
|
693
|
-
}
|
|
694
|
-
if (targetType && this.isAnyOrUnknownType(targetType)) {
|
|
695
|
-
// Don't transform JSON.parse for any/unknown types
|
|
696
|
-
return undefined;
|
|
697
|
-
}
|
|
698
|
-
// If we can't determine the target type and there's no explicit type argument,
|
|
699
|
-
// don't transform - we can't validate against an unknown type
|
|
700
|
-
if (!targetType && !node.typeArguments) {
|
|
701
|
-
return undefined;
|
|
702
|
-
}
|
|
703
|
-
if (this.config.reusableValidators && targetType) {
|
|
704
|
-
// Use reusable parser - use typeNode text to preserve local aliases
|
|
705
|
-
const typeText = this.getTypeKey(targetType, typeChecker);
|
|
706
|
-
// Skip types that failed in typia (retry mechanism)
|
|
707
|
-
if (shouldSkipType(typeText)) {
|
|
708
|
-
if (process.env.DEBUG) {
|
|
709
|
-
console.log(`TYPICAL: Skipping previously failed type for JSON.parse: ${typeText}`);
|
|
710
|
-
}
|
|
711
|
-
return undefined;
|
|
712
|
-
}
|
|
713
|
-
const parserName = this.getOrCreateParser(typeText, targetType);
|
|
714
|
-
return factory.createCallExpression(factory.createIdentifier(parserName), undefined, node.arguments);
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
// Use inline typia.json.assertParse
|
|
718
|
-
const typeArguments = targetType
|
|
719
|
-
? [targetType]
|
|
720
|
-
: node.typeArguments;
|
|
721
|
-
return factory.updateCallExpression(node, factory.createPropertyAccessExpression(factory.createPropertyAccessExpression(factory.createIdentifier("typia"), "json"), "assertParse"), typeArguments, node.arguments);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return undefined;
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Check if a type should be skipped (failed in typia on previous attempt).
|
|
728
|
-
*/
|
|
729
|
-
shouldSkipType(typeText, skippedTypes) {
|
|
730
|
-
if (skippedTypes.size === 0)
|
|
731
|
-
return false;
|
|
732
|
-
// Normalize: remove all whitespace and semicolons for comparison
|
|
733
|
-
const normalize = (s) => s.replace(/[\s;]+/g, '').toLowerCase();
|
|
734
|
-
const normalized = normalize(typeText);
|
|
735
|
-
for (const skipped of skippedTypes) {
|
|
736
|
-
const skippedNormalized = normalize(skipped);
|
|
737
|
-
if (normalized === skippedNormalized || normalized.includes(skippedNormalized) || skippedNormalized.includes(normalized)) {
|
|
738
|
-
if (process.env.DEBUG) {
|
|
739
|
-
console.log(`TYPICAL: Matched skipped type: "${typeText.substring(0, 50)}..." matches "${skipped.substring(0, 50)}..."`);
|
|
740
|
-
}
|
|
741
|
-
return true;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Create an AST visitor function for transforming a source file.
|
|
748
|
-
* The visitor handles JSON calls, type casts, and function declarations.
|
|
749
|
-
*/
|
|
750
|
-
createVisitor(ctx, typeChecker, skippedTypes, state) {
|
|
751
|
-
const { ts } = ctx;
|
|
752
|
-
const shouldSkipType = (typeText) => this.shouldSkipType(typeText, skippedTypes);
|
|
753
|
-
// Forward declaration for mutual recursion
|
|
754
|
-
let transformFunction;
|
|
755
|
-
const visit = (node) => {
|
|
756
|
-
// Transform JSON calls first (before they get wrapped in functions)
|
|
757
|
-
if (ts.isCallExpression(node) &&
|
|
758
|
-
ts.isPropertyAccessExpression(node.expression)) {
|
|
759
|
-
const propertyAccess = node.expression;
|
|
760
|
-
if (ts.isIdentifier(propertyAccess.expression) &&
|
|
761
|
-
propertyAccess.expression.text === "JSON") {
|
|
762
|
-
const transformed = this.transformJSONCall(node, ctx, typeChecker, shouldSkipType);
|
|
763
|
-
if (transformed) {
|
|
764
|
-
state.needsTypiaImport = true;
|
|
765
|
-
return transformed;
|
|
766
|
-
}
|
|
767
|
-
return node;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
// Transform type assertions (as expressions) when validateCasts is enabled
|
|
771
|
-
// e.g., `obj as User` becomes `__typical_assert_N(obj)`
|
|
772
|
-
if (this.config.validateCasts && ts.isAsExpression(node)) {
|
|
773
|
-
const targetType = node.type;
|
|
774
|
-
// Skip 'as any' and 'as unknown' casts - these are intentional escapes
|
|
775
|
-
if (this.isAnyOrUnknownType(targetType)) {
|
|
776
|
-
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
777
|
-
}
|
|
778
|
-
// Skip primitive types - no runtime validation needed
|
|
779
|
-
if (targetType.kind === ts.SyntaxKind.StringKeyword ||
|
|
780
|
-
targetType.kind === ts.SyntaxKind.NumberKeyword ||
|
|
781
|
-
targetType.kind === ts.SyntaxKind.BooleanKeyword) {
|
|
782
|
-
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
783
|
-
}
|
|
784
|
-
// Skip types matching ignoreTypes patterns (including classes extending DOM types)
|
|
785
|
-
const typeText = this.getTypeKey(targetType, typeChecker);
|
|
786
|
-
const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
|
|
787
|
-
if (this.isIgnoredType(typeText, typeChecker, targetTypeObj)) {
|
|
788
|
-
if (process.env.DEBUG) {
|
|
789
|
-
console.log(`TYPICAL: Skipping ignored type for cast: ${typeText}`);
|
|
790
|
-
}
|
|
791
|
-
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
792
|
-
}
|
|
793
|
-
// Skip types that failed in typia (retry mechanism)
|
|
794
|
-
if (shouldSkipType(typeText)) {
|
|
795
|
-
if (process.env.DEBUG) {
|
|
796
|
-
console.log(`TYPICAL: Skipping previously failed type for cast: ${typeText}`);
|
|
797
|
-
}
|
|
798
|
-
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
799
|
-
}
|
|
800
|
-
state.needsTypiaImport = true;
|
|
801
|
-
// Visit the expression first to transform any nested casts
|
|
802
|
-
const visitedExpression = ctx.ts.visitNode(node.expression, visit);
|
|
803
|
-
if (this.config.reusableValidators) {
|
|
804
|
-
// Use typeNode text to preserve local aliases
|
|
805
|
-
const typeText = this.getTypeKey(targetType, typeChecker);
|
|
806
|
-
const validatorName = this.getOrCreateValidator(typeText, targetType);
|
|
807
|
-
// Replace `expr as Type` with `__typical_assert_N(expr)`
|
|
808
|
-
return ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [visitedExpression]);
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
// Inline validator: typia.assert<Type>(expr)
|
|
812
|
-
return ctx.factory.createCallExpression(ctx.factory.createPropertyAccessExpression(ctx.factory.createIdentifier("typia"), "assert"), [targetType], [visitedExpression]);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
// Transform function declarations
|
|
816
|
-
if (ts.isFunctionDeclaration(node)) {
|
|
817
|
-
state.needsTypiaImport = true;
|
|
818
|
-
return transformFunction(node);
|
|
819
|
-
}
|
|
820
|
-
// Transform arrow functions
|
|
821
|
-
if (ts.isArrowFunction(node)) {
|
|
822
|
-
state.needsTypiaImport = true;
|
|
823
|
-
return transformFunction(node);
|
|
824
|
-
}
|
|
825
|
-
// Transform method declarations
|
|
826
|
-
if (ts.isMethodDeclaration(node)) {
|
|
827
|
-
state.needsTypiaImport = true;
|
|
828
|
-
return transformFunction(node);
|
|
829
|
-
}
|
|
830
|
-
return ctx.ts.visitEachChild(node, visit, ctx.context);
|
|
831
|
-
};
|
|
832
|
-
transformFunction = (func) => {
|
|
833
|
-
const body = func.body;
|
|
834
|
-
// For arrow functions with expression bodies (not blocks),
|
|
835
|
-
// still visit the expression to transform JSON calls etc.
|
|
836
|
-
// Also handle Promise return types with .then() validation
|
|
837
|
-
if (body && !ts.isBlock(body) && ts.isArrowFunction(func)) {
|
|
838
|
-
let visitedBody = ctx.ts.visitNode(body, visit);
|
|
839
|
-
// Check if this is a non-async function with Promise return type
|
|
840
|
-
let returnType = func.type;
|
|
841
|
-
let returnTypeForString;
|
|
842
|
-
if (returnType) {
|
|
843
|
-
returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
// Try to infer the return type from the signature
|
|
847
|
-
try {
|
|
848
|
-
const signature = typeChecker.getSignatureFromDeclaration(func);
|
|
849
|
-
if (signature) {
|
|
850
|
-
const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
|
|
851
|
-
returnType = typeChecker.typeToTypeNode(inferredReturnType, func, TYPE_NODE_FLAGS);
|
|
852
|
-
returnTypeForString = inferredReturnType;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
catch {
|
|
856
|
-
// Skip inference
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
// Check for Promise<T> return type
|
|
860
|
-
if (returnType && returnTypeForString) {
|
|
861
|
-
const promiseSymbol = returnTypeForString.getSymbol();
|
|
862
|
-
if (promiseSymbol && promiseSymbol.getName() === "Promise") {
|
|
863
|
-
const typeArgs = returnTypeForString.typeArguments;
|
|
864
|
-
if (typeArgs && typeArgs.length > 0) {
|
|
865
|
-
const innerType = typeArgs[0];
|
|
866
|
-
let innerTypeNode;
|
|
867
|
-
if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
|
|
868
|
-
innerTypeNode = returnType.typeArguments[0];
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
innerTypeNode = typeChecker.typeToTypeNode(innerType, func, TYPE_NODE_FLAGS);
|
|
872
|
-
}
|
|
873
|
-
// Only add validation if validateFunctions is enabled
|
|
874
|
-
if (this.config.validateFunctions !== false && innerTypeNode && !this.isAnyOrUnknownType(innerTypeNode)) {
|
|
875
|
-
const innerTypeText = this.getTypeKey(innerTypeNode, typeChecker, innerType);
|
|
876
|
-
if (!this.isIgnoredType(innerTypeText, typeChecker, innerType) && !shouldSkipType(innerTypeText)) {
|
|
877
|
-
// Wrap expression with .then(validator)
|
|
878
|
-
const validatorName = this.config.reusableValidators
|
|
879
|
-
? this.getOrCreateValidator(innerTypeText, innerTypeNode)
|
|
880
|
-
: null;
|
|
881
|
-
if (validatorName) {
|
|
882
|
-
state.needsTypiaImport = true;
|
|
883
|
-
visitedBody = ctx.factory.createCallExpression(ctx.factory.createPropertyAccessExpression(visitedBody, ctx.factory.createIdentifier("then")), undefined, [ctx.factory.createIdentifier(validatorName)]);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
if (visitedBody !== body) {
|
|
891
|
-
return ctx.factory.updateArrowFunction(func, func.modifiers, func.typeParameters, func.parameters, func.type, func.equalsGreaterThanToken, visitedBody);
|
|
892
|
-
}
|
|
893
|
-
return func;
|
|
894
|
-
}
|
|
895
|
-
if (!body || !ts.isBlock(body))
|
|
896
|
-
return func;
|
|
897
|
-
// Track validated variables (params and consts with type annotations)
|
|
898
|
-
const validatedVariables = new Map();
|
|
899
|
-
// Add parameter validation (only if validateFunctions is enabled)
|
|
900
|
-
const validationStatements = [];
|
|
901
|
-
// Skip parameter validation if validateFunctions is disabled
|
|
902
|
-
const shouldValidateFunctions = this.config.validateFunctions !== false;
|
|
903
|
-
func.parameters.forEach((param) => {
|
|
904
|
-
if (shouldValidateFunctions && param.type) {
|
|
905
|
-
// Skip 'any' and 'unknown' types - no point validating them
|
|
906
|
-
if (this.isAnyOrUnknownType(param.type)) {
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
// Skip types matching ignoreTypes patterns (including classes extending DOM types)
|
|
910
|
-
const typeText = this.getTypeKey(param.type, typeChecker);
|
|
911
|
-
const paramType = typeChecker.getTypeFromTypeNode(param.type);
|
|
912
|
-
// Skip type parameters (generics) - can't be validated at runtime
|
|
913
|
-
if (this.isAnyOrUnknownTypeFlags(paramType)) {
|
|
914
|
-
if (process.env.DEBUG) {
|
|
915
|
-
console.log(`TYPICAL: Skipping type parameter/any for parameter: ${typeText}`);
|
|
916
|
-
}
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (process.env.DEBUG) {
|
|
920
|
-
console.log(`TYPICAL: Processing parameter type: ${typeText}`);
|
|
921
|
-
}
|
|
922
|
-
if (this.isIgnoredType(typeText, typeChecker, paramType)) {
|
|
923
|
-
if (process.env.DEBUG) {
|
|
924
|
-
console.log(`TYPICAL: Skipping ignored type for parameter: ${typeText}`);
|
|
925
|
-
}
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
// Skip types that failed in typia (retry mechanism)
|
|
929
|
-
if (shouldSkipType(typeText)) {
|
|
930
|
-
if (process.env.DEBUG) {
|
|
931
|
-
console.log(`TYPICAL: Skipping previously failed type for parameter: ${typeText}`);
|
|
932
|
-
}
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
const paramName = ts.isIdentifier(param.name)
|
|
936
|
-
? param.name.text
|
|
937
|
-
: "param";
|
|
938
|
-
const paramIdentifier = ctx.factory.createIdentifier(paramName);
|
|
939
|
-
// Track this parameter as validated for flow analysis
|
|
940
|
-
validatedVariables.set(paramName, paramType);
|
|
941
|
-
if (this.config.reusableValidators) {
|
|
942
|
-
// Use reusable validators - use typeNode text to preserve local aliases
|
|
943
|
-
const validatorName = this.getOrCreateValidator(typeText, param.type);
|
|
944
|
-
const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [paramIdentifier]);
|
|
945
|
-
let assertCall = ctx.factory.createExpressionStatement(validatorCall);
|
|
946
|
-
// Add source map marker pointing to the parameter's type annotation
|
|
947
|
-
assertCall = addSourceMapMarker(assertCall, ctx.sourceFile, param.type);
|
|
948
|
-
validationStatements.push(assertCall);
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
// Use inline typia.assert calls
|
|
952
|
-
const typiaIdentifier = ctx.factory.createIdentifier("typia");
|
|
953
|
-
const assertIdentifier = ctx.factory.createIdentifier("assert");
|
|
954
|
-
const propertyAccess = ctx.factory.createPropertyAccessExpression(typiaIdentifier, assertIdentifier);
|
|
955
|
-
const callExpression = ctx.factory.createCallExpression(propertyAccess, [param.type], [paramIdentifier]);
|
|
956
|
-
let assertCall = ctx.factory.createExpressionStatement(callExpression);
|
|
957
|
-
// Add source map marker pointing to the parameter's type annotation
|
|
958
|
-
assertCall = addSourceMapMarker(assertCall, ctx.sourceFile, param.type);
|
|
959
|
-
validationStatements.push(assertCall);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
});
|
|
963
|
-
// First visit all child nodes (including JSON calls) before adding validation
|
|
964
|
-
const visitedBody = ctx.ts.visitNode(body, visit);
|
|
965
|
-
// Also track const declarations with type annotations as validated
|
|
966
|
-
// (the assignment will be validated, and const can't be reassigned)
|
|
967
|
-
const collectConstDeclarations = (node) => {
|
|
968
|
-
if (ts.isVariableStatement(node)) {
|
|
969
|
-
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
970
|
-
if (isConst) {
|
|
971
|
-
for (const decl of node.declarationList.declarations) {
|
|
972
|
-
if (decl.type && ts.isIdentifier(decl.name)) {
|
|
973
|
-
// Skip any/unknown types
|
|
974
|
-
if (!this.isAnyOrUnknownType(decl.type)) {
|
|
975
|
-
const constType = typeChecker.getTypeFromTypeNode(decl.type);
|
|
976
|
-
validatedVariables.set(decl.name.text, constType);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
ts.forEachChild(node, collectConstDeclarations);
|
|
983
|
-
};
|
|
984
|
-
collectConstDeclarations(visitedBody);
|
|
985
|
-
// Transform return statements - use explicit type or infer from type checker
|
|
986
|
-
let transformedStatements = visitedBody.statements;
|
|
987
|
-
let returnType = func.type;
|
|
988
|
-
// Check if this is an async function
|
|
989
|
-
const isAsync = func.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.AsyncKeyword);
|
|
990
|
-
// If no explicit return type, try to infer it from the type checker
|
|
991
|
-
let returnTypeForString;
|
|
992
|
-
if (!returnType) {
|
|
993
|
-
try {
|
|
994
|
-
const signature = typeChecker.getSignatureFromDeclaration(func);
|
|
995
|
-
if (signature) {
|
|
996
|
-
const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
|
|
997
|
-
returnType = typeChecker.typeToTypeNode(inferredReturnType, func, TYPE_NODE_FLAGS);
|
|
998
|
-
returnTypeForString = inferredReturnType;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
// Could not infer signature (e.g., untyped arrow function callback)
|
|
1003
|
-
// Skip return type validation for this function
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
else {
|
|
1007
|
-
// For explicit return types, get the Type from the TypeNode
|
|
1008
|
-
returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
|
|
1009
|
-
}
|
|
1010
|
-
// Handle Promise return types
|
|
1011
|
-
// Track whether this is a non-async function returning Promise (needs .then() wrapper)
|
|
1012
|
-
let isNonAsyncPromiseReturn = false;
|
|
1013
|
-
if (returnType && returnTypeForString) {
|
|
1014
|
-
const promiseSymbol = returnTypeForString.getSymbol();
|
|
1015
|
-
if (promiseSymbol && promiseSymbol.getName() === "Promise") {
|
|
1016
|
-
// Unwrap Promise<T> to get T for validation
|
|
1017
|
-
const typeArgs = returnTypeForString.typeArguments;
|
|
1018
|
-
if (typeArgs && typeArgs.length > 0) {
|
|
1019
|
-
returnTypeForString = typeArgs[0];
|
|
1020
|
-
// Also update the TypeNode to match
|
|
1021
|
-
if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
|
|
1022
|
-
returnType = returnType.typeArguments[0];
|
|
1023
|
-
}
|
|
1024
|
-
else {
|
|
1025
|
-
// Create a new type node from the unwrapped type
|
|
1026
|
-
returnType = typeChecker.typeToTypeNode(returnTypeForString, func, TYPE_NODE_FLAGS);
|
|
1027
|
-
}
|
|
1028
|
-
if (!isAsync) {
|
|
1029
|
-
// For non-async functions returning Promise, we'll use .then(validator)
|
|
1030
|
-
isNonAsyncPromiseReturn = true;
|
|
1031
|
-
if (process.env.DEBUG) {
|
|
1032
|
-
console.log(`TYPICAL: Non-async Promise return type - will use .then() for validation`);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
// Skip 'any' and 'unknown' return types - no point validating them
|
|
1039
|
-
// Also skip types matching ignoreTypes patterns (including classes extending DOM types)
|
|
1040
|
-
// Also skip types containing type parameters or constructor types
|
|
1041
|
-
const returnTypeText = returnType && returnTypeForString
|
|
1042
|
-
? this.getTypeKey(returnType, typeChecker, returnTypeForString)
|
|
1043
|
-
: null;
|
|
1044
|
-
if (process.env.DEBUG && returnTypeText) {
|
|
1045
|
-
console.log(`TYPICAL: Checking return type: "${returnTypeText}" (isAsync: ${isAsync})`);
|
|
1046
|
-
}
|
|
1047
|
-
// Skip if return type contains type parameters, constructor types, or is otherwise unvalidatable
|
|
1048
|
-
const shouldSkipReturnType = returnTypeForString && this.containsUnvalidatableType(returnTypeForString);
|
|
1049
|
-
if (shouldSkipReturnType && process.env.DEBUG) {
|
|
1050
|
-
console.log(`TYPICAL: Skipping unvalidatable return type: ${returnTypeText}`);
|
|
1051
|
-
}
|
|
1052
|
-
const isIgnoredReturnType = returnTypeText && this.isIgnoredType(returnTypeText, typeChecker, returnTypeForString);
|
|
1053
|
-
if (isIgnoredReturnType && process.env.DEBUG) {
|
|
1054
|
-
console.log(`TYPICAL: Skipping ignored type for return: ${returnTypeText}`);
|
|
1055
|
-
}
|
|
1056
|
-
const isSkippedReturnType = returnTypeText && shouldSkipType(returnTypeText);
|
|
1057
|
-
if (isSkippedReturnType && process.env.DEBUG) {
|
|
1058
|
-
console.log(`TYPICAL: Skipping previously failed type for return: ${returnTypeText}`);
|
|
1059
|
-
}
|
|
1060
|
-
if (shouldValidateFunctions && returnType && returnTypeForString && !this.isAnyOrUnknownType(returnType) && !isIgnoredReturnType && !shouldSkipReturnType && !isSkippedReturnType) {
|
|
1061
|
-
const returnTransformer = (node) => {
|
|
1062
|
-
// Don't recurse into nested functions - they have their own return types
|
|
1063
|
-
if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) ||
|
|
1064
|
-
ts.isFunctionExpression(node) || ts.isMethodDeclaration(node)) {
|
|
1065
|
-
return node;
|
|
1066
|
-
}
|
|
1067
|
-
if (ts.isReturnStatement(node) && node.expression) {
|
|
1068
|
-
// Skip return validation if the expression already contains a __typical _parse_* call
|
|
1069
|
-
// since typia.assertParse already validates the parsed data
|
|
1070
|
-
const containsTypicalParse = (n) => {
|
|
1071
|
-
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
1072
|
-
const name = n.expression.text;
|
|
1073
|
-
if (name.startsWith("__typical" + "_parse_")) {
|
|
1074
|
-
return true;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
return ts.forEachChild(n, containsTypicalParse) || false;
|
|
1078
|
-
};
|
|
1079
|
-
if (containsTypicalParse(node.expression)) {
|
|
1080
|
-
return node; // Already validated by parse, skip return validation
|
|
1081
|
-
}
|
|
1082
|
-
// Flow analysis: Skip return validation if returning a validated variable
|
|
1083
|
-
// (or property of one) that hasn't been tainted
|
|
1084
|
-
const rootVar = this.getRootIdentifier(node.expression);
|
|
1085
|
-
if (rootVar && validatedVariables.has(rootVar)) {
|
|
1086
|
-
// Check if the variable has been tainted (mutated, passed to function, etc.)
|
|
1087
|
-
if (!this.isTainted(rootVar, visitedBody)) {
|
|
1088
|
-
// Return expression is rooted at a validated, untainted variable
|
|
1089
|
-
// For direct returns (identifier) or property access, we can skip validation
|
|
1090
|
-
if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
|
|
1091
|
-
return node; // Skip validation - already validated and untainted
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
// For non-async functions returning Promise, use .then(validator)
|
|
1096
|
-
// For async functions, await the expression before validating
|
|
1097
|
-
if (isNonAsyncPromiseReturn) {
|
|
1098
|
-
// return expr.then(validator)
|
|
1099
|
-
const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
|
|
1100
|
-
const validatorName = this.config.reusableValidators
|
|
1101
|
-
? this.getOrCreateValidator(returnTypeText, returnType)
|
|
1102
|
-
: null;
|
|
1103
|
-
// Create the validator reference (either reusable or inline)
|
|
1104
|
-
let validatorExpr;
|
|
1105
|
-
if (validatorName) {
|
|
1106
|
-
validatorExpr = ctx.factory.createIdentifier(validatorName);
|
|
1107
|
-
}
|
|
1108
|
-
else {
|
|
1109
|
-
// Inline: typia.assert<T>
|
|
1110
|
-
validatorExpr = ctx.factory.createPropertyAccessExpression(ctx.factory.createIdentifier("typia"), ctx.factory.createIdentifier("assert"));
|
|
1111
|
-
// Note: For inline mode, we'd need to create a wrapper arrow function
|
|
1112
|
-
// to pass the type argument. For simplicity, just use the property access
|
|
1113
|
-
// and let typia handle it (though this won't work without type args)
|
|
1114
|
-
// In practice, reusableValidators should be true for this to work well
|
|
1115
|
-
}
|
|
1116
|
-
// expr.then(validator)
|
|
1117
|
-
const thenCall = ctx.factory.createCallExpression(ctx.factory.createPropertyAccessExpression(node.expression, ctx.factory.createIdentifier("then")), undefined, [validatorExpr]);
|
|
1118
|
-
let updatedReturn = ctx.factory.updateReturnStatement(node, thenCall);
|
|
1119
|
-
// Add source map marker pointing to the return type annotation
|
|
1120
|
-
if (returnType && returnType.pos >= 0) {
|
|
1121
|
-
updatedReturn = addSourceMapMarker(updatedReturn, ctx.sourceFile, returnType);
|
|
1122
|
-
}
|
|
1123
|
-
return updatedReturn;
|
|
1124
|
-
}
|
|
1125
|
-
// For async functions, we need to await the expression before validating
|
|
1126
|
-
// because the return expression might be a Promise
|
|
1127
|
-
let expressionToValidate = node.expression;
|
|
1128
|
-
if (isAsync) {
|
|
1129
|
-
// Check if the expression is already an await expression
|
|
1130
|
-
const isAlreadyAwaited = ts.isAwaitExpression(node.expression);
|
|
1131
|
-
if (!isAlreadyAwaited) {
|
|
1132
|
-
// Wrap in await: return validate(await expr)
|
|
1133
|
-
expressionToValidate = ctx.factory.createAwaitExpression(node.expression);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
if (this.config.reusableValidators) {
|
|
1137
|
-
// Use reusable validators - use typeNode text to preserve local aliases
|
|
1138
|
-
// Pass returnTypeForString for synthesized nodes (inferred return types)
|
|
1139
|
-
const returnTypeText = this.getTypeKey(returnType, typeChecker, returnTypeForString);
|
|
1140
|
-
const validatorName = this.getOrCreateValidator(returnTypeText, returnType);
|
|
1141
|
-
const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [expressionToValidate]);
|
|
1142
|
-
let updatedReturn = ctx.factory.updateReturnStatement(node, validatorCall);
|
|
1143
|
-
// Add source map marker pointing to the return type annotation
|
|
1144
|
-
if (returnType && returnType.pos >= 0) {
|
|
1145
|
-
updatedReturn = addSourceMapMarker(updatedReturn, ctx.sourceFile, returnType);
|
|
1146
|
-
}
|
|
1147
|
-
return updatedReturn;
|
|
1148
|
-
}
|
|
1149
|
-
else {
|
|
1150
|
-
// Use inline typia.assert calls
|
|
1151
|
-
const typiaIdentifier = ctx.factory.createIdentifier("typia");
|
|
1152
|
-
const assertIdentifier = ctx.factory.createIdentifier("assert");
|
|
1153
|
-
const propertyAccess = ctx.factory.createPropertyAccessExpression(typiaIdentifier, assertIdentifier);
|
|
1154
|
-
const callExpression = ctx.factory.createCallExpression(propertyAccess, [returnType], [expressionToValidate]);
|
|
1155
|
-
let updatedReturn = ctx.factory.updateReturnStatement(node, callExpression);
|
|
1156
|
-
// Add source map marker pointing to the return type annotation
|
|
1157
|
-
if (returnType && returnType.pos >= 0) {
|
|
1158
|
-
updatedReturn = addSourceMapMarker(updatedReturn, ctx.sourceFile, returnType);
|
|
1159
|
-
}
|
|
1160
|
-
return updatedReturn;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
return ctx.ts.visitEachChild(node, returnTransformer, ctx.context);
|
|
1164
|
-
};
|
|
1165
|
-
transformedStatements = ctx.ts.visitNodes(visitedBody.statements, returnTransformer);
|
|
1166
|
-
}
|
|
1167
|
-
// Insert validation statements at the beginning
|
|
1168
|
-
const newStatements = ctx.factory.createNodeArray([
|
|
1169
|
-
...validationStatements,
|
|
1170
|
-
...transformedStatements,
|
|
1171
|
-
]);
|
|
1172
|
-
const newBody = ctx.factory.updateBlock(visitedBody, newStatements);
|
|
1173
|
-
if (ts.isFunctionDeclaration(func)) {
|
|
1174
|
-
return ctx.factory.updateFunctionDeclaration(func, func.modifiers, func.asteriskToken, func.name, func.typeParameters, func.parameters, func.type, newBody);
|
|
1175
|
-
}
|
|
1176
|
-
else if (ts.isArrowFunction(func)) {
|
|
1177
|
-
return ctx.factory.updateArrowFunction(func, func.modifiers, func.typeParameters, func.parameters, func.type, func.equalsGreaterThanToken, newBody);
|
|
1178
|
-
}
|
|
1179
|
-
else if (ts.isMethodDeclaration(func)) {
|
|
1180
|
-
return ctx.factory.updateMethodDeclaration(func, func.modifiers, func.asteriskToken, func.name, func.questionToken, func.typeParameters, func.parameters, func.type, newBody);
|
|
1181
|
-
}
|
|
1182
|
-
return func;
|
|
1183
|
-
};
|
|
1184
|
-
return visit;
|
|
1185
|
-
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Transform a single source file with TypeScript AST
|
|
1188
|
-
*/
|
|
1189
|
-
transformSourceFile(sourceFile, ctx, typeChecker, skippedTypes = new Set()) {
|
|
1190
|
-
if (!sourceFile.fileName.includes('transformer.test.ts')) {
|
|
1191
|
-
// Check if this file has already been transformed by us
|
|
1192
|
-
const sourceText = sourceFile.getFullText();
|
|
1193
|
-
if (sourceText.includes('__typical_' + 'assert_') || sourceText.includes('__typical_' + 'stringify_') || sourceText.includes('__typical_' + 'parse_')) {
|
|
1194
|
-
throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
|
|
1195
|
-
}
|
|
42
|
+
async transform(fileName, mode = 'ts') {
|
|
43
|
+
if (mode === 'js') {
|
|
44
|
+
throw new Error('Mode "js" not yet supported - use "ts" and transpile separately');
|
|
1196
45
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
this.
|
|
1200
|
-
this.typeParsers.clear();
|
|
1201
|
-
// Create state object to track mutable state across visitor calls
|
|
1202
|
-
const state = { needsTypiaImport: false };
|
|
1203
|
-
// Create visitor and transform the source file
|
|
1204
|
-
const visit = this.createVisitor(ctx, typeChecker, skippedTypes, state);
|
|
1205
|
-
let transformedSourceFile = ctx.ts.visitNode(sourceFile, visit);
|
|
1206
|
-
// Add typia import and validator statements if needed
|
|
1207
|
-
if (state.needsTypiaImport) {
|
|
1208
|
-
transformedSourceFile = this.addTypiaImport(transformedSourceFile, ctx);
|
|
1209
|
-
// Add validator statements after imports (only if using reusable validators)
|
|
1210
|
-
if (this.config.reusableValidators) {
|
|
1211
|
-
const validatorStmts = this.createValidatorStatements(ctx);
|
|
1212
|
-
if (validatorStmts.length > 0) {
|
|
1213
|
-
const importStatements = transformedSourceFile.statements.filter(ctx.ts.isImportDeclaration);
|
|
1214
|
-
const otherStatements = transformedSourceFile.statements.filter((stmt) => !ctx.ts.isImportDeclaration(stmt));
|
|
1215
|
-
const newStatements = ctx.factory.createNodeArray([
|
|
1216
|
-
...importStatements,
|
|
1217
|
-
...validatorStmts,
|
|
1218
|
-
...otherStatements,
|
|
1219
|
-
]);
|
|
1220
|
-
transformedSourceFile = ctx.factory.updateSourceFile(transformedSourceFile, newStatements);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
// Add line markers to original statements for source map identity mappings.
|
|
1225
|
-
// This ensures original source lines map to themselves rather than inheriting
|
|
1226
|
-
// from previous @T markers.
|
|
1227
|
-
transformedSourceFile = this.addLineMarkersToStatements(transformedSourceFile, ctx, sourceFile);
|
|
1228
|
-
return transformedSourceFile;
|
|
1229
|
-
}
|
|
1230
|
-
/**
|
|
1231
|
-
* Add @L line markers to nodes that have original source positions.
|
|
1232
|
-
* This preserves identity mappings for original code, so lines from the source
|
|
1233
|
-
* file map back to themselves rather than inheriting from generated code markers.
|
|
1234
|
-
*
|
|
1235
|
-
* We need to add markers to every node that will be printed on its own line,
|
|
1236
|
-
* including nested members of interfaces, classes, etc.
|
|
1237
|
-
*/
|
|
1238
|
-
addLineMarkersToStatements(transformedFile, ctx, originalSourceFile) {
|
|
1239
|
-
const { ts, factory } = ctx;
|
|
1240
|
-
// Check if a node already has a marker comment
|
|
1241
|
-
const hasMarker = (node) => {
|
|
1242
|
-
const existingComments = ts.getSyntheticLeadingComments(node);
|
|
1243
|
-
return existingComments?.some(c => c.text.startsWith('@T:') || c.text.startsWith('@L:')) ?? false;
|
|
1244
|
-
};
|
|
1245
|
-
// Check if node has valid original position
|
|
1246
|
-
const hasOriginalPosition = (node) => {
|
|
1247
|
-
return node.pos >= 0 && node.end > node.pos;
|
|
1248
|
-
};
|
|
1249
|
-
// Recursively process a node and its children to add line markers
|
|
1250
|
-
const addMarkersToNode = (node) => {
|
|
1251
|
-
// Handle interface declarations - add markers to members
|
|
1252
|
-
if (ts.isInterfaceDeclaration(node)) {
|
|
1253
|
-
const markedMembers = node.members.map(member => {
|
|
1254
|
-
if (!hasMarker(member) && hasOriginalPosition(member)) {
|
|
1255
|
-
return addLineMarker(member, originalSourceFile, member);
|
|
1256
|
-
}
|
|
1257
|
-
return member;
|
|
1258
|
-
});
|
|
1259
|
-
const updatedNode = factory.updateInterfaceDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, markedMembers);
|
|
1260
|
-
// Also mark the interface itself
|
|
1261
|
-
if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
|
|
1262
|
-
return addLineMarker(updatedNode, originalSourceFile, node);
|
|
1263
|
-
}
|
|
1264
|
-
return updatedNode;
|
|
1265
|
-
}
|
|
1266
|
-
// Handle type alias declarations
|
|
1267
|
-
if (ts.isTypeAliasDeclaration(node)) {
|
|
1268
|
-
if (!hasMarker(node) && hasOriginalPosition(node)) {
|
|
1269
|
-
return addLineMarker(node, originalSourceFile, node);
|
|
1270
|
-
}
|
|
1271
|
-
return node;
|
|
1272
|
-
}
|
|
1273
|
-
// Handle class declarations - add markers to members
|
|
1274
|
-
if (ts.isClassDeclaration(node)) {
|
|
1275
|
-
const markedMembers = node.members.map(member => {
|
|
1276
|
-
// Recursively process method bodies
|
|
1277
|
-
let processedMember = member;
|
|
1278
|
-
if (ts.isMethodDeclaration(member) && member.body) {
|
|
1279
|
-
const markedBody = addMarkersToBlock(member.body);
|
|
1280
|
-
if (markedBody !== member.body) {
|
|
1281
|
-
processedMember = factory.updateMethodDeclaration(member, member.modifiers, member.asteriskToken, member.name, member.questionToken, member.typeParameters, member.parameters, member.type, markedBody);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
if (!hasMarker(processedMember) && hasOriginalPosition(member)) {
|
|
1285
|
-
return addLineMarker(processedMember, originalSourceFile, member);
|
|
1286
|
-
}
|
|
1287
|
-
return processedMember;
|
|
1288
|
-
});
|
|
1289
|
-
const updatedNode = factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, markedMembers);
|
|
1290
|
-
if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
|
|
1291
|
-
return addLineMarker(updatedNode, originalSourceFile, node);
|
|
1292
|
-
}
|
|
1293
|
-
return updatedNode;
|
|
1294
|
-
}
|
|
1295
|
-
// Handle function declarations - add markers to body statements
|
|
1296
|
-
if (ts.isFunctionDeclaration(node) && node.body) {
|
|
1297
|
-
const markedBody = addMarkersToBlock(node.body);
|
|
1298
|
-
const updatedNode = factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, markedBody);
|
|
1299
|
-
if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
|
|
1300
|
-
return addLineMarker(updatedNode, originalSourceFile, node);
|
|
1301
|
-
}
|
|
1302
|
-
return updatedNode;
|
|
1303
|
-
}
|
|
1304
|
-
// Handle variable statements
|
|
1305
|
-
if (ts.isVariableStatement(node)) {
|
|
1306
|
-
if (!hasMarker(node) && hasOriginalPosition(node)) {
|
|
1307
|
-
return addLineMarker(node, originalSourceFile, node);
|
|
1308
|
-
}
|
|
1309
|
-
return node;
|
|
1310
|
-
}
|
|
1311
|
-
// Handle expression statements
|
|
1312
|
-
if (ts.isExpressionStatement(node)) {
|
|
1313
|
-
if (!hasMarker(node) && hasOriginalPosition(node)) {
|
|
1314
|
-
return addLineMarker(node, originalSourceFile, node);
|
|
1315
|
-
}
|
|
1316
|
-
return node;
|
|
1317
|
-
}
|
|
1318
|
-
// Handle return statements
|
|
1319
|
-
if (ts.isReturnStatement(node)) {
|
|
1320
|
-
if (!hasMarker(node) && hasOriginalPosition(node)) {
|
|
1321
|
-
return addLineMarker(node, originalSourceFile, node);
|
|
1322
|
-
}
|
|
1323
|
-
return node;
|
|
1324
|
-
}
|
|
1325
|
-
// Handle if statements
|
|
1326
|
-
if (ts.isIfStatement(node)) {
|
|
1327
|
-
let thenStmt = node.thenStatement;
|
|
1328
|
-
let elseStmt = node.elseStatement;
|
|
1329
|
-
if (ts.isBlock(thenStmt)) {
|
|
1330
|
-
thenStmt = addMarkersToBlock(thenStmt);
|
|
1331
|
-
}
|
|
1332
|
-
if (elseStmt && ts.isBlock(elseStmt)) {
|
|
1333
|
-
elseStmt = addMarkersToBlock(elseStmt);
|
|
1334
|
-
}
|
|
1335
|
-
const updatedNode = factory.updateIfStatement(node, node.expression, thenStmt, elseStmt);
|
|
1336
|
-
if (!hasMarker(updatedNode) && hasOriginalPosition(node)) {
|
|
1337
|
-
return addLineMarker(updatedNode, originalSourceFile, node);
|
|
1338
|
-
}
|
|
1339
|
-
return updatedNode;
|
|
1340
|
-
}
|
|
1341
|
-
// Default: just mark the node if it has original position
|
|
1342
|
-
if (!hasMarker(node) && hasOriginalPosition(node)) {
|
|
1343
|
-
return addLineMarker(node, originalSourceFile, node);
|
|
1344
|
-
}
|
|
1345
|
-
return node;
|
|
1346
|
-
};
|
|
1347
|
-
// Add markers to statements in a block
|
|
1348
|
-
const addMarkersToBlock = (block) => {
|
|
1349
|
-
const markedStatements = block.statements.map(stmt => addMarkersToNode(stmt));
|
|
1350
|
-
return factory.updateBlock(block, markedStatements);
|
|
1351
|
-
};
|
|
1352
|
-
// Process all top-level statements
|
|
1353
|
-
const newStatements = factory.createNodeArray(transformedFile.statements.map(stmt => addMarkersToNode(stmt)));
|
|
1354
|
-
return factory.updateSourceFile(transformedFile, newStatements);
|
|
1355
|
-
}
|
|
1356
|
-
shouldTransformFile(fileName) {
|
|
1357
|
-
return shouldTransformFile(fileName, this.config);
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Get pre-compiled ignore patterns, caching them for performance.
|
|
1361
|
-
*/
|
|
1362
|
-
getCompiledPatterns() {
|
|
1363
|
-
if (!this.compiledPatterns) {
|
|
1364
|
-
this.compiledPatterns = getCompiledIgnorePatterns(this.config);
|
|
1365
|
-
}
|
|
1366
|
-
return this.compiledPatterns;
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Check if a TypeNode represents a type that shouldn't be validated.
|
|
1370
|
-
* This includes:
|
|
1371
|
-
* - any/unknown (intentional escape hatches)
|
|
1372
|
-
* - Type parameters (generics like T)
|
|
1373
|
-
* - Constructor types (new (...args: any[]) => T)
|
|
1374
|
-
* - Function types ((...args) => T)
|
|
1375
|
-
*/
|
|
1376
|
-
isAnyOrUnknownType(typeNode) {
|
|
1377
|
-
// any/unknown are escape hatches
|
|
1378
|
-
if (typeNode.kind === this.ts.SyntaxKind.AnyKeyword ||
|
|
1379
|
-
typeNode.kind === this.ts.SyntaxKind.UnknownKeyword) {
|
|
1380
|
-
return true;
|
|
1381
|
-
}
|
|
1382
|
-
// Type parameters (generics) can't be validated at runtime
|
|
1383
|
-
if (typeNode.kind === this.ts.SyntaxKind.TypeReference) {
|
|
1384
|
-
const typeRef = typeNode;
|
|
1385
|
-
// Single identifier that's a type parameter
|
|
1386
|
-
if (ts.isIdentifier(typeRef.typeName)) {
|
|
1387
|
-
// Check if it's a type parameter by looking for it in enclosing type parameter lists
|
|
1388
|
-
// For now, we'll check if it's a single uppercase letter or common generic names
|
|
1389
|
-
const name = typeRef.typeName.text;
|
|
1390
|
-
// Common type parameter names - single letters or common conventions
|
|
1391
|
-
if (/^[A-Z]$/.test(name) || /^T[A-Z]?[a-z]*$/.test(name)) {
|
|
1392
|
-
return true;
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
// Constructor types can't be validated by typia
|
|
1397
|
-
if (typeNode.kind === this.ts.SyntaxKind.ConstructorType) {
|
|
1398
|
-
return true;
|
|
1399
|
-
}
|
|
1400
|
-
// Function types generally shouldn't be validated
|
|
1401
|
-
if (typeNode.kind === this.ts.SyntaxKind.FunctionType) {
|
|
1402
|
-
return true;
|
|
1403
|
-
}
|
|
1404
|
-
return false;
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Check if a type contains any unvalidatable parts (type parameters, constructor types, etc.)
|
|
1408
|
-
* This recursively checks intersection and union types.
|
|
1409
|
-
*/
|
|
1410
|
-
containsUnvalidatableType(type) {
|
|
1411
|
-
// Type parameters can't be validated at runtime
|
|
1412
|
-
if ((type.flags & this.ts.TypeFlags.TypeParameter) !== 0) {
|
|
1413
|
-
return true;
|
|
1414
|
-
}
|
|
1415
|
-
// Check intersection types - if any part is unvalidatable, the whole thing is
|
|
1416
|
-
if (type.isIntersection()) {
|
|
1417
|
-
return type.types.some(t => this.containsUnvalidatableType(t));
|
|
1418
|
-
}
|
|
1419
|
-
// Check union types
|
|
1420
|
-
if (type.isUnion()) {
|
|
1421
|
-
return type.types.some(t => this.containsUnvalidatableType(t));
|
|
1422
|
-
}
|
|
1423
|
-
// Check for constructor signatures (like `new (...args) => T`)
|
|
1424
|
-
const callSignatures = type.getConstructSignatures?.() ?? [];
|
|
1425
|
-
if (callSignatures.length > 0) {
|
|
1426
|
-
return true;
|
|
1427
|
-
}
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* Check if a Type has any or unknown flags, or is a type parameter or function/constructor.
|
|
1432
|
-
*/
|
|
1433
|
-
isAnyOrUnknownTypeFlags(type) {
|
|
1434
|
-
// any/unknown
|
|
1435
|
-
if ((type.flags & this.ts.TypeFlags.Any) !== 0 ||
|
|
1436
|
-
(type.flags & this.ts.TypeFlags.Unknown) !== 0) {
|
|
1437
|
-
return true;
|
|
1438
|
-
}
|
|
1439
|
-
// Type parameters (generics) - can't be validated at runtime
|
|
1440
|
-
if ((type.flags & this.ts.TypeFlags.TypeParameter) !== 0) {
|
|
1441
|
-
return true;
|
|
1442
|
-
}
|
|
1443
|
-
return false;
|
|
1444
|
-
}
|
|
1445
|
-
/**
|
|
1446
|
-
* Check if a type name matches any of the ignoreTypes patterns.
|
|
1447
|
-
* Supports wildcards: "React.*" matches "React.FormEvent", "React.ChangeEvent", etc.
|
|
1448
|
-
* Also handles union types: "Document | Element" is ignored if "Document" or "Element" is in ignoreTypes.
|
|
1449
|
-
*/
|
|
1450
|
-
isIgnoredType(typeName, typeChecker, type) {
|
|
1451
|
-
const compiled = this.getCompiledPatterns();
|
|
1452
|
-
if (compiled.allPatterns.length === 0)
|
|
1453
|
-
return false;
|
|
1454
|
-
// For union types, check each constituent
|
|
1455
|
-
if (type && type.isUnion()) {
|
|
1456
|
-
const nonNullTypes = type.types.filter(t => !(t.flags & this.ts.TypeFlags.Null) && !(t.flags & this.ts.TypeFlags.Undefined));
|
|
1457
|
-
if (nonNullTypes.length === 0)
|
|
1458
|
-
return false;
|
|
1459
|
-
// All non-null types must be ignored
|
|
1460
|
-
return nonNullTypes.every(t => this.isIgnoredSingleType(t, compiled.allPatterns, typeChecker));
|
|
1461
|
-
}
|
|
1462
|
-
// For non-union types, check directly
|
|
1463
|
-
if (type && typeChecker) {
|
|
1464
|
-
return this.isIgnoredSingleType(type, compiled.allPatterns, typeChecker);
|
|
1465
|
-
}
|
|
1466
|
-
// Fallback: string-based matching for union types like "Document | Element | null"
|
|
1467
|
-
const typeParts = typeName.split(' | ').map(t => t.trim());
|
|
1468
|
-
const nonNullParts = typeParts.filter(t => t !== 'null' && t !== 'undefined');
|
|
1469
|
-
if (nonNullParts.length === 0)
|
|
1470
|
-
return false;
|
|
1471
|
-
return nonNullParts.every(part => this.matchesIgnorePatternCompiled(part, compiled.allPatterns));
|
|
1472
|
-
}
|
|
1473
|
-
/**
|
|
1474
|
-
* Check if a single type (not a union) should be ignored.
|
|
1475
|
-
* Checks both the type name and its base classes.
|
|
1476
|
-
* Uses Set-based cycle detection to handle recursive type hierarchies.
|
|
1477
|
-
* @param patterns Pre-compiled RegExp patterns
|
|
1478
|
-
* @param visited Set of type IDs already visited (for cycle detection)
|
|
1479
|
-
*/
|
|
1480
|
-
isIgnoredSingleType(type, patterns, typeChecker, visited = new Set()) {
|
|
1481
|
-
// Use type ID for cycle detection (more precise than depth counter)
|
|
1482
|
-
const typeId = type.id;
|
|
1483
|
-
if (typeId !== undefined) {
|
|
1484
|
-
if (visited.has(typeId)) {
|
|
1485
|
-
if (process.env.DEBUG) {
|
|
1486
|
-
console.log(`TYPICAL: Cycle detected for type "${type.symbol?.name || '?'}" (id: ${typeId}), skipping`);
|
|
1487
|
-
}
|
|
1488
|
-
return false; // Already visited this type, not ignored
|
|
1489
|
-
}
|
|
1490
|
-
visited.add(typeId);
|
|
1491
|
-
}
|
|
1492
|
-
const typeName = type.symbol?.name || '';
|
|
1493
|
-
if (process.env.DEBUG) {
|
|
1494
|
-
console.log(`TYPICAL: isIgnoredSingleType checking type: "${typeName}" (visited: ${visited.size})`);
|
|
1495
|
-
}
|
|
1496
|
-
// Check direct name match
|
|
1497
|
-
if (this.matchesIgnorePatternCompiled(typeName, patterns)) {
|
|
1498
|
-
if (process.env.DEBUG) {
|
|
1499
|
-
console.log(`TYPICAL: Type "${typeName}" matched ignore pattern directly`);
|
|
1500
|
-
}
|
|
1501
|
-
return true;
|
|
1502
|
-
}
|
|
1503
|
-
// Check base classes (for classes extending DOM types like HTMLElement)
|
|
1504
|
-
// This works for class types that have getBaseTypes available
|
|
1505
|
-
const baseTypes = type.getBaseTypes?.() ?? [];
|
|
1506
|
-
if (process.env.DEBUG && baseTypes.length > 0) {
|
|
1507
|
-
console.log(`TYPICAL: Type "${typeName}" has ${baseTypes.length} base types: ${baseTypes.map(t => t.symbol?.name || '?').join(', ')}`);
|
|
1508
|
-
}
|
|
1509
|
-
for (const baseType of baseTypes) {
|
|
1510
|
-
if (this.isIgnoredSingleType(baseType, patterns, typeChecker, visited)) {
|
|
1511
|
-
if (process.env.DEBUG) {
|
|
1512
|
-
console.log(`TYPICAL: Type "${typeName}" ignored because base type "${baseType.symbol?.name}" is ignored`);
|
|
1513
|
-
}
|
|
1514
|
-
return true;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
// Also check the declared type's symbol for heritage clauses (alternative approach)
|
|
1518
|
-
// This handles cases where getBaseTypes doesn't return what we expect
|
|
1519
|
-
if (type.symbol?.declarations) {
|
|
1520
|
-
for (const decl of type.symbol.declarations) {
|
|
1521
|
-
if (this.ts.isClassDeclaration(decl) && decl.heritageClauses) {
|
|
1522
|
-
for (const heritage of decl.heritageClauses) {
|
|
1523
|
-
if (heritage.token === this.ts.SyntaxKind.ExtendsKeyword) {
|
|
1524
|
-
for (const heritageType of heritage.types) {
|
|
1525
|
-
const baseTypeName = heritageType.expression.getText();
|
|
1526
|
-
if (process.env.DEBUG) {
|
|
1527
|
-
console.log(`TYPICAL: Type "${typeName}" extends "${baseTypeName}" (from heritage clause)`);
|
|
1528
|
-
}
|
|
1529
|
-
if (this.matchesIgnorePatternCompiled(baseTypeName, patterns)) {
|
|
1530
|
-
if (process.env.DEBUG) {
|
|
1531
|
-
console.log(`TYPICAL: Type "${typeName}" ignored because it extends "${baseTypeName}"`);
|
|
1532
|
-
}
|
|
1533
|
-
return true;
|
|
1534
|
-
}
|
|
1535
|
-
// Recursively check the heritage type
|
|
1536
|
-
if (typeChecker) {
|
|
1537
|
-
const heritageTypeObj = typeChecker.getTypeAtLocation(heritageType);
|
|
1538
|
-
if (this.isIgnoredSingleType(heritageTypeObj, patterns, typeChecker, visited)) {
|
|
1539
|
-
return true;
|
|
1540
|
-
}
|
|
1541
|
-
// For mixin patterns like `extends VueWatcher(BaseElement)`, the expression is a CallExpression.
|
|
1542
|
-
// We need to check the return type of the mixin function AND the arguments passed to it.
|
|
1543
|
-
if (this.ts.isCallExpression(heritageType.expression)) {
|
|
1544
|
-
// Check arguments to the mixin (e.g., BaseElement in VueWatcher(BaseElement))
|
|
1545
|
-
for (const arg of heritageType.expression.arguments) {
|
|
1546
|
-
const argType = typeChecker.getTypeAtLocation(arg);
|
|
1547
|
-
if (process.env.DEBUG) {
|
|
1548
|
-
console.log(`TYPICAL: Type "${typeName}" mixin arg: "${argType.symbol?.name}" (from call expression)`);
|
|
1549
|
-
}
|
|
1550
|
-
if (this.isIgnoredSingleType(argType, patterns, typeChecker, visited)) {
|
|
1551
|
-
if (process.env.DEBUG) {
|
|
1552
|
-
console.log(`TYPICAL: Type "${typeName}" ignored because mixin argument "${argType.symbol?.name}" is ignored`);
|
|
1553
|
-
}
|
|
1554
|
-
return true;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
return false;
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* Check if a single type name matches any pre-compiled ignore pattern.
|
|
1569
|
-
* @param patterns Pre-compiled RegExp patterns
|
|
1570
|
-
*/
|
|
1571
|
-
matchesIgnorePatternCompiled(typeName, patterns) {
|
|
1572
|
-
return patterns.some(pattern => pattern.test(typeName));
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* Find untransformed typia calls in the output code.
|
|
1576
|
-
* These indicate types that typia could not process.
|
|
1577
|
-
*/
|
|
1578
|
-
findUntransformedTypiaCalls(code) {
|
|
1579
|
-
const results = [];
|
|
1580
|
-
// Match patterns like: typia.createAssert<Type>() or typia.json.createAssertParse<Type>()
|
|
1581
|
-
// The type argument can contain nested generics like React.FormEvent<HTMLElement>
|
|
1582
|
-
const patterns = [
|
|
1583
|
-
/typia\.createAssert<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
|
|
1584
|
-
/typia\.json\.createAssertParse<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
|
|
1585
|
-
/typia\.json\.createStringify<([^>]+(?:<[^>]*>)?)>\s*\(\)/g,
|
|
1586
|
-
];
|
|
1587
|
-
for (const pattern of patterns) {
|
|
1588
|
-
let match;
|
|
1589
|
-
while ((match = pattern.exec(code)) !== null) {
|
|
1590
|
-
const methodMatch = match[0].match(/typia\.([\w.]+)</);
|
|
1591
|
-
results.push({
|
|
1592
|
-
method: methodMatch ? methodMatch[1] : 'unknown',
|
|
1593
|
-
type: match[1]
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
return results;
|
|
1598
|
-
}
|
|
1599
|
-
/**
|
|
1600
|
-
* Infer type information from a JSON.stringify argument for creating a reusable stringifier.
|
|
1601
|
-
*/
|
|
1602
|
-
inferStringifyType(arg, typeChecker, ctx) {
|
|
1603
|
-
const ts = this.ts;
|
|
1604
|
-
// Type assertion: use the asserted type directly
|
|
1605
|
-
if (ts.isAsExpression(arg)) {
|
|
1606
|
-
const typeNode = arg.type;
|
|
1607
|
-
const typeKey = this.getTypeKey(typeNode, typeChecker);
|
|
1608
|
-
return { typeText: typeKey, typeNode };
|
|
1609
|
-
}
|
|
1610
|
-
// Object literal: infer type from type checker
|
|
1611
|
-
if (ts.isObjectLiteralExpression(arg)) {
|
|
1612
|
-
const objectType = typeChecker.getTypeAtLocation(arg);
|
|
1613
|
-
const typeNode = typeChecker.typeToTypeNode(objectType, arg, TYPE_NODE_FLAGS);
|
|
1614
|
-
if (!typeNode) {
|
|
1615
|
-
throw new Error('unknown type node for object literal: ' + arg.getText());
|
|
1616
|
-
}
|
|
1617
|
-
const typeKey = this.getTypeKey(typeNode, typeChecker, objectType);
|
|
1618
|
-
return { typeText: typeKey, typeNode };
|
|
1619
|
-
}
|
|
1620
|
-
// Other expressions: infer from type checker
|
|
1621
|
-
const argType = typeChecker.getTypeAtLocation(arg);
|
|
1622
|
-
const typeNode = typeChecker.typeToTypeNode(argType, arg, TYPE_NODE_FLAGS);
|
|
1623
|
-
if (typeNode) {
|
|
1624
|
-
const typeKey = this.getTypeKey(typeNode, typeChecker, argType);
|
|
1625
|
-
return { typeText: typeKey, typeNode };
|
|
1626
|
-
}
|
|
1627
|
-
// Fallback to unknown
|
|
46
|
+
await this.ensureInitialized();
|
|
47
|
+
const resolvedPath = resolve(fileName);
|
|
48
|
+
const result = await this.compiler.transformFile(this.projectHandle, resolvedPath);
|
|
1628
49
|
return {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
// ============================================
|
|
1634
|
-
// Flow Analysis Helpers
|
|
1635
|
-
// ============================================
|
|
1636
|
-
/**
|
|
1637
|
-
* Gets the root identifier from an expression.
|
|
1638
|
-
* e.g., `user.address.city` -> "user"
|
|
1639
|
-
*/
|
|
1640
|
-
getRootIdentifier(expr) {
|
|
1641
|
-
if (this.ts.isIdentifier(expr)) {
|
|
1642
|
-
return expr.text;
|
|
1643
|
-
}
|
|
1644
|
-
if (this.ts.isPropertyAccessExpression(expr)) {
|
|
1645
|
-
return this.getRootIdentifier(expr.expression);
|
|
1646
|
-
}
|
|
1647
|
-
return undefined;
|
|
1648
|
-
}
|
|
1649
|
-
/**
|
|
1650
|
-
* Check if a validated variable has been tainted (mutated) in the function body.
|
|
1651
|
-
* A variable is tainted if it's reassigned, has properties modified, is passed
|
|
1652
|
-
* to a function, has methods called on it, or if an await occurs.
|
|
1653
|
-
*/
|
|
1654
|
-
isTainted(varName, body) {
|
|
1655
|
-
let tainted = false;
|
|
1656
|
-
const ts = this.ts;
|
|
1657
|
-
// Collect aliases (variables that reference properties of varName)
|
|
1658
|
-
// e.g., const addr = user.address; -> addr is an alias
|
|
1659
|
-
const aliases = new Set([varName]);
|
|
1660
|
-
const collectAliases = (node) => {
|
|
1661
|
-
if (ts.isVariableStatement(node)) {
|
|
1662
|
-
for (const decl of node.declarationList.declarations) {
|
|
1663
|
-
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
1664
|
-
const initRoot = this.getRootIdentifier(decl.initializer);
|
|
1665
|
-
if (initRoot && aliases.has(initRoot)) {
|
|
1666
|
-
aliases.add(decl.name.text);
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
ts.forEachChild(node, collectAliases);
|
|
1672
|
-
};
|
|
1673
|
-
collectAliases(body);
|
|
1674
|
-
// Helper to check if any alias is involved
|
|
1675
|
-
const involvesTrackedVar = (expr) => {
|
|
1676
|
-
const root = this.getRootIdentifier(expr);
|
|
1677
|
-
return root !== undefined && aliases.has(root);
|
|
1678
|
-
};
|
|
1679
|
-
const checkTainting = (node) => {
|
|
1680
|
-
if (tainted)
|
|
1681
|
-
return;
|
|
1682
|
-
// Reassignment: trackedVar = ...
|
|
1683
|
-
if (ts.isBinaryExpression(node) &&
|
|
1684
|
-
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1685
|
-
ts.isIdentifier(node.left) &&
|
|
1686
|
-
aliases.has(node.left.text)) {
|
|
1687
|
-
tainted = true;
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
// Property assignment: trackedVar.x = ... or alias.x = ...
|
|
1691
|
-
if (ts.isBinaryExpression(node) &&
|
|
1692
|
-
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1693
|
-
ts.isPropertyAccessExpression(node.left) &&
|
|
1694
|
-
involvesTrackedVar(node.left)) {
|
|
1695
|
-
tainted = true;
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
// Element assignment: trackedVar[x] = ... or alias[x] = ...
|
|
1699
|
-
if (ts.isBinaryExpression(node) &&
|
|
1700
|
-
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1701
|
-
ts.isElementAccessExpression(node.left) &&
|
|
1702
|
-
involvesTrackedVar(node.left.expression)) {
|
|
1703
|
-
tainted = true;
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
// Passed as argument to a function: fn(trackedVar) or fn(alias)
|
|
1707
|
-
if (ts.isCallExpression(node)) {
|
|
1708
|
-
for (const arg of node.arguments) {
|
|
1709
|
-
let hasTrackedRef = false;
|
|
1710
|
-
const checkRef = (n) => {
|
|
1711
|
-
if (ts.isIdentifier(n) && aliases.has(n.text)) {
|
|
1712
|
-
hasTrackedRef = true;
|
|
1713
|
-
}
|
|
1714
|
-
ts.forEachChild(n, checkRef);
|
|
1715
|
-
};
|
|
1716
|
-
checkRef(arg);
|
|
1717
|
-
if (hasTrackedRef) {
|
|
1718
|
-
tainted = true;
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
// Method call on the variable: trackedVar.method() or alias.method()
|
|
1724
|
-
if (ts.isCallExpression(node) &&
|
|
1725
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
1726
|
-
involvesTrackedVar(node.expression.expression)) {
|
|
1727
|
-
tainted = true;
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
// Await expression (async boundary - external code could run)
|
|
1731
|
-
if (ts.isAwaitExpression(node)) {
|
|
1732
|
-
tainted = true;
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
ts.forEachChild(node, checkTainting);
|
|
1736
|
-
};
|
|
1737
|
-
checkTainting(body);
|
|
1738
|
-
return tainted;
|
|
1739
|
-
}
|
|
1740
|
-
addTypiaImport(sourceFile, ctx) {
|
|
1741
|
-
const { factory } = ctx;
|
|
1742
|
-
const existingImports = sourceFile.statements.filter(ctx.ts.isImportDeclaration);
|
|
1743
|
-
const hasTypiaImport = existingImports.some((imp) => imp.moduleSpecifier &&
|
|
1744
|
-
ctx.ts.isStringLiteral(imp.moduleSpecifier) &&
|
|
1745
|
-
imp.moduleSpecifier.text === "typia");
|
|
1746
|
-
if (!hasTypiaImport) {
|
|
1747
|
-
const typiaImport = factory.createImportDeclaration(undefined, factory.createImportClause(false, factory.createIdentifier("typia"), undefined), factory.createStringLiteral("typia"));
|
|
1748
|
-
const newSourceFile = factory.updateSourceFile(sourceFile, factory.createNodeArray([typiaImport, ...sourceFile.statements]));
|
|
1749
|
-
return newSourceFile;
|
|
1750
|
-
}
|
|
1751
|
-
return sourceFile;
|
|
1752
|
-
}
|
|
1753
|
-
/**
|
|
1754
|
-
* Gets type text for use as a validator map key.
|
|
1755
|
-
* Uses getText() to preserve local aliases (e.g., "User1" vs "User2"),
|
|
1756
|
-
* but falls back to typeToString() for synthesized nodes without source positions.
|
|
1757
|
-
*
|
|
1758
|
-
* @param typeNode The TypeNode to get a key for
|
|
1759
|
-
* @param typeChecker The TypeChecker to use
|
|
1760
|
-
* @param typeObj Optional Type object - use this for synthesized nodes since
|
|
1761
|
-
* getTypeFromTypeNode doesn't work correctly on them
|
|
1762
|
-
*/
|
|
1763
|
-
getTypeKey(typeNode, typeChecker, typeObj) {
|
|
1764
|
-
// Check if node has a real position (not synthesized)
|
|
1765
|
-
if (typeNode.pos >= 0 && typeNode.end > typeNode.pos) {
|
|
1766
|
-
try {
|
|
1767
|
-
const text = typeNode.getText();
|
|
1768
|
-
// Check for truncation patterns in source text (shouldn't happen but be safe)
|
|
1769
|
-
if (!text.includes('...') || !text.match(/\.\.\.\d+\s+more/)) {
|
|
1770
|
-
return text;
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
catch {
|
|
1774
|
-
// Fall through to typeToString
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
// Fallback for synthesized nodes - use the provided Type object if available,
|
|
1778
|
-
// otherwise try to get it from the node (which may not work correctly)
|
|
1779
|
-
const type = typeObj ?? typeChecker.getTypeFromTypeNode(typeNode);
|
|
1780
|
-
const typeString = typeChecker.typeToString(type, undefined, this.ts.TypeFormatFlags.NoTruncation);
|
|
1781
|
-
// TypeScript may still truncate very large types even with NoTruncation flag.
|
|
1782
|
-
// Detect truncation patterns like "...19 more..." and use a hash-based key instead.
|
|
1783
|
-
if (typeString.match(/\.\.\.\d+\s+more/)) {
|
|
1784
|
-
const hash = this.hashString(typeString);
|
|
1785
|
-
return `__complex_type_${hash}`;
|
|
1786
|
-
}
|
|
1787
|
-
return typeString;
|
|
1788
|
-
}
|
|
1789
|
-
/**
|
|
1790
|
-
* Simple string hash for creating unique identifiers from type strings.
|
|
1791
|
-
*/
|
|
1792
|
-
hashString(str) {
|
|
1793
|
-
let hash = 0;
|
|
1794
|
-
for (let i = 0; i < str.length; i++) {
|
|
1795
|
-
const char = str.charCodeAt(i);
|
|
1796
|
-
hash = ((hash << 5) - hash) + char;
|
|
1797
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
1798
|
-
}
|
|
1799
|
-
return Math.abs(hash).toString(36);
|
|
1800
|
-
}
|
|
1801
|
-
/**
|
|
1802
|
-
* Format typia's error message into a cleaner list format.
|
|
1803
|
-
* Typia outputs verbose messages like:
|
|
1804
|
-
* "unsupported type detected\n\n- Window.ondevicemotion: unknown\n - nonsensible intersection\n\n- Window.ondeviceorientation..."
|
|
1805
|
-
* We want to extract just the problematic types and their issues.
|
|
1806
|
-
*/
|
|
1807
|
-
formatTypiaError(message) {
|
|
1808
|
-
const lines = message.split('\n');
|
|
1809
|
-
const firstLine = lines[0]; // e.g., "unsupported type detected"
|
|
1810
|
-
// Parse the error entries - each starts with "- " at the beginning of a line
|
|
1811
|
-
const issues = [];
|
|
1812
|
-
let currentIssue = null;
|
|
1813
|
-
for (const line of lines.slice(1)) {
|
|
1814
|
-
if (line.startsWith('- ')) {
|
|
1815
|
-
// New type entry
|
|
1816
|
-
if (currentIssue) {
|
|
1817
|
-
issues.push(currentIssue);
|
|
1818
|
-
}
|
|
1819
|
-
currentIssue = { type: line.slice(2), reasons: [] };
|
|
1820
|
-
}
|
|
1821
|
-
else if (line.startsWith(' - ') && currentIssue) {
|
|
1822
|
-
// Reason for current type
|
|
1823
|
-
currentIssue.reasons.push(line.slice(4));
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
if (currentIssue) {
|
|
1827
|
-
issues.push(currentIssue);
|
|
1828
|
-
}
|
|
1829
|
-
if (issues.length === 0) {
|
|
1830
|
-
return ` ${firstLine}`;
|
|
1831
|
-
}
|
|
1832
|
-
// Limit to 5 issues, show count of remaining
|
|
1833
|
-
const maxIssues = 5;
|
|
1834
|
-
const displayIssues = issues.slice(0, maxIssues);
|
|
1835
|
-
const remainingCount = issues.length - maxIssues;
|
|
1836
|
-
const formatted = displayIssues.map(issue => {
|
|
1837
|
-
const reasons = issue.reasons.map(r => ` - ${r}`).join('\n');
|
|
1838
|
-
return ` - ${issue.type}\n${reasons}`;
|
|
1839
|
-
}).join('\n');
|
|
1840
|
-
const suffix = remainingCount > 0 ? `\n (and ${remainingCount} more errors)` : '';
|
|
1841
|
-
return ` ${firstLine}\n${formatted}${suffix}`;
|
|
1842
|
-
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Creates a readable name suffix from a type string.
|
|
1845
|
-
* For simple identifiers like "User" or "string", returns the name directly.
|
|
1846
|
-
* For complex types, returns a numeric index.
|
|
1847
|
-
*/
|
|
1848
|
-
getTypeNameSuffix(typeText, existingNames, fallbackIndex) {
|
|
1849
|
-
// Complex types from getTypeKey() - use numeric index
|
|
1850
|
-
if (typeText.startsWith('__complex_type_')) {
|
|
1851
|
-
return String(fallbackIndex);
|
|
1852
|
-
}
|
|
1853
|
-
// Check if it's a simple identifier (letters, numbers, underscore, starting with letter/underscore)
|
|
1854
|
-
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(typeText)) {
|
|
1855
|
-
// It's a simple type name like "User", "string", "MyType"
|
|
1856
|
-
let name = typeText;
|
|
1857
|
-
// Handle collisions by appending a number
|
|
1858
|
-
if (existingNames.has(name)) {
|
|
1859
|
-
let i = 2;
|
|
1860
|
-
while (existingNames.has(`${typeText}${i}`)) {
|
|
1861
|
-
i++;
|
|
1862
|
-
}
|
|
1863
|
-
name = `${typeText}${i}`;
|
|
1864
|
-
}
|
|
1865
|
-
return name;
|
|
1866
|
-
}
|
|
1867
|
-
// Complex type - use numeric index
|
|
1868
|
-
return String(fallbackIndex);
|
|
1869
|
-
}
|
|
1870
|
-
/**
|
|
1871
|
-
* Generic method to get or create a typed function (validator, stringifier, or parser).
|
|
1872
|
-
*/
|
|
1873
|
-
getOrCreateTypedFunction(kind, typeText, typeNode) {
|
|
1874
|
-
const maps = {
|
|
1875
|
-
assert: this.typeValidators,
|
|
1876
|
-
stringify: this.typeStringifiers,
|
|
1877
|
-
parse: this.typeParsers,
|
|
1878
|
-
};
|
|
1879
|
-
const prefixes = {
|
|
1880
|
-
assert: '__typical_assert_',
|
|
1881
|
-
stringify: '__typical_stringify_',
|
|
1882
|
-
parse: '__typical_parse_',
|
|
50
|
+
code: result.code,
|
|
51
|
+
map: result.sourceMap ?? null,
|
|
1883
52
|
};
|
|
1884
|
-
const map = maps[kind];
|
|
1885
|
-
const prefix = prefixes[kind];
|
|
1886
|
-
if (map.has(typeText)) {
|
|
1887
|
-
return map.get(typeText).name;
|
|
1888
|
-
}
|
|
1889
|
-
const existingSuffixes = [...map.values()].map(v => v.name.slice(prefix.length));
|
|
1890
|
-
const existingNames = new Set(existingSuffixes);
|
|
1891
|
-
const numericCount = existingSuffixes.filter(s => /^\d+$/.test(s)).length;
|
|
1892
|
-
const suffix = this.getTypeNameSuffix(typeText, existingNames, numericCount);
|
|
1893
|
-
const name = `${prefix}${suffix}`;
|
|
1894
|
-
map.set(typeText, { name, typeNode });
|
|
1895
|
-
return name;
|
|
1896
|
-
}
|
|
1897
|
-
getOrCreateValidator(typeText, typeNode) {
|
|
1898
|
-
return this.getOrCreateTypedFunction('assert', typeText, typeNode);
|
|
1899
|
-
}
|
|
1900
|
-
getOrCreateStringifier(typeText, typeNode) {
|
|
1901
|
-
return this.getOrCreateTypedFunction('stringify', typeText, typeNode);
|
|
1902
|
-
}
|
|
1903
|
-
getOrCreateParser(typeText, typeNode) {
|
|
1904
|
-
return this.getOrCreateTypedFunction('parse', typeText, typeNode);
|
|
1905
53
|
}
|
|
1906
54
|
/**
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
55
|
+
* Close the Go compiler process and release resources.
|
|
56
|
+
* This immediately kills the process without waiting for pending operations.
|
|
1909
57
|
*/
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}
|
|
1915
|
-
return expr;
|
|
1916
|
-
}
|
|
1917
|
-
createValidatorStatements(ctx) {
|
|
1918
|
-
const { factory } = ctx;
|
|
1919
|
-
const statements = [];
|
|
1920
|
-
const configs = [
|
|
1921
|
-
{ map: this.typeValidators, methodPath: ['typia', 'createAssert'] },
|
|
1922
|
-
{ map: this.typeStringifiers, methodPath: ['typia', 'json', 'createStringify'] },
|
|
1923
|
-
{ map: this.typeParsers, methodPath: ['typia', 'json', 'createAssertParse'] },
|
|
1924
|
-
];
|
|
1925
|
-
for (const { map, methodPath } of configs) {
|
|
1926
|
-
for (const [, { name, typeNode }] of map) {
|
|
1927
|
-
const createCall = factory.createCallExpression(this.createPropertyAccessChain(factory, methodPath), [typeNode], []);
|
|
1928
|
-
let declaration = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, undefined, undefined, createCall)], ctx.ts.NodeFlags.Const));
|
|
1929
|
-
// Add source map marker pointing to the type node that triggered this validator
|
|
1930
|
-
// This ensures all the expanded typia validation code maps back to the original type
|
|
1931
|
-
if (typeNode.pos >= 0) {
|
|
1932
|
-
declaration = addSourceMapMarker(declaration, ctx.sourceFile, typeNode);
|
|
1933
|
-
}
|
|
1934
|
-
statements.push(declaration);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
return statements;
|
|
58
|
+
async close() {
|
|
59
|
+
this.projectHandle = null;
|
|
60
|
+
this.initPromise = null;
|
|
61
|
+
await this.compiler.close();
|
|
1938
62
|
}
|
|
1939
63
|
}
|
|
1940
64
|
//# sourceMappingURL=transformer.js.map
|