@mnemonica/tactica 0.1.0 → 0.1.2
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 +340 -262
- package/lib/analyzer.d.ts +195 -7
- package/lib/analyzer.js +1584 -19
- package/lib/analyzer.js.map +1 -1
- package/lib/cli.d.ts +6 -0
- package/lib/cli.js +202 -6
- package/lib/cli.js.map +1 -1
- package/lib/generator.d.ts +41 -7
- package/lib/generator.js +243 -49
- package/lib/generator.js.map +1 -1
- package/lib/graph.d.ts +4 -0
- package/lib/graph.js +11 -0
- package/lib/graph.js.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +39 -9
- package/lib/index.js.map +1 -1
- package/lib/topologica-analyzer.d.ts +85 -0
- package/lib/topologica-analyzer.js +509 -0
- package/lib/topologica-analyzer.js.map +1 -0
- package/lib/types.d.ts +110 -0
- package/lib/writer.d.ts +25 -13
- package/lib/writer.js +94 -51
- package/lib/writer.js.map +1 -1
- package/package.json +22 -24
- package/lib/plugin.d.ts +0 -8
- package/lib/plugin.js +0 -128
- package/lib/plugin.js.map +0 -1
package/lib/analyzer.js
CHANGED
|
@@ -43,14 +43,33 @@ class MnemonicaAnalyzer {
|
|
|
43
43
|
constructor(program) {
|
|
44
44
|
this.errors = [];
|
|
45
45
|
this.graph = new graph_1.TypeGraphImpl();
|
|
46
|
+
this.definitions = new Map();
|
|
47
|
+
this.usages = new Map();
|
|
48
|
+
this.edsUsages = new Map();
|
|
49
|
+
this.flowUsages = new Map();
|
|
50
|
+
this.typeAliases = new Map();
|
|
51
|
+
// Track variable assignments: variableName -> fullPath of the type it holds
|
|
52
|
+
this.variableToTypeMap = new Map();
|
|
46
53
|
// Store program for future use (currently unused but kept for extensibility)
|
|
47
54
|
void program;
|
|
48
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Reset usage-related state for a fresh pass.
|
|
58
|
+
* Call before the usage-collection pass to avoid duplicates from definition pass.
|
|
59
|
+
*/
|
|
60
|
+
resetUsages() {
|
|
61
|
+
this.usages.clear();
|
|
62
|
+
this.edsUsages.clear();
|
|
63
|
+
this.flowUsages.clear();
|
|
64
|
+
this.variableToTypeMap.clear();
|
|
65
|
+
}
|
|
49
66
|
/**
|
|
50
67
|
* Analyze a source file for Mnemonica type definitions
|
|
51
68
|
*/
|
|
52
69
|
analyzeFile(sourceFile) {
|
|
53
70
|
this.errors = [];
|
|
71
|
+
// Ensure parent nodes are set for AST traversal
|
|
72
|
+
this.setParentNodesInSourceFile(sourceFile);
|
|
54
73
|
this.visitNode(sourceFile, sourceFile);
|
|
55
74
|
return {
|
|
56
75
|
types: this.graph.getAllTypes(),
|
|
@@ -70,6 +89,71 @@ class MnemonicaAnalyzer {
|
|
|
70
89
|
getGraph() {
|
|
71
90
|
return this.graph;
|
|
72
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Get collected definitions
|
|
94
|
+
*/
|
|
95
|
+
getDefinitions() {
|
|
96
|
+
return this.definitions;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get collected usages
|
|
100
|
+
*/
|
|
101
|
+
getUsages() {
|
|
102
|
+
return this.usages;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get collected EDS usages
|
|
106
|
+
*/
|
|
107
|
+
getEDSUsages() {
|
|
108
|
+
return this.edsUsages;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get collected flow usages
|
|
112
|
+
*/
|
|
113
|
+
getFlowUsages() {
|
|
114
|
+
return this.flowUsages;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Add a topologica type to the analyzer for usage tracking.
|
|
118
|
+
* This allows the analyzer to recognize topologica types when collecting usages.
|
|
119
|
+
*/
|
|
120
|
+
addTopologicaType(fullPath, node) {
|
|
121
|
+
// Skip if already exists
|
|
122
|
+
if (this.graph.allTypes.has(fullPath)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Add to graph so it can be found during usage collection
|
|
126
|
+
if (node.parent) {
|
|
127
|
+
// Add as child of parent
|
|
128
|
+
this.graph.addChild(node.parent, node);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Add as root
|
|
132
|
+
this.graph.addRoot(node);
|
|
133
|
+
}
|
|
134
|
+
// Also add to definitions so it's recognized as a known type
|
|
135
|
+
const definition = {
|
|
136
|
+
name: node.name,
|
|
137
|
+
location: `${node.sourceFile}:${node.line}:${node.column}`,
|
|
138
|
+
kind: 'define',
|
|
139
|
+
parent: node.parent ? node.parent.fullPath : null,
|
|
140
|
+
strictChain: true,
|
|
141
|
+
blockErrors: false
|
|
142
|
+
};
|
|
143
|
+
this.definitions.set(fullPath, definition);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Set parent nodes in a source file to enable AST traversal up
|
|
147
|
+
*/
|
|
148
|
+
setParentNodesInSourceFile(sourceFile) {
|
|
149
|
+
const setParent = (node, parent) => {
|
|
150
|
+
// TypeScript doesn't expose parent as writable, but we need it
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
node.parent = parent;
|
|
153
|
+
ts.forEachChild(node, child => setParent(child, node));
|
|
154
|
+
};
|
|
155
|
+
setParent(sourceFile);
|
|
156
|
+
}
|
|
73
157
|
/**
|
|
74
158
|
* Visit a node in the AST
|
|
75
159
|
*/
|
|
@@ -82,6 +166,16 @@ class MnemonicaAnalyzer {
|
|
|
82
166
|
if (this.isDecorateDecorator(node)) {
|
|
83
167
|
this.processDecorateDecorator(node, sourceFile, currentClass);
|
|
84
168
|
}
|
|
169
|
+
// Check for type usages (new Type(), type annotations, etc.)
|
|
170
|
+
this.collectUsage(node, sourceFile);
|
|
171
|
+
// Check for EDS patterns (wrap, link, getLastContext, etc.)
|
|
172
|
+
this.collectEDS(node, sourceFile);
|
|
173
|
+
// Check for native flow patterns (property access, method calls, etc.)
|
|
174
|
+
this.collectFlow(node, sourceFile);
|
|
175
|
+
// Collect type aliases for resolving type references
|
|
176
|
+
if (ts.isTypeAliasDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
177
|
+
this.typeAliases.set(node.name.text, node.type);
|
|
178
|
+
}
|
|
85
179
|
// Track class declarations for decorator parent lookup
|
|
86
180
|
if (ts.isClassDeclaration(node)) {
|
|
87
181
|
// Visit children with this class as the current context
|
|
@@ -111,8 +205,37 @@ class MnemonicaAnalyzer {
|
|
|
111
205
|
return false;
|
|
112
206
|
}
|
|
113
207
|
/**
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
* Extract config options from define() call
|
|
209
|
+
*/
|
|
210
|
+
extractConfig(call) {
|
|
211
|
+
const config = {};
|
|
212
|
+
// Config is the third argument: define('Name', handler, config)
|
|
213
|
+
const configArg = call.arguments[2];
|
|
214
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg)) {
|
|
215
|
+
return config;
|
|
216
|
+
}
|
|
217
|
+
for (const prop of configArg.properties) {
|
|
218
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
219
|
+
const propName = prop.name.text;
|
|
220
|
+
if (propName === 'strictChain' && prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
221
|
+
config.strictChain = true;
|
|
222
|
+
}
|
|
223
|
+
else if (propName === 'strictChain' && prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
224
|
+
config.strictChain = false;
|
|
225
|
+
}
|
|
226
|
+
else if (propName === 'blockErrors' && prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
227
|
+
config.blockErrors = true;
|
|
228
|
+
}
|
|
229
|
+
else if (propName === 'blockErrors' && prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
230
|
+
config.blockErrors = false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return config;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if a node is a @decorate() decorator
|
|
238
|
+
*/
|
|
116
239
|
isDecorateDecorator(node) {
|
|
117
240
|
if (!ts.isDecorator(node)) {
|
|
118
241
|
return false;
|
|
@@ -135,9 +258,25 @@ class MnemonicaAnalyzer {
|
|
|
135
258
|
* Process a define() call
|
|
136
259
|
*/
|
|
137
260
|
processDefineCall(call, sourceFile) {
|
|
138
|
-
|
|
261
|
+
// Check if this exact call has already been processed (prevents duplicates from chained calls)
|
|
262
|
+
if (call.__tactica_processed) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
call.__tactica_processed = true;
|
|
139
266
|
// Get the type name from arguments
|
|
140
267
|
const typeName = this.extractTypeName(call);
|
|
268
|
+
// For chained calls like define('A').define('B'), we want the position of the .define('B') part
|
|
269
|
+
// not the start of the entire expression
|
|
270
|
+
let positionNode = call;
|
|
271
|
+
// If this is a chained call, get the position of the property access expression
|
|
272
|
+
// which is the .define part
|
|
273
|
+
if (ts.isPropertyAccessExpression(call.expression)) {
|
|
274
|
+
// The expression is the property access: (define('RootAsync', ...)).define
|
|
275
|
+
// We want the position of just the .define part
|
|
276
|
+
positionNode = call.expression.name; // This is the 'define' identifier
|
|
277
|
+
}
|
|
278
|
+
const startPos = positionNode.getStart(sourceFile);
|
|
279
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, startPos);
|
|
141
280
|
if (!typeName) {
|
|
142
281
|
this.errors.push({
|
|
143
282
|
message: 'Could not extract type name from define() call',
|
|
@@ -148,11 +287,27 @@ class MnemonicaAnalyzer {
|
|
|
148
287
|
return;
|
|
149
288
|
}
|
|
150
289
|
// Determine parent type
|
|
151
|
-
const parentNode = this.findParentType(call);
|
|
290
|
+
const parentNode = this.findParentType(call, sourceFile);
|
|
291
|
+
// Build full path
|
|
292
|
+
const fullPath = parentNode ? `${parentNode.fullPath}.${typeName}` : typeName;
|
|
293
|
+
// Extract config options
|
|
294
|
+
const config = this.extractConfig(call);
|
|
295
|
+
// Create definition info
|
|
296
|
+
const definition = {
|
|
297
|
+
name: typeName,
|
|
298
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
299
|
+
kind: 'define',
|
|
300
|
+
parent: parentNode ? parentNode.fullPath : null,
|
|
301
|
+
strictChain: config.strictChain ?? true,
|
|
302
|
+
blockErrors: config.blockErrors ?? false,
|
|
303
|
+
};
|
|
304
|
+
this.definitions.set(fullPath, definition);
|
|
152
305
|
// Create type node
|
|
153
306
|
const node = graph_1.TypeGraphImpl.createNode(typeName, parentNode, sourceFile.fileName, line + 1, character + 1);
|
|
154
307
|
// Extract properties from constructor function
|
|
155
308
|
node.properties = this.extractProperties(call);
|
|
309
|
+
// Extract constructor parameters for TypeRegistry signature
|
|
310
|
+
node.constructorParams = this.extractConstructorParams(call);
|
|
156
311
|
// Add to graph
|
|
157
312
|
if (parentNode) {
|
|
158
313
|
this.graph.addChild(parentNode, node);
|
|
@@ -160,9 +315,76 @@ class MnemonicaAnalyzer {
|
|
|
160
315
|
else {
|
|
161
316
|
this.graph.addRoot(node);
|
|
162
317
|
}
|
|
318
|
+
// Track variable assignment: const User = define('UserEntity', ...) -> map "User" to "UserEntity"
|
|
319
|
+
// For chained calls like const X = define('A').define('B'), we want to map X -> A (the root)
|
|
320
|
+
this.trackVariableAssignment(call, parentNode, fullPath);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Track variable assignments that capture define() results
|
|
324
|
+
* e.g., const User = define('UserEntity', ...) maps "User" -> "UserEntity"
|
|
325
|
+
* For chained calls like const X = define('A').define('B'), we map X -> A (the root type)
|
|
326
|
+
*/
|
|
327
|
+
trackVariableAssignment(call, parentNode, fullPath) {
|
|
328
|
+
// Check if this call is the right-hand side of a variable declaration
|
|
329
|
+
// Walk up the tree to find VariableDeclaration
|
|
330
|
+
let current = call.parent;
|
|
331
|
+
while (current) {
|
|
332
|
+
if (ts.isVariableDeclaration(current)) {
|
|
333
|
+
// Found: const X = define(...)
|
|
334
|
+
if (ts.isIdentifier(current.name)) {
|
|
335
|
+
const varName = current.name.text;
|
|
336
|
+
// If this is a chained call (has parent), don't overwrite existing mapping
|
|
337
|
+
// The first define in the chain sets the mapping to the root type
|
|
338
|
+
if (parentNode && this.variableToTypeMap.has(varName)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.variableToTypeMap.set(varName, fullPath);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
current = current.parent;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Track variable assignments from lookup() calls
|
|
350
|
+
* e.g., const SentienceConstructor = lookup('Sentience') maps "SentienceConstructor" -> "Sentience"
|
|
351
|
+
*/
|
|
352
|
+
trackLookupAssignment(call, typePath) {
|
|
353
|
+
// Walk up the tree to find VariableDeclaration
|
|
354
|
+
let current = call.parent;
|
|
355
|
+
while (current) {
|
|
356
|
+
if (ts.isVariableDeclaration(current)) {
|
|
357
|
+
// Found: const X = lookup(...)
|
|
358
|
+
if (ts.isIdentifier(current.name)) {
|
|
359
|
+
const varName = current.name.text;
|
|
360
|
+
this.variableToTypeMap.set(varName, typePath);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
current = current.parent;
|
|
365
|
+
}
|
|
163
366
|
}
|
|
164
367
|
/**
|
|
165
|
-
|
|
368
|
+
* Track variable assignments from new Type() calls
|
|
369
|
+
* e.g., const user = new UserType() maps "user" -> "UserType"
|
|
370
|
+
*/
|
|
371
|
+
trackNewAssignment(newExpr, typePath) {
|
|
372
|
+
// Walk up the tree to find VariableDeclaration
|
|
373
|
+
let current = newExpr.parent;
|
|
374
|
+
while (current) {
|
|
375
|
+
if (ts.isVariableDeclaration(current)) {
|
|
376
|
+
// Found: const X = new Type(...)
|
|
377
|
+
if (ts.isIdentifier(current.name)) {
|
|
378
|
+
const varName = current.name.text;
|
|
379
|
+
this.variableToTypeMap.set(varName, typePath);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
current = current.parent;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Process a @decorate() decorator
|
|
166
388
|
*/
|
|
167
389
|
processDecorateDecorator(decorator, sourceFile, classDeclParam) {
|
|
168
390
|
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, decorator.getStart(sourceFile));
|
|
@@ -189,6 +411,7 @@ class MnemonicaAnalyzer {
|
|
|
189
411
|
}
|
|
190
412
|
// Try to find parent type from decorator arguments
|
|
191
413
|
let parentNode;
|
|
414
|
+
let parentFullPath = null;
|
|
192
415
|
if (ts.isCallExpression(decorator.expression)) {
|
|
193
416
|
const args = decorator.expression.arguments;
|
|
194
417
|
if (args.length > 0 && ts.isIdentifier(args[0])) {
|
|
@@ -204,8 +427,23 @@ class MnemonicaAnalyzer {
|
|
|
204
427
|
}
|
|
205
428
|
}
|
|
206
429
|
}
|
|
430
|
+
if (parentNode) {
|
|
431
|
+
parentFullPath = parentNode.fullPath;
|
|
432
|
+
}
|
|
207
433
|
}
|
|
208
434
|
}
|
|
435
|
+
// Build full path
|
|
436
|
+
const fullPath = parentNode ? `${parentNode.fullPath}.${typeName}` : typeName;
|
|
437
|
+
// Create definition info for decorate
|
|
438
|
+
const definition = {
|
|
439
|
+
name: typeName,
|
|
440
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
441
|
+
kind: 'decorate',
|
|
442
|
+
parent: parentFullPath,
|
|
443
|
+
strictChain: true, // decorate uses strict defaults
|
|
444
|
+
blockErrors: false,
|
|
445
|
+
};
|
|
446
|
+
this.definitions.set(fullPath, definition);
|
|
209
447
|
// Create type node
|
|
210
448
|
const node = graph_1.TypeGraphImpl.createNode(typeName, parentNode, sourceFile.fileName, line + 1, character + 1);
|
|
211
449
|
// Extract properties from class members
|
|
@@ -248,7 +486,7 @@ class MnemonicaAnalyzer {
|
|
|
248
486
|
/**
|
|
249
487
|
* Find parent type node for nested define calls
|
|
250
488
|
*/
|
|
251
|
-
findParentType(call) {
|
|
489
|
+
findParentType(call, _sourceFile) {
|
|
252
490
|
const expression = call.expression;
|
|
253
491
|
// Check for method call: SomeType.define('SubType', ...)
|
|
254
492
|
if (!ts.isPropertyAccessExpression(expression)) {
|
|
@@ -259,6 +497,14 @@ class MnemonicaAnalyzer {
|
|
|
259
497
|
if (ts.isIdentifier(obj)) {
|
|
260
498
|
// Direct reference to parent type - search by simple name
|
|
261
499
|
const name = obj.text;
|
|
500
|
+
// First check variable mapping: const User = define('UserEntity', ...)
|
|
501
|
+
// In this case, name is "User" but we need to find "UserEntity"
|
|
502
|
+
const mappedFullPath = this.variableToTypeMap.get(name);
|
|
503
|
+
if (mappedFullPath) {
|
|
504
|
+
const mappedNode = this.graph.findType(mappedFullPath);
|
|
505
|
+
if (mappedNode)
|
|
506
|
+
return mappedNode;
|
|
507
|
+
}
|
|
262
508
|
// First try exact match
|
|
263
509
|
const exact = this.graph.findType(name);
|
|
264
510
|
if (exact)
|
|
@@ -278,11 +524,42 @@ class MnemonicaAnalyzer {
|
|
|
278
524
|
return this.graph.findType(chain.join('.'));
|
|
279
525
|
}
|
|
280
526
|
}
|
|
527
|
+
// Handle chained define: define('UserEntity', ...).define('UserResponse', ...)
|
|
528
|
+
if (ts.isCallExpression(obj)) {
|
|
529
|
+
// This is a chained call - the object is itself a define() call
|
|
530
|
+
// Check if the parent call is a define() that hasn't been processed yet
|
|
531
|
+
if (this.isDefineCall(obj)) {
|
|
532
|
+
// Process the parent define() call first to create its type node
|
|
533
|
+
// Use the sourceFile from the current context
|
|
534
|
+
this.processDefineCall(obj, _sourceFile);
|
|
535
|
+
// Now find and return the newly created parent type
|
|
536
|
+
const parentTypeName = this.extractTypeName(obj);
|
|
537
|
+
if (parentTypeName) {
|
|
538
|
+
return this.findParentTypeByName(parentTypeName);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
281
542
|
return undefined;
|
|
282
543
|
}
|
|
283
544
|
/**
|
|
284
|
-
|
|
285
|
-
|
|
545
|
+
* Find a parent type by its name, searching in the graph
|
|
546
|
+
*/
|
|
547
|
+
findParentTypeByName(name) {
|
|
548
|
+
// First try exact match
|
|
549
|
+
const exact = this.graph.findType(name);
|
|
550
|
+
if (exact)
|
|
551
|
+
return exact;
|
|
552
|
+
// Then search through all types for one with matching name
|
|
553
|
+
for (const type of this.graph.getAllTypes()) {
|
|
554
|
+
if (type.name === name) {
|
|
555
|
+
return type;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Get property chain from nested access
|
|
562
|
+
*/
|
|
286
563
|
getPropertyChain(expr) {
|
|
287
564
|
const chain = [];
|
|
288
565
|
let current = expr;
|
|
@@ -306,22 +583,41 @@ class MnemonicaAnalyzer {
|
|
|
306
583
|
if (!handlerArg) {
|
|
307
584
|
return properties;
|
|
308
585
|
}
|
|
586
|
+
// Build type map from data parameter (for this.x = data.x patterns)
|
|
587
|
+
const dataTypeMap = this.buildDataTypeMap(handlerArg);
|
|
309
588
|
// Handle function expression
|
|
310
589
|
if (ts.isFunctionExpression(handlerArg) || ts.isArrowFunction(handlerArg)) {
|
|
311
590
|
const body = handlerArg.body;
|
|
591
|
+
// First, extract properties from `this` parameter type annotation
|
|
592
|
+
// This handles patterns like: function(this: SomeType, data: SomeType) { }
|
|
593
|
+
const thisParamProperties = this.extractThisParamProperties(handlerArg);
|
|
594
|
+
for (const [name, propInfo] of thisParamProperties) {
|
|
595
|
+
properties.set(name, propInfo);
|
|
596
|
+
}
|
|
312
597
|
// Function body with statements
|
|
313
598
|
if (ts.isBlock(body)) {
|
|
314
599
|
for (const stmt of body.statements) {
|
|
315
600
|
if (ts.isExpressionStatement(stmt)) {
|
|
316
|
-
this.extractPropertyFromStatement(stmt.expression, properties);
|
|
601
|
+
this.extractPropertyFromStatement(stmt.expression, properties, dataTypeMap);
|
|
317
602
|
}
|
|
318
603
|
}
|
|
319
604
|
}
|
|
320
605
|
}
|
|
321
606
|
// Handle class expression
|
|
322
607
|
if (ts.isClassExpression(handlerArg)) {
|
|
608
|
+
// First pass: collect all property types for method inference
|
|
609
|
+
const classPropertyTypes = this.extractClassPropertyTypes(handlerArg);
|
|
323
610
|
for (const member of handlerArg.members) {
|
|
611
|
+
// Handle property declarations
|
|
324
612
|
if (ts.isPropertyDeclaration(member) && member.name) {
|
|
613
|
+
// Skip private and protected properties
|
|
614
|
+
if (member.modifiers) {
|
|
615
|
+
const hasPrivateOrProtected = member.modifiers.some(m => m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
616
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
617
|
+
if (hasPrivateOrProtected) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
325
621
|
const name = ts.isIdentifier(member.name) ? member.name.text : '';
|
|
326
622
|
if (name) {
|
|
327
623
|
properties.set(name, {
|
|
@@ -331,14 +627,120 @@ class MnemonicaAnalyzer {
|
|
|
331
627
|
});
|
|
332
628
|
}
|
|
333
629
|
}
|
|
630
|
+
// Handle method declarations
|
|
631
|
+
if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
|
|
632
|
+
// Skip private and protected methods
|
|
633
|
+
if (member.modifiers) {
|
|
634
|
+
const hasPrivateOrProtected = member.modifiers.some(m => m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
635
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
636
|
+
if (hasPrivateOrProtected) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const name = member.name.text;
|
|
641
|
+
const type = this.inferMethodType(member, classPropertyTypes);
|
|
642
|
+
properties.set(name, {
|
|
643
|
+
name,
|
|
644
|
+
type,
|
|
645
|
+
optional: false,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// Handle getter declarations
|
|
649
|
+
if (ts.isGetAccessor(member) && member.name && ts.isIdentifier(member.name)) {
|
|
650
|
+
// Skip private and protected getters
|
|
651
|
+
if (member.modifiers) {
|
|
652
|
+
const hasPrivateOrProtected = member.modifiers.some(m => m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
653
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
654
|
+
if (hasPrivateOrProtected) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const name = member.name.text;
|
|
659
|
+
// First try explicit type annotation, then infer from getter body
|
|
660
|
+
let type = this.inferType(member.type);
|
|
661
|
+
if (type === 'unknown' && member.body) {
|
|
662
|
+
type = this.inferReturnTypeFromBody(member.body, classPropertyTypes);
|
|
663
|
+
}
|
|
664
|
+
properties.set(name, {
|
|
665
|
+
name,
|
|
666
|
+
type,
|
|
667
|
+
optional: false,
|
|
668
|
+
readonly: true,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
334
671
|
}
|
|
335
672
|
}
|
|
336
673
|
return properties;
|
|
337
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Build a type map from all parameters with inline object type annotations
|
|
677
|
+
* Returns a map of "paramName.propertyName" -> type
|
|
678
|
+
*/
|
|
679
|
+
buildDataTypeMap(handlerArg) {
|
|
680
|
+
const typeMap = new Map();
|
|
681
|
+
if (!ts.isFunctionExpression(handlerArg) && !ts.isArrowFunction(handlerArg)) {
|
|
682
|
+
return typeMap;
|
|
683
|
+
}
|
|
684
|
+
// Iterate over ALL parameters
|
|
685
|
+
for (const param of handlerArg.parameters) {
|
|
686
|
+
if (!param.name || !param.type)
|
|
687
|
+
continue;
|
|
688
|
+
// Get parameter name
|
|
689
|
+
let paramName = '';
|
|
690
|
+
if (ts.isIdentifier(param.name)) {
|
|
691
|
+
paramName = param.name.text;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
continue; // Skip destructured parameters for now
|
|
695
|
+
}
|
|
696
|
+
// Check if it's an inline object type literal
|
|
697
|
+
if (ts.isTypeLiteralNode(param.type)) {
|
|
698
|
+
for (const member of param.type.members) {
|
|
699
|
+
if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
|
|
700
|
+
const propName = member.name.text;
|
|
701
|
+
const type = this.inferType(member.type);
|
|
702
|
+
typeMap.set(`${paramName}.${propName}`, type);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
// Store simple parameter types like `decorateValue: string`
|
|
708
|
+
const type = this.inferType(param.type);
|
|
709
|
+
if (type !== 'unknown') {
|
|
710
|
+
typeMap.set(paramName, type);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return typeMap;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Extract property access chain (e.g., "dataRenamed.id" from dataRenamed.id)
|
|
718
|
+
* Handles fallbacks like: data.permissions || []
|
|
719
|
+
*/
|
|
720
|
+
getPropertyAccessChain(expr) {
|
|
721
|
+
// Handle identifier: data
|
|
722
|
+
if (ts.isIdentifier(expr)) {
|
|
723
|
+
return expr.text;
|
|
724
|
+
}
|
|
725
|
+
// Handle property access: data.permissions
|
|
726
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
727
|
+
const base = this.getPropertyAccessChain(expr.expression);
|
|
728
|
+
if (base) {
|
|
729
|
+
return `${base}.${expr.name.text}`;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Handle fallback pattern: data.permissions || []
|
|
733
|
+
if (ts.isBinaryExpression(expr) &&
|
|
734
|
+
expr.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
735
|
+
// Return the left side of || operator
|
|
736
|
+
return this.getPropertyAccessChain(expr.left);
|
|
737
|
+
}
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
338
740
|
/**
|
|
339
741
|
* Extract property assignment from statement
|
|
340
742
|
*/
|
|
341
|
-
extractPropertyFromStatement(expr, properties) {
|
|
743
|
+
extractPropertyFromStatement(expr, properties, dataTypeMap = new Map()) {
|
|
342
744
|
// Handle: this.property = value
|
|
343
745
|
if (ts.isBinaryExpression(expr) &&
|
|
344
746
|
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
@@ -348,11 +750,28 @@ class MnemonicaAnalyzer {
|
|
|
348
750
|
if (left.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
349
751
|
const name = left.name?.text;
|
|
350
752
|
if (name) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
753
|
+
// Try to get type from dataTypeMap using full access chain (e.g., "dataRenamed.id")
|
|
754
|
+
const accessChain = this.getPropertyAccessChain(expr.right);
|
|
755
|
+
let type = accessChain ? dataTypeMap.get(accessChain) : undefined;
|
|
756
|
+
// If not found and RHS is a simple identifier, try looking it up directly
|
|
757
|
+
if (!type && ts.isIdentifier(expr.right)) {
|
|
758
|
+
type = dataTypeMap.get(expr.right.text);
|
|
759
|
+
}
|
|
760
|
+
if (!type) {
|
|
761
|
+
type = this.inferTypeFromInitializer(expr.right, dataTypeMap);
|
|
762
|
+
}
|
|
763
|
+
// Don't overwrite a known type from `this` annotation with unknown
|
|
764
|
+
const existing = properties.get(name);
|
|
765
|
+
if (existing && existing.type !== 'unknown' && type === 'unknown') {
|
|
766
|
+
// Keep the better type from explicit annotation
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
properties.set(name, {
|
|
770
|
+
name,
|
|
771
|
+
type,
|
|
772
|
+
optional: false,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
356
775
|
}
|
|
357
776
|
}
|
|
358
777
|
}
|
|
@@ -385,12 +804,21 @@ class MnemonicaAnalyzer {
|
|
|
385
804
|
}
|
|
386
805
|
}
|
|
387
806
|
/**
|
|
388
|
-
* Extract properties from class declaration
|
|
807
|
+
* Extract properties from class declaration (including methods and getters)
|
|
389
808
|
*/
|
|
390
809
|
extractClassProperties(classDecl) {
|
|
391
810
|
const properties = new Map();
|
|
392
811
|
for (const member of classDecl.members) {
|
|
812
|
+
// Handle property declarations
|
|
393
813
|
if (ts.isPropertyDeclaration(member) && member.name) {
|
|
814
|
+
// Skip private and protected properties
|
|
815
|
+
if (member.modifiers) {
|
|
816
|
+
const hasPrivateOrProtected = member.modifiers.some(m => m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
817
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
818
|
+
if (hasPrivateOrProtected) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
394
822
|
const name = ts.isIdentifier(member.name) ? member.name.text : '';
|
|
395
823
|
if (name) {
|
|
396
824
|
// If no explicit type but has initializer, infer from initializer
|
|
@@ -405,9 +833,137 @@ class MnemonicaAnalyzer {
|
|
|
405
833
|
});
|
|
406
834
|
}
|
|
407
835
|
}
|
|
836
|
+
// Handle method declarations
|
|
837
|
+
if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
|
|
838
|
+
// Skip private and protected methods
|
|
839
|
+
if (member.modifiers) {
|
|
840
|
+
const hasPrivateOrProtected = member.modifiers.some(m => m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
841
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
842
|
+
if (hasPrivateOrProtected) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const name = member.name.text;
|
|
847
|
+
const type = this.inferMethodType(member);
|
|
848
|
+
properties.set(name, {
|
|
849
|
+
name,
|
|
850
|
+
type,
|
|
851
|
+
optional: false,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
// Handle getter declarations
|
|
855
|
+
if (ts.isGetAccessor(member) && member.name && ts.isIdentifier(member.name)) {
|
|
856
|
+
// Skip private and protected getters
|
|
857
|
+
if (member.modifiers) {
|
|
858
|
+
const hasPrivateOrProtected = member.modifiers.some(m => m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
859
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
860
|
+
if (hasPrivateOrProtected) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const name = member.name.text;
|
|
865
|
+
// First try explicit type annotation, then infer from getter body
|
|
866
|
+
let type = this.inferType(member.type);
|
|
867
|
+
if (type === 'unknown' && member.body) {
|
|
868
|
+
type = this.inferReturnTypeFromBody(member.body);
|
|
869
|
+
}
|
|
870
|
+
properties.set(name, {
|
|
871
|
+
name,
|
|
872
|
+
type,
|
|
873
|
+
optional: false,
|
|
874
|
+
readonly: true,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return properties;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Extract class property types for method return type inference
|
|
882
|
+
* Maps property names to their TypeScript type strings
|
|
883
|
+
* Note: Includes private/protected properties for method inference
|
|
884
|
+
*/
|
|
885
|
+
extractClassPropertyTypes(classDecl) {
|
|
886
|
+
const propertyTypes = new Map();
|
|
887
|
+
for (const member of classDecl.members) {
|
|
888
|
+
if (ts.isPropertyDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
|
|
889
|
+
// Include ALL properties (even private) for method return type inference
|
|
890
|
+
// The visibility check is done when adding to output properties
|
|
891
|
+
const name = member.name.text;
|
|
892
|
+
if (member.type) {
|
|
893
|
+
propertyTypes.set(name, this.inferType(member.type));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return propertyTypes;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Infer method type from method declaration
|
|
901
|
+
*/
|
|
902
|
+
inferMethodType(method, classPropertyTypes) {
|
|
903
|
+
const params = method.parameters.map(param => {
|
|
904
|
+
const paramName = ts.isIdentifier(param.name) ? param.name.text : 'arg';
|
|
905
|
+
const paramType = this.inferType(param.type);
|
|
906
|
+
return `${paramName}: ${paramType}`;
|
|
907
|
+
}).join(', ');
|
|
908
|
+
const returnType = this.inferReturnType(method, classPropertyTypes);
|
|
909
|
+
if (params) {
|
|
910
|
+
return `(${params}) => ${returnType}`;
|
|
911
|
+
}
|
|
912
|
+
return `() => ${returnType}`;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Extract properties from `this` parameter type annotation
|
|
916
|
+
* Handles patterns like: function(this: SomeType, data: SomeType) { }
|
|
917
|
+
*/
|
|
918
|
+
extractThisParamProperties(handlerArg) {
|
|
919
|
+
const properties = new Map();
|
|
920
|
+
// Find the `this` parameter (if any)
|
|
921
|
+
for (const param of handlerArg.parameters) {
|
|
922
|
+
if (param.name && ts.isIdentifier(param.name) && param.name.text === 'this' && param.type) {
|
|
923
|
+
// Check if it's a type reference (e.g., `this: usage`)
|
|
924
|
+
if (ts.isTypeReferenceNode(param.type)) {
|
|
925
|
+
const typeName = ts.isIdentifier(param.type.typeName)
|
|
926
|
+
? param.type.typeName.text
|
|
927
|
+
: '';
|
|
928
|
+
// Look up the type alias in our collected type aliases
|
|
929
|
+
const aliasedType = this.typeAliases.get(typeName);
|
|
930
|
+
if (aliasedType && ts.isTypeLiteralNode(aliasedType)) {
|
|
931
|
+
// Extract properties from the type literal
|
|
932
|
+
for (const member of aliasedType.members) {
|
|
933
|
+
if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
|
|
934
|
+
const propName = member.name.text;
|
|
935
|
+
const type = this.inferType(member.type);
|
|
936
|
+
properties.set(propName, {
|
|
937
|
+
name: propName,
|
|
938
|
+
type,
|
|
939
|
+
optional: !!member.questionToken,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Check if it's directly an inline type literal (e.g., `this: { id: string }`)
|
|
946
|
+
else if (ts.isTypeLiteralNode(param.type)) {
|
|
947
|
+
for (const member of param.type.members) {
|
|
948
|
+
if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
|
|
949
|
+
const propName = member.name.text;
|
|
950
|
+
const type = this.inferType(member.type);
|
|
951
|
+
properties.set(propName, {
|
|
952
|
+
name: propName,
|
|
953
|
+
type,
|
|
954
|
+
optional: !!member.questionToken,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
break; // Found the `this` parameter, no need to continue
|
|
960
|
+
}
|
|
408
961
|
}
|
|
409
962
|
return properties;
|
|
410
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Infer TypeScript type from type node
|
|
966
|
+
*/
|
|
411
967
|
/**
|
|
412
968
|
* Infer TypeScript type from type node
|
|
413
969
|
*/
|
|
@@ -434,17 +990,207 @@ class MnemonicaAnalyzer {
|
|
|
434
990
|
return 'void';
|
|
435
991
|
case ts.SyntaxKind.ArrayType:
|
|
436
992
|
return 'Array<' + this.inferType(typeNode.elementType) + '>';
|
|
437
|
-
case ts.SyntaxKind.TypeLiteral:
|
|
438
|
-
|
|
993
|
+
case ts.SyntaxKind.TypeLiteral: {
|
|
994
|
+
// Inline-expand type literals instead of collapsing to 'object'
|
|
995
|
+
const typeLit = typeNode;
|
|
996
|
+
const props = [];
|
|
997
|
+
for (const member of typeLit.members) {
|
|
998
|
+
if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
|
|
999
|
+
const propName = member.name.text;
|
|
1000
|
+
const optional = member.questionToken ? '?' : '';
|
|
1001
|
+
const type = this.inferType(member.type);
|
|
1002
|
+
props.push(`${propName}${optional}: ${type}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return `{ ${props.join('; ')} }`;
|
|
1006
|
+
}
|
|
1007
|
+
case ts.SyntaxKind.LiteralType: {
|
|
1008
|
+
// Handle string literal types like 'user', 'admin', etc.
|
|
1009
|
+
const literal = typeNode.literal;
|
|
1010
|
+
if (ts.isStringLiteral(literal)) {
|
|
1011
|
+
// Return the actual literal value (e.g., 'user' instead of string)
|
|
1012
|
+
return `'${literal.text}'`;
|
|
1013
|
+
}
|
|
1014
|
+
if (ts.isNumericLiteral(literal)) {
|
|
1015
|
+
return literal.text;
|
|
1016
|
+
}
|
|
1017
|
+
if (literal.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1018
|
+
return 'true';
|
|
1019
|
+
}
|
|
1020
|
+
if (literal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1021
|
+
return 'false';
|
|
1022
|
+
}
|
|
1023
|
+
if (literal.kind === ts.SyntaxKind.NullKeyword) {
|
|
1024
|
+
return 'null';
|
|
1025
|
+
}
|
|
1026
|
+
return 'unknown';
|
|
1027
|
+
}
|
|
1028
|
+
case ts.SyntaxKind.TypeReference: {
|
|
1029
|
+
// Handle type references like Map<string, number>, PropertyInfo, etc.
|
|
1030
|
+
const typeRef = typeNode;
|
|
1031
|
+
const typeName = ts.isIdentifier(typeRef.typeName)
|
|
1032
|
+
? typeRef.typeName.text
|
|
1033
|
+
: ts.isQualifiedName(typeRef.typeName)
|
|
1034
|
+
? this.getQualifiedNameText(typeRef.typeName)
|
|
1035
|
+
: 'unknown';
|
|
1036
|
+
// Check if this is a type alias we can resolve
|
|
1037
|
+
const aliasedType = this.typeAliases.get(typeName);
|
|
1038
|
+
if (aliasedType) {
|
|
1039
|
+
// Resolve the type alias
|
|
1040
|
+
return this.inferType(aliasedType);
|
|
1041
|
+
}
|
|
1042
|
+
// Handle InstanceType<typeof X> pattern -> convert to Parent_X
|
|
1043
|
+
if (typeName === 'InstanceType' && typeRef.typeArguments && typeRef.typeArguments.length === 1) {
|
|
1044
|
+
const arg = typeRef.typeArguments[0];
|
|
1045
|
+
if (arg.kind === ts.SyntaxKind.TypeQuery) {
|
|
1046
|
+
const typeQuery = arg;
|
|
1047
|
+
if (ts.isIdentifier(typeQuery.exprName)) {
|
|
1048
|
+
const typeName = typeQuery.exprName.text;
|
|
1049
|
+
// Look up the type in the graph to get full path
|
|
1050
|
+
const typeNode = this.graph.findTypeByName(typeName);
|
|
1051
|
+
if (typeNode) {
|
|
1052
|
+
// Convert full path with dots to underscores: Usages.UsageEntry -> Usages_UsageEntry
|
|
1053
|
+
return typeNode.fullPath.replace(/\./g, '_');
|
|
1054
|
+
}
|
|
1055
|
+
// Fallback: just use the type name if not found in graph
|
|
1056
|
+
return typeName;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (!typeRef.typeArguments || typeRef.typeArguments.length === 0) {
|
|
1061
|
+
// Check if this type exists in our graph - convert to full path format
|
|
1062
|
+
const typeNode = this.graph.findTypeByName(typeName);
|
|
1063
|
+
if (typeNode) {
|
|
1064
|
+
// Convert full path with dots to underscores: Usages.UsageEntry -> Usages_UsageEntry
|
|
1065
|
+
return typeNode.fullPath.replace(/\./g, "_");
|
|
1066
|
+
}
|
|
1067
|
+
return typeName;
|
|
1068
|
+
}
|
|
1069
|
+
// Build generic type arguments
|
|
1070
|
+
const args = typeRef.typeArguments.map(arg => this.inferType(arg));
|
|
1071
|
+
return `${typeName}<${args.join(', ')}>`;
|
|
1072
|
+
}
|
|
1073
|
+
case ts.SyntaxKind.UnionType: {
|
|
1074
|
+
// Handle union types like 'a' | 'b' | 'c'
|
|
1075
|
+
const unionType = typeNode;
|
|
1076
|
+
const types = unionType.types.map(t => this.inferType(t));
|
|
1077
|
+
return types.join(' | ');
|
|
1078
|
+
}
|
|
1079
|
+
case ts.SyntaxKind.IntersectionType: {
|
|
1080
|
+
// Handle intersection types like TypeA & TypeB
|
|
1081
|
+
const intersectionType = typeNode;
|
|
1082
|
+
const types = intersectionType.types.map(t => this.inferType(t));
|
|
1083
|
+
return types.join(' & ');
|
|
1084
|
+
}
|
|
1085
|
+
case ts.SyntaxKind.TupleType: {
|
|
1086
|
+
// Handle tuple types like [string, number]
|
|
1087
|
+
const tupleType = typeNode;
|
|
1088
|
+
const elements = tupleType.elements.map(elem => this.inferType(elem));
|
|
1089
|
+
return `[${elements.join(', ')}]`;
|
|
1090
|
+
}
|
|
1091
|
+
case ts.SyntaxKind.OptionalType: {
|
|
1092
|
+
// Handle optional element in tuple: string?
|
|
1093
|
+
const optionalType = typeNode;
|
|
1094
|
+
return this.inferType(optionalType.type) + '?';
|
|
1095
|
+
}
|
|
1096
|
+
case ts.SyntaxKind.RestType: {
|
|
1097
|
+
// Handle rest element: ...T
|
|
1098
|
+
const restType = typeNode;
|
|
1099
|
+
return '...' + this.inferType(restType.type);
|
|
1100
|
+
}
|
|
1101
|
+
case ts.SyntaxKind.ParenthesizedType: {
|
|
1102
|
+
// Handle parenthesized types: (A | B)
|
|
1103
|
+
return this.inferType(typeNode.type);
|
|
1104
|
+
}
|
|
1105
|
+
case ts.SyntaxKind.IndexedAccessType: {
|
|
1106
|
+
// Handle indexed access: T[K]
|
|
1107
|
+
const indexed = typeNode;
|
|
1108
|
+
let objectType = this.inferType(indexed.objectType);
|
|
1109
|
+
const indexType = this.inferType(indexed.indexType);
|
|
1110
|
+
// If objectType is 'object', try to resolve the underlying type alias
|
|
1111
|
+
if (objectType === 'object' && ts.isTypeReferenceNode(indexed.objectType)) {
|
|
1112
|
+
const refName = ts.isIdentifier(indexed.objectType.typeName) ? indexed.objectType.typeName.text : '';
|
|
1113
|
+
const aliased = this.typeAliases.get(refName);
|
|
1114
|
+
if (aliased) {
|
|
1115
|
+
objectType = this.inferType(aliased);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return `${objectType}[${indexType}]`;
|
|
1119
|
+
}
|
|
1120
|
+
case ts.SyntaxKind.TypeOperator: {
|
|
1121
|
+
// Handle keyof, readonly, unique operators
|
|
1122
|
+
const typeOp = typeNode;
|
|
1123
|
+
const operator = ts.SyntaxKind[typeOp.operator];
|
|
1124
|
+
return `${operator} ${this.inferType(typeOp.type)}`;
|
|
1125
|
+
}
|
|
1126
|
+
case ts.SyntaxKind.TypeQuery: {
|
|
1127
|
+
// Handle typeof expressions like `typeof UsageEntry`
|
|
1128
|
+
const typeQuery = typeNode;
|
|
1129
|
+
if (ts.isIdentifier(typeQuery.exprName)) {
|
|
1130
|
+
return `typeof ${typeQuery.exprName.text}`;
|
|
1131
|
+
}
|
|
1132
|
+
return 'unknown';
|
|
1133
|
+
}
|
|
439
1134
|
default:
|
|
440
1135
|
// For complex types, return the text representation
|
|
441
1136
|
return 'unknown';
|
|
442
1137
|
}
|
|
443
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Infer return type from a method declaration
|
|
1141
|
+
* Uses explicit return type annotation or infers from return statements
|
|
1142
|
+
*/
|
|
1143
|
+
inferReturnType(method, classPropertyTypes) {
|
|
1144
|
+
// If method has explicit return type annotation, use it
|
|
1145
|
+
if (method.type) {
|
|
1146
|
+
return this.inferType(method.type);
|
|
1147
|
+
}
|
|
1148
|
+
// Otherwise, try to infer from return statements in the method body
|
|
1149
|
+
if (method.body) {
|
|
1150
|
+
return this.inferReturnTypeFromBody(method.body, classPropertyTypes);
|
|
1151
|
+
}
|
|
1152
|
+
return 'unknown';
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Infer return type by analyzing return statements in the method body
|
|
1156
|
+
*/
|
|
1157
|
+
inferReturnTypeFromBody(body, classPropertyTypes) {
|
|
1158
|
+
const returnTypes = new Set();
|
|
1159
|
+
const visit = (node) => {
|
|
1160
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
1161
|
+
const type = this.inferTypeFromInitializer(node.expression, undefined, classPropertyTypes);
|
|
1162
|
+
if (type !== 'unknown') {
|
|
1163
|
+
returnTypes.add(type);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
ts.forEachChild(node, visit);
|
|
1167
|
+
};
|
|
1168
|
+
visit(body);
|
|
1169
|
+
if (returnTypes.size === 0) {
|
|
1170
|
+
return 'void';
|
|
1171
|
+
}
|
|
1172
|
+
if (returnTypes.size === 1) {
|
|
1173
|
+
return Array.from(returnTypes)[0];
|
|
1174
|
+
}
|
|
1175
|
+
return Array.from(returnTypes).join(' | ');
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Get full text from a qualified name (e.g., Namespace.Type)
|
|
1179
|
+
*/
|
|
1180
|
+
getQualifiedNameText(qualifiedName) {
|
|
1181
|
+
const parts = [];
|
|
1182
|
+
let current = qualifiedName;
|
|
1183
|
+
while (ts.isQualifiedName(current)) {
|
|
1184
|
+
parts.unshift(current.right.text);
|
|
1185
|
+
current = current.left;
|
|
1186
|
+
}
|
|
1187
|
+
parts.unshift(current.text);
|
|
1188
|
+
return parts.join('.');
|
|
1189
|
+
}
|
|
444
1190
|
/**
|
|
445
1191
|
* Infer type from initializer
|
|
446
1192
|
*/
|
|
447
|
-
inferTypeFromInitializer(initializer) {
|
|
1193
|
+
inferTypeFromInitializer(initializer, dataTypeMap, classPropertyTypes) {
|
|
448
1194
|
switch (initializer.kind) {
|
|
449
1195
|
case ts.SyntaxKind.StringLiteral:
|
|
450
1196
|
return 'string';
|
|
@@ -469,10 +1215,829 @@ class MnemonicaAnalyzer {
|
|
|
469
1215
|
}
|
|
470
1216
|
return 'object';
|
|
471
1217
|
}
|
|
1218
|
+
case ts.SyntaxKind.BinaryExpression: {
|
|
1219
|
+
// Handle arithmetic operations: a * b, a + b, a - b, a / b
|
|
1220
|
+
const binaryExpr = initializer;
|
|
1221
|
+
const leftType = this.inferTypeFromInitializer(binaryExpr.left, dataTypeMap, classPropertyTypes);
|
|
1222
|
+
const rightType = this.inferTypeFromInitializer(binaryExpr.right, dataTypeMap, classPropertyTypes);
|
|
1223
|
+
// Check if it's an arithmetic operator
|
|
1224
|
+
const operator = binaryExpr.operatorToken.kind;
|
|
1225
|
+
if (operator === ts.SyntaxKind.AsteriskToken ||
|
|
1226
|
+
operator === ts.SyntaxKind.SlashToken ||
|
|
1227
|
+
operator === ts.SyntaxKind.MinusToken ||
|
|
1228
|
+
operator === ts.SyntaxKind.PercentToken) {
|
|
1229
|
+
// Arithmetic operations on numbers produce numbers
|
|
1230
|
+
if ((leftType === 'number' || leftType === 'unknown') &&
|
|
1231
|
+
(rightType === 'number' || rightType === 'unknown')) {
|
|
1232
|
+
return 'number';
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (operator === ts.SyntaxKind.PlusToken) {
|
|
1236
|
+
// Plus can be addition or string concatenation
|
|
1237
|
+
if (leftType === 'string' || rightType === 'string') {
|
|
1238
|
+
return 'string';
|
|
1239
|
+
}
|
|
1240
|
+
if (leftType === 'number' && rightType === 'number') {
|
|
1241
|
+
return 'number';
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return 'unknown';
|
|
1245
|
+
}
|
|
1246
|
+
case ts.SyntaxKind.PropertyAccessExpression: {
|
|
1247
|
+
// Handle property access like data.value, data.id
|
|
1248
|
+
if (dataTypeMap) {
|
|
1249
|
+
const accessChain = this.getPropertyAccessChain(initializer);
|
|
1250
|
+
if (accessChain) {
|
|
1251
|
+
const type = dataTypeMap.get(accessChain);
|
|
1252
|
+
if (type) {
|
|
1253
|
+
return type;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
// Handle this.map.size pattern (Map.size returns number)
|
|
1258
|
+
const propAccess = initializer;
|
|
1259
|
+
if (ts.isPropertyAccessExpression(propAccess.expression)) {
|
|
1260
|
+
const outerProp = propAccess.expression;
|
|
1261
|
+
// Check for this.map pattern
|
|
1262
|
+
let innerName = '';
|
|
1263
|
+
if (outerProp.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
1264
|
+
innerName = 'this';
|
|
1265
|
+
}
|
|
1266
|
+
else if (ts.isIdentifier(outerProp.expression)) {
|
|
1267
|
+
innerName = outerProp.expression.text;
|
|
1268
|
+
}
|
|
1269
|
+
const mapProp = outerProp.name.text;
|
|
1270
|
+
const finalProp = propAccess.name.text;
|
|
1271
|
+
// this.map.size -> number
|
|
1272
|
+
if (innerName === 'this' && mapProp === 'map' && finalProp === 'size') {
|
|
1273
|
+
return 'number';
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return 'unknown';
|
|
1277
|
+
}
|
|
1278
|
+
case ts.SyntaxKind.Identifier: {
|
|
1279
|
+
// Handle identifier references if in dataTypeMap
|
|
1280
|
+
if (dataTypeMap) {
|
|
1281
|
+
const name = initializer.text;
|
|
1282
|
+
const type = dataTypeMap.get(name);
|
|
1283
|
+
if (type) {
|
|
1284
|
+
return type;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return 'unknown';
|
|
1288
|
+
}
|
|
1289
|
+
case ts.SyntaxKind.CallExpression: {
|
|
1290
|
+
// Handle function calls like Date.now(), parseInt(), etc.
|
|
1291
|
+
const callExpr = initializer;
|
|
1292
|
+
if (ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
1293
|
+
const methodName = callExpr.expression.name.text;
|
|
1294
|
+
const objName = ts.isIdentifier(callExpr.expression.expression)
|
|
1295
|
+
? callExpr.expression.expression.text
|
|
1296
|
+
: '';
|
|
1297
|
+
// Date.now() -> number
|
|
1298
|
+
if (objName === 'Date' && methodName === 'now') {
|
|
1299
|
+
return 'number';
|
|
1300
|
+
}
|
|
1301
|
+
// String methods that return string
|
|
1302
|
+
if (methodName === 'toString' || methodName === 'valueOf') {
|
|
1303
|
+
return 'string';
|
|
1304
|
+
}
|
|
1305
|
+
// Handle Map property access on class instances (this.map.*)
|
|
1306
|
+
if (ts.isPropertyAccessExpression(callExpr.expression.expression)) {
|
|
1307
|
+
const outerProp = callExpr.expression.expression;
|
|
1308
|
+
// Handle both 'this' keyword and identifier patterns
|
|
1309
|
+
let innerName = '';
|
|
1310
|
+
if (outerProp.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
1311
|
+
innerName = 'this';
|
|
1312
|
+
}
|
|
1313
|
+
else if (ts.isIdentifier(outerProp.expression)) {
|
|
1314
|
+
innerName = outerProp.expression.text;
|
|
1315
|
+
}
|
|
1316
|
+
const mapProp = outerProp.name.text;
|
|
1317
|
+
// this.map.X() patterns
|
|
1318
|
+
if (innerName === 'this' && mapProp === 'map') {
|
|
1319
|
+
// Try to get the Map's value type from class properties
|
|
1320
|
+
let mapValueType = 'unknown';
|
|
1321
|
+
if (classPropertyTypes) {
|
|
1322
|
+
const mapType = classPropertyTypes.get('map');
|
|
1323
|
+
if (mapType && mapType.startsWith('Map<')) {
|
|
1324
|
+
// Parse Map<K, V> to get V
|
|
1325
|
+
const match = mapType.match(/Map<[^,]+,\s*(.+)>$/);
|
|
1326
|
+
if (match) {
|
|
1327
|
+
mapValueType = match[1];
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (methodName === 'has')
|
|
1332
|
+
return 'boolean';
|
|
1333
|
+
if (methodName === 'set')
|
|
1334
|
+
return 'this';
|
|
1335
|
+
if (methodName === 'get')
|
|
1336
|
+
return mapValueType;
|
|
1337
|
+
if (methodName === 'delete')
|
|
1338
|
+
return 'boolean';
|
|
1339
|
+
if (methodName === 'clear')
|
|
1340
|
+
return 'void';
|
|
1341
|
+
if (methodName === 'values')
|
|
1342
|
+
return `IterableIterator<${mapValueType}>`;
|
|
1343
|
+
if (methodName === 'keys')
|
|
1344
|
+
return 'IterableIterator<string>';
|
|
1345
|
+
if (methodName === 'entries')
|
|
1346
|
+
return `IterableIterator<[string, ${mapValueType}]>`;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
// Direct map.X() calls
|
|
1350
|
+
if (objName === 'map' || objName === 'obj') {
|
|
1351
|
+
if (methodName === 'has')
|
|
1352
|
+
return 'boolean';
|
|
1353
|
+
if (methodName === 'set')
|
|
1354
|
+
return 'this';
|
|
1355
|
+
if (methodName === 'get')
|
|
1356
|
+
return 'unknown';
|
|
1357
|
+
if (methodName === 'delete')
|
|
1358
|
+
return 'boolean';
|
|
1359
|
+
if (methodName === 'clear')
|
|
1360
|
+
return 'void';
|
|
1361
|
+
if (methodName === 'values')
|
|
1362
|
+
return 'IterableIterator<unknown>';
|
|
1363
|
+
if (methodName === 'keys')
|
|
1364
|
+
return 'IterableIterator<string>';
|
|
1365
|
+
if (methodName === 'entries')
|
|
1366
|
+
return 'IterableIterator<[string, unknown]>';
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// parseInt, parseFloat -> number
|
|
1370
|
+
if (ts.isIdentifier(callExpr.expression)) {
|
|
1371
|
+
const fnName = callExpr.expression.text;
|
|
1372
|
+
if (fnName === 'parseInt' || fnName === 'parseFloat') {
|
|
1373
|
+
return 'number';
|
|
1374
|
+
}
|
|
1375
|
+
if (fnName === 'String') {
|
|
1376
|
+
return 'string';
|
|
1377
|
+
}
|
|
1378
|
+
if (fnName === 'Number') {
|
|
1379
|
+
return 'number';
|
|
1380
|
+
}
|
|
1381
|
+
if (fnName === 'Boolean') {
|
|
1382
|
+
return 'boolean';
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return 'unknown';
|
|
1386
|
+
}
|
|
1387
|
+
case ts.SyntaxKind.TemplateExpression:
|
|
1388
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral: {
|
|
1389
|
+
// Template literals like `${baseValue}-${extra}` always produce strings
|
|
1390
|
+
return 'string';
|
|
1391
|
+
}
|
|
472
1392
|
default:
|
|
473
1393
|
return 'unknown';
|
|
474
1394
|
}
|
|
475
1395
|
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Collect usage information for type references
|
|
1398
|
+
*/
|
|
1399
|
+
collectUsage(node, sourceFile) {
|
|
1400
|
+
// Check for new Type() instantiation
|
|
1401
|
+
if (ts.isNewExpression(node) && node.expression) {
|
|
1402
|
+
let typeName;
|
|
1403
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
1404
|
+
typeName = this.resolveTypePath(node.expression);
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
typeName = this.getTypeNameFromExpression(node.expression);
|
|
1408
|
+
}
|
|
1409
|
+
if (typeName) {
|
|
1410
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1411
|
+
this.addUsage(typeName, {
|
|
1412
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
1413
|
+
kind: 'instantiation',
|
|
1414
|
+
code: node.getText(sourceFile).slice(0, 100),
|
|
1415
|
+
});
|
|
1416
|
+
// Track variable assignment from new Type() for flow analysis
|
|
1417
|
+
this.trackNewAssignment(node, typeName);
|
|
1418
|
+
// Also record as flow event
|
|
1419
|
+
this.addFlow(typeName, {
|
|
1420
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
1421
|
+
kind: 'instantiation',
|
|
1422
|
+
code: node.getText(sourceFile).slice(0, 100),
|
|
1423
|
+
context: 'new expression',
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// Check for property access on instances (user.AdminType)
|
|
1428
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1429
|
+
const propName = node.name.text;
|
|
1430
|
+
// Check if this looks like a type access pattern
|
|
1431
|
+
if (propName && this.isLikelyTypeName(propName)) {
|
|
1432
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1433
|
+
// Try to resolve full path
|
|
1434
|
+
const fullPath = this.resolveTypePath(node);
|
|
1435
|
+
if (fullPath) {
|
|
1436
|
+
this.addUsage(fullPath, {
|
|
1437
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
1438
|
+
kind: 'propertyAccess',
|
|
1439
|
+
code: node.getText(sourceFile).slice(0, 100),
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
// Check for lookup('TypeName') calls
|
|
1445
|
+
if (ts.isCallExpression(node) && node.expression) {
|
|
1446
|
+
const funcName = this.getFunctionName(node.expression);
|
|
1447
|
+
if (funcName === 'lookup' && node.arguments.length > 0) {
|
|
1448
|
+
const firstArg = node.arguments[0];
|
|
1449
|
+
if (ts.isStringLiteral(firstArg) || ts.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
1450
|
+
const typePath = firstArg.text;
|
|
1451
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1452
|
+
this.addUsage(typePath, {
|
|
1453
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
1454
|
+
kind: 'lookup',
|
|
1455
|
+
code: node.getText(sourceFile).slice(0, 100),
|
|
1456
|
+
});
|
|
1457
|
+
// Track variable assignment from lookup for instantiation tracking
|
|
1458
|
+
this.trackLookupAssignment(node, typePath);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Get function name from expression (identifier or property access)
|
|
1465
|
+
*/
|
|
1466
|
+
getFunctionName(expr) {
|
|
1467
|
+
if (ts.isIdentifier(expr)) {
|
|
1468
|
+
return expr.text;
|
|
1469
|
+
}
|
|
1470
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
1471
|
+
return expr.name.text;
|
|
1472
|
+
}
|
|
1473
|
+
return undefined;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Add a usage to the collection
|
|
1477
|
+
*/
|
|
1478
|
+
addUsage(typePath, usage) {
|
|
1479
|
+
// Only track usages of mnemonica-defined types
|
|
1480
|
+
if (!this.definitions.has(typePath)) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (!this.usages.has(typePath)) {
|
|
1484
|
+
this.usages.set(typePath, []);
|
|
1485
|
+
}
|
|
1486
|
+
// Check for duplicates based on location, code, and kind
|
|
1487
|
+
const existingUsages = this.usages.get(typePath);
|
|
1488
|
+
const isDuplicate = existingUsages.some(existing => existing.location === usage.location &&
|
|
1489
|
+
existing.code === usage.code &&
|
|
1490
|
+
existing.kind === usage.kind);
|
|
1491
|
+
if (!isDuplicate) {
|
|
1492
|
+
existingUsages.push(usage);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Collect EDS (Execution Data Storage) usage information
|
|
1497
|
+
*/
|
|
1498
|
+
collectEDS(node, sourceFile) {
|
|
1499
|
+
if (!ts.isCallExpression(node) || !node.expression) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const funcName = this.getFunctionName(node.expression);
|
|
1503
|
+
if (!funcName) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1507
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1508
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1509
|
+
// wrap(fn), wrapArgs(fn), wrapInstanceMethods(obj)
|
|
1510
|
+
if (funcName === 'wrap' || funcName === 'wrapArgs' || funcName === 'wrapInstanceMethods') {
|
|
1511
|
+
const targetType = this.resolveEDSArgumentType(node.arguments[0]);
|
|
1512
|
+
this.addEDS(targetType || 'unknown', {
|
|
1513
|
+
location,
|
|
1514
|
+
kind: 'wrap',
|
|
1515
|
+
code,
|
|
1516
|
+
targetType: targetType || undefined,
|
|
1517
|
+
});
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
// link(parent, child), runWithInstance(instance, fn)
|
|
1521
|
+
if (funcName === 'link' || funcName === 'runWithInstance') {
|
|
1522
|
+
const targetType = this.resolveEDSArgumentType(node.arguments[0]);
|
|
1523
|
+
this.addEDS(targetType || 'unknown', {
|
|
1524
|
+
location,
|
|
1525
|
+
kind: 'link',
|
|
1526
|
+
code,
|
|
1527
|
+
targetType: targetType || undefined,
|
|
1528
|
+
});
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
// getLastContext(), getErrorInstance(err)
|
|
1532
|
+
if (funcName === 'getLastContext' || funcName === 'getErrorInstance') {
|
|
1533
|
+
this.addEDS('unknown', {
|
|
1534
|
+
location,
|
|
1535
|
+
kind: 'contextConsume',
|
|
1536
|
+
code,
|
|
1537
|
+
});
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
// enrichError(err, instance)
|
|
1541
|
+
if (funcName === 'enrichError') {
|
|
1542
|
+
const targetType = node.arguments.length > 1
|
|
1543
|
+
? this.resolveEDSArgumentType(node.arguments[1])
|
|
1544
|
+
: undefined;
|
|
1545
|
+
this.addEDS(targetType || 'unknown', {
|
|
1546
|
+
location,
|
|
1547
|
+
kind: 'errorEnrich',
|
|
1548
|
+
code,
|
|
1549
|
+
targetType: targetType || undefined,
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
// attachHooks(types), attachHooks([A, B])
|
|
1554
|
+
if (funcName === 'attachHooks' && node.arguments.length > 0) {
|
|
1555
|
+
const arg = node.arguments[0];
|
|
1556
|
+
if (ts.isArrayLiteralExpression(arg)) {
|
|
1557
|
+
for (const element of arg.elements) {
|
|
1558
|
+
const targetType = this.resolveEDSArgumentType(element);
|
|
1559
|
+
this.addEDS(targetType || 'unknown', {
|
|
1560
|
+
location,
|
|
1561
|
+
kind: 'hookAttach',
|
|
1562
|
+
code,
|
|
1563
|
+
targetType: targetType || undefined,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
const targetType = this.resolveEDSArgumentType(arg);
|
|
1569
|
+
this.addEDS(targetType || 'unknown', {
|
|
1570
|
+
location,
|
|
1571
|
+
kind: 'hookAttach',
|
|
1572
|
+
code,
|
|
1573
|
+
targetType: targetType || undefined,
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
// Adapters: createDiveInterceptor, createDivePlugin, createDiveMiddleware
|
|
1579
|
+
if (funcName === 'createDiveInterceptor' || funcName === 'createDivePlugin' || funcName === 'createDiveMiddleware') {
|
|
1580
|
+
this.addEDS('unknown', {
|
|
1581
|
+
location,
|
|
1582
|
+
kind: 'adapterUse',
|
|
1583
|
+
code,
|
|
1584
|
+
});
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Resolve type from EDS call argument (best effort)
|
|
1590
|
+
*/
|
|
1591
|
+
resolveEDSArgumentType(arg) {
|
|
1592
|
+
if (!arg) {
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
// Identifier: variable name
|
|
1596
|
+
if (ts.isIdentifier(arg)) {
|
|
1597
|
+
const mapped = this.variableToTypeMap.get(arg.text);
|
|
1598
|
+
if (mapped) {
|
|
1599
|
+
return mapped;
|
|
1600
|
+
}
|
|
1601
|
+
// Maybe it's a type name directly
|
|
1602
|
+
if (this.definitions.has(arg.text)) {
|
|
1603
|
+
return arg.text;
|
|
1604
|
+
}
|
|
1605
|
+
return undefined;
|
|
1606
|
+
}
|
|
1607
|
+
// Property access: obj.prop
|
|
1608
|
+
if (ts.isPropertyAccessExpression(arg)) {
|
|
1609
|
+
return this.resolveTypePath(arg);
|
|
1610
|
+
}
|
|
1611
|
+
// This expression: this.something
|
|
1612
|
+
if (ts.isPropertyAccessExpression(arg) && ts.isIdentifier(arg.expression) && arg.expression.text === 'this') {
|
|
1613
|
+
return undefined;
|
|
1614
|
+
}
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Add an EDS usage to the collection
|
|
1619
|
+
*/
|
|
1620
|
+
addEDS(typePath, info) {
|
|
1621
|
+
if (!this.edsUsages.has(typePath)) {
|
|
1622
|
+
this.edsUsages.set(typePath, []);
|
|
1623
|
+
}
|
|
1624
|
+
const existing = this.edsUsages.get(typePath);
|
|
1625
|
+
const isDuplicate = existing.some(e => e.location === info.location && e.kind === info.kind && e.code === info.code);
|
|
1626
|
+
if (!isDuplicate) {
|
|
1627
|
+
existing.push(info);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Collect native flow patterns (instance usage after creation)
|
|
1632
|
+
* Phase 1: property access, method calls, arguments, return, destructuring, etc.
|
|
1633
|
+
*/
|
|
1634
|
+
collectFlow(node, sourceFile) {
|
|
1635
|
+
// Property read: user.name or user?.name
|
|
1636
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1637
|
+
this.collectFlowPropertyAccess(node, sourceFile);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
// Element access: user['name']
|
|
1641
|
+
if (ts.isElementAccessExpression(node)) {
|
|
1642
|
+
this.collectFlowElementAccess(node, sourceFile);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
// Property write: user.name = value
|
|
1646
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
1647
|
+
this.collectFlowAssignment(node, sourceFile);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
// Method call: user.validate() AND argument passing: processUser(user)
|
|
1651
|
+
if (ts.isCallExpression(node) && node.expression) {
|
|
1652
|
+
this.collectFlowMethodCall(node, sourceFile);
|
|
1653
|
+
this.collectFlowArgumentPass(node, sourceFile);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
// Destructure read: const { name } = user
|
|
1657
|
+
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
1658
|
+
this.collectFlowDestructure(node, sourceFile);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
// Return instance: return user
|
|
1662
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
1663
|
+
this.collectFlowReturn(node, sourceFile);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
// Spread: { ...user }
|
|
1667
|
+
if (ts.isSpreadElement(node)) {
|
|
1668
|
+
this.collectFlowSpread(node, sourceFile);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Collect property access flow (read or conditional)
|
|
1674
|
+
*/
|
|
1675
|
+
collectFlowPropertyAccess(node, sourceFile) {
|
|
1676
|
+
const objectType = this.resolveExpressionType(node.expression);
|
|
1677
|
+
if (!objectType) {
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
const propName = node.name.text;
|
|
1681
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1682
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1683
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1684
|
+
// Skip if this is a type constructor access (e.g., UserType.define)
|
|
1685
|
+
if (propName === 'define') {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
this.addFlow(objectType, {
|
|
1689
|
+
location,
|
|
1690
|
+
kind: 'propertyRead',
|
|
1691
|
+
code,
|
|
1692
|
+
propertyName: propName,
|
|
1693
|
+
targetType: objectType
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Collect element access flow: user['name']
|
|
1698
|
+
*/
|
|
1699
|
+
collectFlowElementAccess(node, sourceFile) {
|
|
1700
|
+
const objectType = this.resolveExpressionType(node.expression);
|
|
1701
|
+
if (!objectType) {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1705
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1706
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1707
|
+
this.addFlow(objectType, {
|
|
1708
|
+
location,
|
|
1709
|
+
kind: 'elementAccess',
|
|
1710
|
+
code,
|
|
1711
|
+
targetType: objectType
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Collect assignment flow: user.name = value or user = other
|
|
1716
|
+
*/
|
|
1717
|
+
collectFlowAssignment(node, sourceFile) {
|
|
1718
|
+
// Property write: user.name = value
|
|
1719
|
+
if (ts.isPropertyAccessExpression(node.left)) {
|
|
1720
|
+
const objectType = this.resolveExpressionType(node.left.expression);
|
|
1721
|
+
if (!objectType) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const propName = node.left.name.text;
|
|
1725
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1726
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1727
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1728
|
+
this.addFlow(objectType, {
|
|
1729
|
+
location,
|
|
1730
|
+
kind: 'propertyWrite',
|
|
1731
|
+
code,
|
|
1732
|
+
propertyName: propName,
|
|
1733
|
+
targetType: objectType
|
|
1734
|
+
});
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
// Variable reassignment: user = other
|
|
1738
|
+
if (ts.isIdentifier(node.left)) {
|
|
1739
|
+
const varName = node.left.text;
|
|
1740
|
+
const mappedType = this.variableToTypeMap.get(varName);
|
|
1741
|
+
if (!mappedType) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1745
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1746
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1747
|
+
this.addFlow(mappedType, {
|
|
1748
|
+
location,
|
|
1749
|
+
kind: 'reassignment',
|
|
1750
|
+
code,
|
|
1751
|
+
targetType: mappedType
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Collect method call flow: user.validate()
|
|
1757
|
+
*/
|
|
1758
|
+
collectFlowMethodCall(node, sourceFile) {
|
|
1759
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
const objectType = this.resolveExpressionType(node.expression.expression);
|
|
1763
|
+
if (!objectType) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const methodName = node.expression.name.text;
|
|
1767
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1768
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1769
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1770
|
+
// Skip if this is a type constructor call (e.g., new UserType())
|
|
1771
|
+
if (methodName === 'define') {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
this.addFlow(objectType, {
|
|
1775
|
+
location,
|
|
1776
|
+
kind: 'methodCall',
|
|
1777
|
+
code,
|
|
1778
|
+
propertyName: methodName,
|
|
1779
|
+
targetType: objectType
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Collect argument passing flow: processUser(user)
|
|
1784
|
+
*/
|
|
1785
|
+
collectFlowArgumentPass(node, sourceFile) {
|
|
1786
|
+
for (let i = 0; i < node.arguments.length; i++) {
|
|
1787
|
+
const arg = node.arguments[i];
|
|
1788
|
+
const argType = this.resolveExpressionType(arg);
|
|
1789
|
+
if (!argType) {
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const funcName = this.getFunctionName(node.expression) || 'anonymous';
|
|
1793
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1794
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1795
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1796
|
+
this.addFlow(argType, {
|
|
1797
|
+
location,
|
|
1798
|
+
kind: 'passAsArg',
|
|
1799
|
+
code,
|
|
1800
|
+
targetType: argType,
|
|
1801
|
+
context: `arg ${i} to ${funcName}`
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Collect destructuring flow: const { name } = user
|
|
1807
|
+
*/
|
|
1808
|
+
collectFlowDestructure(node, sourceFile) {
|
|
1809
|
+
if (!ts.isObjectBindingPattern(node.name)) {
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const sourceType = this.resolveExpressionType(node.initializer);
|
|
1813
|
+
if (!sourceType) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1817
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1818
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1819
|
+
// Extract destructured property names
|
|
1820
|
+
const props = [];
|
|
1821
|
+
for (const element of node.name.elements) {
|
|
1822
|
+
if (ts.isIdentifier(element.name)) {
|
|
1823
|
+
props.push(element.name.text);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
this.addFlow(sourceType, {
|
|
1827
|
+
location,
|
|
1828
|
+
kind: 'destructureRead',
|
|
1829
|
+
code,
|
|
1830
|
+
targetType: sourceType,
|
|
1831
|
+
context: props.join(', ')
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Collect return flow: return user
|
|
1836
|
+
*/
|
|
1837
|
+
collectFlowReturn(node, sourceFile) {
|
|
1838
|
+
const returnType = this.resolveExpressionType(node.expression);
|
|
1839
|
+
if (!returnType) {
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1843
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1844
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1845
|
+
this.addFlow(returnType, {
|
|
1846
|
+
location,
|
|
1847
|
+
kind: 'return',
|
|
1848
|
+
code,
|
|
1849
|
+
targetType: returnType
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Collect spread flow: { ...user }
|
|
1854
|
+
*/
|
|
1855
|
+
collectFlowSpread(node, sourceFile) {
|
|
1856
|
+
const spreadType = this.resolveExpressionType(node.expression);
|
|
1857
|
+
if (!spreadType) {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1861
|
+
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1862
|
+
const code = node.getText(sourceFile).slice(0, 100);
|
|
1863
|
+
this.addFlow(spreadType, {
|
|
1864
|
+
location,
|
|
1865
|
+
kind: 'spread',
|
|
1866
|
+
code,
|
|
1867
|
+
targetType: spreadType
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Resolve type from an expression (identifier, property access, etc.)
|
|
1872
|
+
*/
|
|
1873
|
+
resolveExpressionType(expr) {
|
|
1874
|
+
// Identifier: user
|
|
1875
|
+
if (ts.isIdentifier(expr)) {
|
|
1876
|
+
return this.variableToTypeMap.get(expr.text);
|
|
1877
|
+
}
|
|
1878
|
+
// Property access: user.name (return object type, not property type)
|
|
1879
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
1880
|
+
return this.resolveExpressionType(expr.expression);
|
|
1881
|
+
}
|
|
1882
|
+
// Element access: user['name']
|
|
1883
|
+
if (ts.isElementAccessExpression(expr)) {
|
|
1884
|
+
return this.resolveExpressionType(expr.expression);
|
|
1885
|
+
}
|
|
1886
|
+
// This expression: this (if in a method, we can't resolve without more context)
|
|
1887
|
+
if (expr.kind === ts.SyntaxKind.ThisKeyword) {
|
|
1888
|
+
return undefined;
|
|
1889
|
+
}
|
|
1890
|
+
return undefined;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Add a flow usage to the collection
|
|
1894
|
+
*/
|
|
1895
|
+
addFlow(typePath, info) {
|
|
1896
|
+
if (!this.flowUsages.has(typePath)) {
|
|
1897
|
+
this.flowUsages.set(typePath, []);
|
|
1898
|
+
}
|
|
1899
|
+
const existing = this.flowUsages.get(typePath);
|
|
1900
|
+
const isDuplicate = existing.some(e => e.location === info.location && e.kind === info.kind && e.code === info.code);
|
|
1901
|
+
if (!isDuplicate) {
|
|
1902
|
+
existing.push(info);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Get type name from expression (identifier or property access)
|
|
1907
|
+
*/
|
|
1908
|
+
getTypeNameFromExpression(expr) {
|
|
1909
|
+
if (ts.isIdentifier(expr)) {
|
|
1910
|
+
const name = expr.text;
|
|
1911
|
+
// Check if this identifier is a variable mapped to a type (e.g., from lookup)
|
|
1912
|
+
const mappedType = this.variableToTypeMap.get(name);
|
|
1913
|
+
if (mappedType) {
|
|
1914
|
+
return mappedType;
|
|
1915
|
+
}
|
|
1916
|
+
return name;
|
|
1917
|
+
}
|
|
1918
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
1919
|
+
const chain = this.getPropertyChain(expr);
|
|
1920
|
+
return chain.join('.');
|
|
1921
|
+
}
|
|
1922
|
+
return undefined;
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Resolve full type path from property access
|
|
1926
|
+
*/
|
|
1927
|
+
resolveTypePath(expr) {
|
|
1928
|
+
const chain = this.getPropertyChain(expr);
|
|
1929
|
+
if (chain.length === 0)
|
|
1930
|
+
return undefined;
|
|
1931
|
+
// Check if this chain matches a known type
|
|
1932
|
+
const fullPath = chain.join('.');
|
|
1933
|
+
if (this.definitions.has(fullPath)) {
|
|
1934
|
+
return fullPath;
|
|
1935
|
+
}
|
|
1936
|
+
// Try just the property name
|
|
1937
|
+
const propName = chain[chain.length - 1];
|
|
1938
|
+
for (const [path] of this.definitions) {
|
|
1939
|
+
if (path.endsWith(`.${propName}`) || path === propName) {
|
|
1940
|
+
return path;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return fullPath;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Check if a name looks like a type (starts with uppercase)
|
|
1947
|
+
*/
|
|
1948
|
+
isLikelyTypeName(name) {
|
|
1949
|
+
return name[0] >= 'A' && name[0] <= 'Z';
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Extract constructor parameters from define() call
|
|
1953
|
+
* This is used for TypeRegistry constructor signatures
|
|
1954
|
+
* Preserves parameter names and expands object types to their structure
|
|
1955
|
+
*/
|
|
1956
|
+
extractConstructorParams(call) {
|
|
1957
|
+
const params = [];
|
|
1958
|
+
const handlerArg = call.arguments[1];
|
|
1959
|
+
if (!handlerArg)
|
|
1960
|
+
return params;
|
|
1961
|
+
// Helper to resolve type alias and extract properties
|
|
1962
|
+
const resolveTypeAndExtract = (typeNode) => {
|
|
1963
|
+
if (!typeNode)
|
|
1964
|
+
return undefined;
|
|
1965
|
+
// Direct inline type literal: { prop: type }
|
|
1966
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
1967
|
+
const props = [];
|
|
1968
|
+
for (const member of typeNode.members) {
|
|
1969
|
+
if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
|
|
1970
|
+
const propName = member.name.text;
|
|
1971
|
+
const optional = member.questionToken ? '?' : '';
|
|
1972
|
+
const type = this.inferType(member.type);
|
|
1973
|
+
props.push(`${propName}${optional}: ${type}`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return `{ ${props.join('; ')} }`;
|
|
1977
|
+
}
|
|
1978
|
+
// Type reference: usage, UserData, etc. - recursively expand
|
|
1979
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
1980
|
+
const typeName = typeNode.typeName.text;
|
|
1981
|
+
const aliasedType = this.typeAliases.get(typeName);
|
|
1982
|
+
if (aliasedType) {
|
|
1983
|
+
const expanded = resolveTypeAndExtract(aliasedType);
|
|
1984
|
+
if (expanded)
|
|
1985
|
+
return expanded;
|
|
1986
|
+
}
|
|
1987
|
+
// If not an object type alias, return the type name with args
|
|
1988
|
+
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
1989
|
+
const args = typeNode.typeArguments.map(arg => this.inferType(arg));
|
|
1990
|
+
return typeName + '<' + args.join(', ') + '>';
|
|
1991
|
+
}
|
|
1992
|
+
return typeName;
|
|
1993
|
+
}
|
|
1994
|
+
return undefined;
|
|
1995
|
+
};
|
|
1996
|
+
// Handle function expression or arrow function
|
|
1997
|
+
if (ts.isFunctionExpression(handlerArg) || ts.isArrowFunction(handlerArg)) {
|
|
1998
|
+
// Look for constructor parameters (second param after `this`)
|
|
1999
|
+
// Patterns: function(this: Type, data: { ... }) or (this: Type, data: { ... }) =>
|
|
2000
|
+
for (let i = 0; i < handlerArg.parameters.length; i++) {
|
|
2001
|
+
const param = handlerArg.parameters[i];
|
|
2002
|
+
if (!param.type)
|
|
2003
|
+
continue;
|
|
2004
|
+
// Skip `this` parameter (first param)
|
|
2005
|
+
if (i === 0 && param.name.kind === ts.SyntaxKind.Identifier && param.name.text === 'this') {
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
// Get parameter name and expand its type
|
|
2009
|
+
const paramName = ts.isIdentifier(param.name) ? param.name.text : 'arg';
|
|
2010
|
+
const expandedType = resolveTypeAndExtract(param.type) || this.inferType(param.type);
|
|
2011
|
+
params.push({
|
|
2012
|
+
name: paramName,
|
|
2013
|
+
type: expandedType,
|
|
2014
|
+
optional: !!param.questionToken || !!param.initializer
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
// Handle class expression - check constructor method
|
|
2019
|
+
if (ts.isClassExpression(handlerArg)) {
|
|
2020
|
+
for (const member of handlerArg.members) {
|
|
2021
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
2022
|
+
for (const param of member.parameters) {
|
|
2023
|
+
if (!param.name || !ts.isIdentifier(param.name))
|
|
2024
|
+
continue;
|
|
2025
|
+
if (param.type) {
|
|
2026
|
+
const paramName = param.name.text;
|
|
2027
|
+
const expandedType = resolveTypeAndExtract(param.type) || this.inferType(param.type);
|
|
2028
|
+
params.push({
|
|
2029
|
+
name: paramName,
|
|
2030
|
+
type: expandedType,
|
|
2031
|
+
optional: !!param.questionToken || !!param.initializer
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
break; // Only process first constructor
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return params;
|
|
2040
|
+
}
|
|
476
2041
|
}
|
|
477
2042
|
exports.MnemonicaAnalyzer = MnemonicaAnalyzer;
|
|
478
2043
|
//# sourceMappingURL=analyzer.js.map
|