@mnemonica/tactica 0.1.0 → 0.1.1

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/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
- * Check if a node is a @decorate() decorator
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
- const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, call.getStart(sourceFile));
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 lookupTyped() calls
350
+ * e.g., const SentienceConstructor = lookupTyped('Sentience') maps "SentienceConstructor" -> "Sentience"
351
+ */
352
+ trackLookupTypedAssignment(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 = lookupTyped(...)
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
- * Process a @decorate() decorator
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
- * Get property chain from nested access
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
- properties.set(name, {
352
- name,
353
- type: this.inferTypeFromInitializer(expr.right),
354
- optional: false,
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
- return 'object';
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 lookupTyped('TypeName') calls
1445
+ if (ts.isCallExpression(node) && node.expression) {
1446
+ const funcName = this.getFunctionName(node.expression);
1447
+ if (funcName === 'lookupTyped' && 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 lookupTyped for instantiation tracking
1458
+ this.trackLookupTypedAssignment(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 lookupTyped)
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