@mnemonica/tactica 0.1.4 → 0.1.7
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 +136 -2
- package/lib/analyzer.d.ts +167 -23
- package/lib/analyzer.js +953 -177
- package/lib/analyzer.js.map +1 -1
- package/lib/cli.js +3 -2
- package/lib/cli.js.map +1 -1
- package/lib/generator.d.ts +19 -7
- package/lib/generator.js +136 -20
- package/lib/generator.js.map +1 -1
- package/lib/graph.d.ts +8 -1
- package/lib/graph.js +16 -3
- package/lib/graph.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/topologica-analyzer.js +3 -3
- package/lib/topologica-analyzer.js.map +1 -1
- package/lib/types.d.ts +5 -1
- package/lib/writer.js.map +1 -1
- package/package.json +12 -5
package/lib/analyzer.js
CHANGED
|
@@ -50,6 +50,15 @@ class MnemonicaAnalyzer {
|
|
|
50
50
|
this.typeAliases = new Map();
|
|
51
51
|
// Track variable assignments: variableName -> fullPath of the type it holds
|
|
52
52
|
this.variableToTypeMap = new Map();
|
|
53
|
+
// Track mnemonica module-object variables (e.g., import { mnemonica } from 'mnemonica'; const m = mnemonica)
|
|
54
|
+
this.moduleObjectVariables = new Set();
|
|
55
|
+
// Track imported aliases of createTypesCollection (e.g., import { createTypesCollection as ctc })
|
|
56
|
+
this.createTypesCollectionVariables = new Set();
|
|
57
|
+
// Track custom collection variables: variableName -> collectionId
|
|
58
|
+
this.collectionVariables = new Map();
|
|
59
|
+
// Track custom collection metadata for Option B registry emission
|
|
60
|
+
this.collectionInfo = new Map();
|
|
61
|
+
this.collectionCounter = 0;
|
|
53
62
|
// Store program for future use (currently unused but kept for extensibility)
|
|
54
63
|
void program;
|
|
55
64
|
}
|
|
@@ -62,6 +71,8 @@ class MnemonicaAnalyzer {
|
|
|
62
71
|
this.edsUsages.clear();
|
|
63
72
|
this.flowUsages.clear();
|
|
64
73
|
this.variableToTypeMap.clear();
|
|
74
|
+
// Note: moduleObjectVariables and collectionVariables intentionally persist
|
|
75
|
+
// across definition and usage passes.
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* Analyze a source file for Mnemonica type definitions
|
|
@@ -158,10 +169,19 @@ class MnemonicaAnalyzer {
|
|
|
158
169
|
* Visit a node in the AST
|
|
159
170
|
*/
|
|
160
171
|
visitNode(node, sourceFile, currentClass) {
|
|
172
|
+
// Track mnemonica module-object aliases and custom collection variables
|
|
173
|
+
// before processing define()/lookup() calls so source resolution works.
|
|
174
|
+
this.trackImports(node);
|
|
175
|
+
this.trackModuleObjectAliases(node);
|
|
176
|
+
this.trackCollectionAliases(node, sourceFile);
|
|
161
177
|
// Check for define() calls
|
|
162
178
|
if (this.isDefineCall(node)) {
|
|
163
179
|
this.processDefineCall(node, sourceFile);
|
|
164
180
|
}
|
|
181
|
+
// Check for lazy() calls
|
|
182
|
+
if (this.isLazyCall(node)) {
|
|
183
|
+
this.processLazyCall(node, sourceFile);
|
|
184
|
+
}
|
|
165
185
|
// Check for decorate() decorator
|
|
166
186
|
if (this.isDecorateDecorator(node)) {
|
|
167
187
|
this.processDecorateDecorator(node, sourceFile, currentClass);
|
|
@@ -186,6 +206,167 @@ class MnemonicaAnalyzer {
|
|
|
186
206
|
ts.forEachChild(node, child => this.visitNode(child, sourceFile, currentClass));
|
|
187
207
|
}
|
|
188
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Track imports from 'mnemonica' so aliases of the module object and
|
|
211
|
+
* createTypesCollection are recognized without relying on the type checker.
|
|
212
|
+
*/
|
|
213
|
+
trackImports(node) {
|
|
214
|
+
if (!ts.isImportDeclaration(node)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const { moduleSpecifier } = node;
|
|
218
|
+
if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== 'mnemonica') {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const clause = node.importClause;
|
|
222
|
+
if (!clause) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// import { mnemonica, createTypesCollection } from 'mnemonica'
|
|
226
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
227
|
+
for (const element of clause.namedBindings.elements) {
|
|
228
|
+
const localName = element.name.text;
|
|
229
|
+
const importedName = element.propertyName
|
|
230
|
+
? element.propertyName.text
|
|
231
|
+
: localName;
|
|
232
|
+
if (importedName === 'mnemonica') {
|
|
233
|
+
this.moduleObjectVariables.add(localName);
|
|
234
|
+
}
|
|
235
|
+
if (importedName === 'createTypesCollection') {
|
|
236
|
+
this.createTypesCollectionVariables.add(localName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// import * as mnemonica from 'mnemonica'
|
|
241
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
242
|
+
this.moduleObjectVariables.add(clause.namedBindings.name.text);
|
|
243
|
+
}
|
|
244
|
+
// import mnemonica from 'mnemonica' (default import) — treat as module object too
|
|
245
|
+
if (clause.name) {
|
|
246
|
+
this.moduleObjectVariables.add(clause.name.text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Track aliases of the mnemonica module object, e.g.:
|
|
251
|
+
* const m = mnemonica;
|
|
252
|
+
* const App = m;
|
|
253
|
+
*/
|
|
254
|
+
trackModuleObjectAliases(node) {
|
|
255
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const { initializer } = node;
|
|
259
|
+
if (!initializer) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (ts.isIdentifier(initializer) && this.moduleObjectVariables.has(initializer.text)) {
|
|
263
|
+
this.moduleObjectVariables.add(node.name.text);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Track custom collection variables, e.g.:
|
|
268
|
+
* const MyCollection = createTypesCollection();
|
|
269
|
+
* const Other = MyCollection;
|
|
270
|
+
*
|
|
271
|
+
* Also detects Option B user-provided registry interfaces:
|
|
272
|
+
* export interface MyCollectionRegistry {}
|
|
273
|
+
* const MyCollection = createTypesCollection<MyCollectionRegistry>();
|
|
274
|
+
*/
|
|
275
|
+
trackCollectionAliases(node, sourceFile) {
|
|
276
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const { initializer } = node;
|
|
280
|
+
if (!initializer) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Direct createTypesCollection() call
|
|
284
|
+
if (this.isCreateTypesCollectionCall(initializer)) {
|
|
285
|
+
const collectionId = this.nextCollectionId();
|
|
286
|
+
this.collectionVariables.set(node.name.text, collectionId);
|
|
287
|
+
const registryInterfaceName = this.extractRegistryInterfaceName(initializer, sourceFile);
|
|
288
|
+
this.collectionInfo.set(collectionId, {
|
|
289
|
+
variableName: node.name.text,
|
|
290
|
+
sourceFile: sourceFile.fileName,
|
|
291
|
+
registryInterfaceName: registryInterfaceName
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Alias of another collection variable
|
|
296
|
+
if (ts.isIdentifier(initializer)) {
|
|
297
|
+
const existing = this.collectionVariables.get(initializer.text);
|
|
298
|
+
if (existing) {
|
|
299
|
+
this.collectionVariables.set(node.name.text, existing);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Extract the registry interface name from createTypesCollection<Registry>()
|
|
305
|
+
* when the interface is declared in the same source file.
|
|
306
|
+
*/
|
|
307
|
+
extractRegistryInterfaceName(call, sourceFile) {
|
|
308
|
+
const typeArgs = call.typeArguments;
|
|
309
|
+
if (!typeArgs || typeArgs.length === 0) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
const firstTypeArg = typeArgs[0];
|
|
313
|
+
if (!ts.isTypeReferenceNode(firstTypeArg) || !ts.isIdentifier(firstTypeArg.typeName)) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
const name = firstTypeArg.typeName.text;
|
|
317
|
+
// Confirm the interface exists in the same source file.
|
|
318
|
+
for (const statement of sourceFile.statements) {
|
|
319
|
+
if (ts.isInterfaceDeclaration(statement) &&
|
|
320
|
+
statement.name.text === name) {
|
|
321
|
+
return name;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get the registry interface name for a collection id.
|
|
328
|
+
*/
|
|
329
|
+
getRegistryInterfaceName(collectionId) {
|
|
330
|
+
if (!collectionId) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
return this.collectionInfo.get(collectionId)?.registryInterfaceName;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Check if an expression is a createTypesCollection() call.
|
|
337
|
+
* Handles:
|
|
338
|
+
* createTypesCollection()
|
|
339
|
+
* ctc() // aliased import
|
|
340
|
+
* mnemonica.createTypesCollection() // module object method
|
|
341
|
+
* m.createTypesCollection() // aliased module object
|
|
342
|
+
*/
|
|
343
|
+
isCreateTypesCollectionCall(node) {
|
|
344
|
+
if (!ts.isCallExpression(node)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
const expr = node.expression;
|
|
348
|
+
// Direct call or aliased import: createTypesCollection() / ctc()
|
|
349
|
+
if (ts.isIdentifier(expr)) {
|
|
350
|
+
return expr.text === 'createTypesCollection' ||
|
|
351
|
+
this.createTypesCollectionVariables.has(expr.text);
|
|
352
|
+
}
|
|
353
|
+
// Module object method: mnemonica.createTypesCollection()
|
|
354
|
+
if (ts.isPropertyAccessExpression(expr) &&
|
|
355
|
+
expr.name.text === 'createTypesCollection' &&
|
|
356
|
+
ts.isIdentifier(expr.expression) &&
|
|
357
|
+
this.moduleObjectVariables.has(expr.expression.text)) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Generate a unique collection identifier.
|
|
364
|
+
*/
|
|
365
|
+
nextCollectionId() {
|
|
366
|
+
this.collectionCounter++;
|
|
367
|
+
const result = `collection_${this.collectionCounter}`;
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
189
370
|
/**
|
|
190
371
|
* Check if a node is a define() call
|
|
191
372
|
*/
|
|
@@ -193,7 +374,7 @@ class MnemonicaAnalyzer {
|
|
|
193
374
|
if (!ts.isCallExpression(node)) {
|
|
194
375
|
return false;
|
|
195
376
|
}
|
|
196
|
-
const expression = node
|
|
377
|
+
const { expression } = node;
|
|
197
378
|
// Check for direct call: define('TypeName', ...)
|
|
198
379
|
if (ts.isIdentifier(expression) && expression.text === 'define') {
|
|
199
380
|
return true;
|
|
@@ -204,6 +385,24 @@ class MnemonicaAnalyzer {
|
|
|
204
385
|
}
|
|
205
386
|
return false;
|
|
206
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Check if a node is a lazy() call
|
|
390
|
+
*/
|
|
391
|
+
isLazyCall(node) {
|
|
392
|
+
if (!ts.isCallExpression(node)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
const { expression } = node;
|
|
396
|
+
// Check for direct call: lazy('TypeName', getter, ...)
|
|
397
|
+
if (ts.isIdentifier(expression) && expression.text === 'lazy') {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
// Check for method call: SomeType.lazy('SubType', getter, ...)
|
|
401
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
402
|
+
return expression.name?.text === 'lazy';
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
207
406
|
/**
|
|
208
407
|
* Extract config options from an object literal
|
|
209
408
|
*/
|
|
@@ -247,7 +446,7 @@ class MnemonicaAnalyzer {
|
|
|
247
446
|
if (!ts.isDecorator(node)) {
|
|
248
447
|
return false;
|
|
249
448
|
}
|
|
250
|
-
const expression = node
|
|
449
|
+
const { expression } = node;
|
|
251
450
|
// Check for @decorate
|
|
252
451
|
if (ts.isIdentifier(expression) && expression.text === 'decorate') {
|
|
253
452
|
return true;
|
|
@@ -258,20 +457,37 @@ class MnemonicaAnalyzer {
|
|
|
258
457
|
if (ts.isIdentifier(fnName) && fnName.text === 'decorate') {
|
|
259
458
|
return true;
|
|
260
459
|
}
|
|
460
|
+
// Check for @MyCollection.decorate() where MyCollection is a custom collection
|
|
461
|
+
if (ts.isPropertyAccessExpression(fnName) &&
|
|
462
|
+
fnName.name.text === 'decorate' &&
|
|
463
|
+
ts.isIdentifier(fnName.expression) &&
|
|
464
|
+
this.collectionVariables.has(fnName.expression.text)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
261
467
|
}
|
|
262
468
|
return false;
|
|
263
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Mark a call expression as processed and return whether it already was.
|
|
472
|
+
*/
|
|
473
|
+
markProcessed(call) {
|
|
474
|
+
const marked = call;
|
|
475
|
+
if (marked.__tactica_processed) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
marked.__tactica_processed = true;
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
264
481
|
/**
|
|
265
482
|
* Process a define() call
|
|
266
483
|
*/
|
|
267
484
|
processDefineCall(call, sourceFile) {
|
|
268
485
|
// Check if this exact call has already been processed (prevents duplicates from chained calls)
|
|
269
|
-
if (call
|
|
486
|
+
if (this.markProcessed(call)) {
|
|
270
487
|
return;
|
|
271
488
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const typeName = this.extractTypeName(call);
|
|
489
|
+
// Get the type name and source context from arguments
|
|
490
|
+
const defineContext = this.extractDefineContext(call);
|
|
275
491
|
// For chained calls like define('A').define('B'), we want the position of the .define('B') part
|
|
276
492
|
// not the start of the entire expression
|
|
277
493
|
let positionNode = call;
|
|
@@ -284,7 +500,7 @@ class MnemonicaAnalyzer {
|
|
|
284
500
|
}
|
|
285
501
|
const startPos = positionNode.getStart(sourceFile);
|
|
286
502
|
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, startPos);
|
|
287
|
-
if (!typeName) {
|
|
503
|
+
if (!defineContext.typeName) {
|
|
288
504
|
this.errors.push({
|
|
289
505
|
message: 'Could not extract type name from define() call',
|
|
290
506
|
file: sourceFile.fileName,
|
|
@@ -293,13 +509,27 @@ class MnemonicaAnalyzer {
|
|
|
293
509
|
});
|
|
294
510
|
return;
|
|
295
511
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
512
|
+
const { typeName } = defineContext;
|
|
513
|
+
// Determine parent type and collection based on the call source.
|
|
514
|
+
const parentNode = defineContext.parentType;
|
|
515
|
+
const { collectionId } = defineContext;
|
|
300
516
|
// Extract config options
|
|
301
517
|
const config = this.extractConfig(call);
|
|
302
|
-
// Create
|
|
518
|
+
// Create type node first so its internal fullPath (including any collection prefix) is resolved.
|
|
519
|
+
const node = graph_1.TypeGraphImpl.createNode(typeName, parentNode, sourceFile.fileName, line + 1, character + 1, collectionId);
|
|
520
|
+
node.registryInterfaceName = this.getRegistryInterfaceName(collectionId);
|
|
521
|
+
// Extract properties from constructor function
|
|
522
|
+
node.properties = this.extractProperties(call);
|
|
523
|
+
// Extract constructor parameters for TypeRegistry signature
|
|
524
|
+
node.constructorParams = this.extractConstructorParams(call);
|
|
525
|
+
// Add to graph
|
|
526
|
+
if (parentNode) {
|
|
527
|
+
this.graph.addChild(parentNode, node);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
this.graph.addRoot(node);
|
|
531
|
+
}
|
|
532
|
+
// Create definition info using the node's resolved fullPath
|
|
303
533
|
const definition = {
|
|
304
534
|
name: typeName,
|
|
305
535
|
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
@@ -308,10 +538,52 @@ class MnemonicaAnalyzer {
|
|
|
308
538
|
strictChain: config.strictChain ?? true,
|
|
309
539
|
blockErrors: config.blockErrors ?? false,
|
|
310
540
|
};
|
|
311
|
-
this.definitions.set(fullPath, definition);
|
|
312
|
-
//
|
|
313
|
-
const
|
|
314
|
-
|
|
541
|
+
this.definitions.set(node.fullPath, definition);
|
|
542
|
+
// Track variable assignment: const User = define('UserEntity', ...) -> map "User" to "UserEntity"
|
|
543
|
+
// For chained calls like const X = define('A').define('B'), we want to map X -> A (the root)
|
|
544
|
+
this.trackVariableAssignment(call, parentNode, node.fullPath);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Process a lazy() call
|
|
548
|
+
*/
|
|
549
|
+
processLazyCall(call, sourceFile) {
|
|
550
|
+
// Check if this exact call has already been processed (prevents duplicates from chained calls)
|
|
551
|
+
if (this.markProcessed(call)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Get the type name and source context from arguments
|
|
555
|
+
const lazyContext = this.extractLazyContext(call, sourceFile);
|
|
556
|
+
// For chained calls like define('A').lazy('B'), we want the position of the .lazy('B') part
|
|
557
|
+
// not the start of the entire expression
|
|
558
|
+
let positionNode = call;
|
|
559
|
+
// If this is a chained call, get the position of the property access expression
|
|
560
|
+
// which is the .lazy part
|
|
561
|
+
if (ts.isPropertyAccessExpression(call.expression)) {
|
|
562
|
+
// The expression is the property access: (define('RootAsync', ...)).lazy
|
|
563
|
+
// We want the position of just the .lazy part
|
|
564
|
+
positionNode = call.expression.name; // This is the 'lazy' identifier
|
|
565
|
+
}
|
|
566
|
+
const startPos = positionNode.getStart(sourceFile);
|
|
567
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, startPos);
|
|
568
|
+
if (!lazyContext.typeName) {
|
|
569
|
+
this.errors.push({
|
|
570
|
+
message: 'Could not extract type name from lazy() call',
|
|
571
|
+
file: sourceFile.fileName,
|
|
572
|
+
line: line + 1,
|
|
573
|
+
column: character + 1,
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const { typeName } = lazyContext;
|
|
578
|
+
// Determine parent type and collection based on the call source.
|
|
579
|
+
const parentNode = lazyContext.parentType;
|
|
580
|
+
const { collectionId } = lazyContext;
|
|
581
|
+
// Extract config options
|
|
582
|
+
const config = this.extractLazyConfig(call);
|
|
583
|
+
// Create type node first so its internal fullPath (including any collection prefix) is resolved.
|
|
584
|
+
const node = graph_1.TypeGraphImpl.createNode(typeName, parentNode, sourceFile.fileName, line + 1, character + 1, collectionId);
|
|
585
|
+
node.registryInterfaceName = this.getRegistryInterfaceName(collectionId);
|
|
586
|
+
// Extract properties from the constructor returned by the lazy getter
|
|
315
587
|
node.properties = this.extractProperties(call);
|
|
316
588
|
// Extract constructor parameters for TypeRegistry signature
|
|
317
589
|
node.constructorParams = this.extractConstructorParams(call);
|
|
@@ -322,9 +594,271 @@ class MnemonicaAnalyzer {
|
|
|
322
594
|
else {
|
|
323
595
|
this.graph.addRoot(node);
|
|
324
596
|
}
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
597
|
+
// Create definition info using the node's resolved fullPath
|
|
598
|
+
const definition = {
|
|
599
|
+
name: typeName,
|
|
600
|
+
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
601
|
+
kind: 'define',
|
|
602
|
+
parent: parentNode ? parentNode.fullPath : null,
|
|
603
|
+
strictChain: config.strictChain ?? true,
|
|
604
|
+
blockErrors: config.blockErrors ?? false,
|
|
605
|
+
};
|
|
606
|
+
this.definitions.set(node.fullPath, definition);
|
|
607
|
+
// Track variable assignment: const LazyType = lazy('LazyType', ...) -> map "LazyType" -> "LazyType"
|
|
608
|
+
// For chained calls like const X = lazy('A').define('B'), we want to map X -> A (the root)
|
|
609
|
+
this.trackVariableAssignment(call, parentNode, node.fullPath);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Extract lazy() call arguments into a normalized shape.
|
|
613
|
+
* Handles named/unnamed and explicit-source forms, both as free calls
|
|
614
|
+
* and as method calls.
|
|
615
|
+
*/
|
|
616
|
+
extractLazyCallArgs(call) {
|
|
617
|
+
const args = call.arguments;
|
|
618
|
+
const isMethodCall = ts.isPropertyAccessExpression(call.expression);
|
|
619
|
+
if (isMethodCall) {
|
|
620
|
+
// Source is the object of the property access: Type.lazy(...)
|
|
621
|
+
const source = call.expression.expression;
|
|
622
|
+
if (args.length === 0) {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
const methodFirstArg = args[0];
|
|
626
|
+
if (ts.isStringLiteral(methodFirstArg)) {
|
|
627
|
+
// Type.lazy('Name', getter, config?)
|
|
628
|
+
if (args.length < 2) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
source,
|
|
633
|
+
name: methodFirstArg.text,
|
|
634
|
+
getter: args[1],
|
|
635
|
+
config: args[2],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// Type.lazy(getter, config?)
|
|
639
|
+
return {
|
|
640
|
+
source,
|
|
641
|
+
getter: methodFirstArg,
|
|
642
|
+
config: args[1],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// Free call: lazy(...)
|
|
646
|
+
if (args.length === 0) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
const firstArg = args[0];
|
|
650
|
+
// Explicit-source form: lazy(source, 'Name', getter, config?)
|
|
651
|
+
// or lazy(source, getter, config?)
|
|
652
|
+
if (args.length >= 2 && ts.isIdentifier(firstArg)) {
|
|
653
|
+
const secondArg = args[1];
|
|
654
|
+
if (ts.isStringLiteral(secondArg)) {
|
|
655
|
+
// lazy(source, 'Name', getter, config?)
|
|
656
|
+
if (args.length < 3) {
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
source: firstArg,
|
|
661
|
+
name: secondArg.text,
|
|
662
|
+
getter: args[2],
|
|
663
|
+
config: args[3],
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
// lazy(source, getter, config?)
|
|
667
|
+
return {
|
|
668
|
+
source: firstArg,
|
|
669
|
+
getter: secondArg,
|
|
670
|
+
config: args[2],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
// Named root form: lazy('Name', getter, config?)
|
|
674
|
+
if (ts.isStringLiteral(firstArg)) {
|
|
675
|
+
if (args.length < 2) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
name: firstArg.text,
|
|
680
|
+
getter: args[1],
|
|
681
|
+
config: args[2],
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
// Unnamed root form: lazy(getter, config?)
|
|
685
|
+
return {
|
|
686
|
+
getter: firstArg,
|
|
687
|
+
config: args[1],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Unwrap the constructor returned by a lazy getter.
|
|
692
|
+
* Supports:
|
|
693
|
+
* () => class Name {}
|
|
694
|
+
* () => function Name() {}
|
|
695
|
+
* () => { return class Name {}; }
|
|
696
|
+
* function () { return function Name() {}; }
|
|
697
|
+
*/
|
|
698
|
+
unwrapLazyGetter(getterExpr) {
|
|
699
|
+
if (ts.isArrowFunction(getterExpr)) {
|
|
700
|
+
const { body } = getterExpr;
|
|
701
|
+
if (!ts.isBlock(body)) {
|
|
702
|
+
return body;
|
|
703
|
+
}
|
|
704
|
+
for (const stmt of body.statements) {
|
|
705
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
706
|
+
return stmt.expression;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
if (ts.isFunctionExpression(getterExpr)) {
|
|
712
|
+
const { body } = getterExpr;
|
|
713
|
+
for (const stmt of body.statements) {
|
|
714
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
715
|
+
return stmt.expression;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
// Not a recognized getter pattern
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Extract a constructor name from a class expression, class declaration,
|
|
725
|
+
* or named function expression.
|
|
726
|
+
*/
|
|
727
|
+
extractConstructorName(constructorExpr) {
|
|
728
|
+
if (ts.isClassExpression(constructorExpr) && constructorExpr.name) {
|
|
729
|
+
return constructorExpr.name.text;
|
|
730
|
+
}
|
|
731
|
+
if (ts.isClassDeclaration(constructorExpr) && constructorExpr.name) {
|
|
732
|
+
return constructorExpr.name.text;
|
|
733
|
+
}
|
|
734
|
+
if (ts.isFunctionExpression(constructorExpr) && constructorExpr.name) {
|
|
735
|
+
return constructorExpr.name.text;
|
|
736
|
+
}
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Extract the type name from either a define() or lazy() call.
|
|
741
|
+
*/
|
|
742
|
+
extractMnemonicaTypeName(call) {
|
|
743
|
+
if (this.isDefineCall(call)) {
|
|
744
|
+
return this.extractTypeName(call);
|
|
745
|
+
}
|
|
746
|
+
if (this.isLazyCall(call)) {
|
|
747
|
+
const args = this.extractLazyCallArgs(call);
|
|
748
|
+
if (!args) {
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
if (args.name) {
|
|
752
|
+
return args.name;
|
|
753
|
+
}
|
|
754
|
+
const constructorExpr = this.unwrapLazyGetter(args.getter);
|
|
755
|
+
if (constructorExpr) {
|
|
756
|
+
return this.extractConstructorName(constructorExpr);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Extract the full lazy() call context: type name, parent type, and collection.
|
|
763
|
+
* Handles direct calls, property-access calls, chained calls, and the
|
|
764
|
+
* explicit-source form `lazy(source, 'TypeName', getter)`.
|
|
765
|
+
*/
|
|
766
|
+
extractLazyContext(call, sourceFile) {
|
|
767
|
+
const args = this.extractLazyCallArgs(call);
|
|
768
|
+
if (!args) {
|
|
769
|
+
return {};
|
|
770
|
+
}
|
|
771
|
+
let typeName = args.name;
|
|
772
|
+
if (!typeName) {
|
|
773
|
+
const constructorExpr = this.unwrapLazyGetter(args.getter);
|
|
774
|
+
if (constructorExpr) {
|
|
775
|
+
typeName = this.extractConstructorName(constructorExpr);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (!typeName) {
|
|
779
|
+
return {};
|
|
780
|
+
}
|
|
781
|
+
const { expression } = call;
|
|
782
|
+
// Direct call: lazy('TypeName', ...) or lazy(source, 'TypeName', getter)
|
|
783
|
+
if (ts.isIdentifier(expression) && expression.text === 'lazy') {
|
|
784
|
+
if (args.source && ts.isIdentifier(args.source)) {
|
|
785
|
+
const sourceContext = this.resolveDefineSource(args.source.text);
|
|
786
|
+
return {
|
|
787
|
+
typeName,
|
|
788
|
+
parentType: sourceContext.parentType,
|
|
789
|
+
collectionId: sourceContext.collectionId,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
// Plain root lazy in default collection
|
|
793
|
+
return { typeName };
|
|
794
|
+
}
|
|
795
|
+
// Property access: X.lazy('TypeName', ...)
|
|
796
|
+
if (ts.isPropertyAccessExpression(expression) && expression.name.text === 'lazy') {
|
|
797
|
+
const obj = expression.expression;
|
|
798
|
+
if (ts.isIdentifier(obj)) {
|
|
799
|
+
const sourceContext = this.resolveDefineSource(obj.text);
|
|
800
|
+
return {
|
|
801
|
+
typeName,
|
|
802
|
+
parentType: sourceContext.parentType,
|
|
803
|
+
collectionId: sourceContext.collectionId,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (ts.isPropertyAccessExpression(obj)) {
|
|
807
|
+
// Nested access: instance.Type.lazy - try to resolve
|
|
808
|
+
const chain = this.getPropertyChain(obj);
|
|
809
|
+
if (chain.length > 0) {
|
|
810
|
+
const parentNode = this.graph.findType(chain.join('.'));
|
|
811
|
+
return { typeName, parentType: parentNode };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (ts.isCallExpression(obj)) {
|
|
815
|
+
// Determine the collection context from the root of the chain so that
|
|
816
|
+
// custom-collection types do not get confused with default-collection types.
|
|
817
|
+
const rootId = this.getRootIdentifier(obj.expression);
|
|
818
|
+
const expectedCollectionId = rootId
|
|
819
|
+
? this.resolveDefineSource(rootId.text).collectionId
|
|
820
|
+
: undefined;
|
|
821
|
+
// Chained call: define('A').lazy('B') or lazy('A').lazy('B')
|
|
822
|
+
if (this.isDefineCall(obj)) {
|
|
823
|
+
this.processDefineCall(obj, sourceFile);
|
|
824
|
+
const parentTypeName = this.extractMnemonicaTypeName(obj);
|
|
825
|
+
if (parentTypeName) {
|
|
826
|
+
const parentNode = this.findParentTypeByName(parentTypeName, expectedCollectionId);
|
|
827
|
+
return { typeName, parentType: parentNode, collectionId: parentNode?.collectionId };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (this.isLazyCall(obj)) {
|
|
831
|
+
this.processLazyCall(obj, sourceFile);
|
|
832
|
+
const parentTypeName = this.extractMnemonicaTypeName(obj);
|
|
833
|
+
if (parentTypeName) {
|
|
834
|
+
const parentNode = this.findParentTypeByName(parentTypeName, expectedCollectionId);
|
|
835
|
+
return { typeName, parentType: parentNode, collectionId: parentNode?.collectionId };
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Builder lookup chain: App.lookup('User').lazy('Admin')
|
|
839
|
+
if (this.isLookupCall(obj)) {
|
|
840
|
+
const lookedUpPath = this.resolveLookupPath(obj);
|
|
841
|
+
if (lookedUpPath) {
|
|
842
|
+
const parentNode = this.graph.findType(lookedUpPath);
|
|
843
|
+
if (parentNode) {
|
|
844
|
+
return { typeName, parentType: parentNode, collectionId: parentNode.collectionId };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return { typeName };
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Extract config options from lazy() call
|
|
854
|
+
*/
|
|
855
|
+
extractLazyConfig(call) {
|
|
856
|
+
const args = this.extractLazyCallArgs(call);
|
|
857
|
+
if (!args || !args.config || !ts.isObjectLiteralExpression(args.config)) {
|
|
858
|
+
return {};
|
|
859
|
+
}
|
|
860
|
+
const configResult = this.extractConfigFromObjectLiteral(args.config);
|
|
861
|
+
return configResult;
|
|
328
862
|
}
|
|
329
863
|
/**
|
|
330
864
|
* Track variable assignments that capture define() results
|
|
@@ -417,51 +951,68 @@ class MnemonicaAnalyzer {
|
|
|
417
951
|
return;
|
|
418
952
|
}
|
|
419
953
|
// Parse decorator arguments: @decorate(), @decorate(Parent),
|
|
420
|
-
// @decorate({ ... }), @decorate(Parent, { ... })
|
|
954
|
+
// @decorate({ ... }), @decorate(Parent, { ... }),
|
|
955
|
+
// @MyCollection.decorate(), @MyCollection.decorate({ ... })
|
|
421
956
|
let parentNode;
|
|
422
957
|
let parentFullPath = null;
|
|
958
|
+
let collectionId;
|
|
423
959
|
let decoratorConfig = {};
|
|
424
960
|
if (ts.isCallExpression(decorator.expression)) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
961
|
+
const callExpr = decorator.expression;
|
|
962
|
+
const callee = callExpr.expression;
|
|
963
|
+
// Check for @MyCollection.decorate() where MyCollection is a custom collection.
|
|
964
|
+
// The decorated class becomes a root type in that collection.
|
|
965
|
+
if (ts.isPropertyAccessExpression(callee) &&
|
|
966
|
+
callee.name.text === 'decorate' &&
|
|
967
|
+
ts.isIdentifier(callee.expression) &&
|
|
968
|
+
this.collectionVariables.has(callee.expression.text)) {
|
|
969
|
+
collectionId = this.collectionVariables.get(callee.expression.text);
|
|
970
|
+
if (callExpr.arguments.length === 1 && ts.isObjectLiteralExpression(callExpr.arguments[0])) {
|
|
971
|
+
decoratorConfig = this.extractConfigFromObjectLiteral(callExpr.arguments[0]);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
const args = callExpr.arguments;
|
|
976
|
+
let parentArg;
|
|
977
|
+
let configArg;
|
|
978
|
+
for (const arg of args) {
|
|
979
|
+
if (ts.isIdentifier(arg)) {
|
|
980
|
+
if (parentArg) {
|
|
981
|
+
this.errors.push({
|
|
982
|
+
message: '@decorate() accepts only one parent reference',
|
|
983
|
+
file: sourceFile.fileName,
|
|
984
|
+
line: line + 1,
|
|
985
|
+
column: character + 1,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
parentArg = arg;
|
|
990
|
+
}
|
|
437
991
|
}
|
|
438
|
-
else {
|
|
439
|
-
|
|
992
|
+
else if (ts.isObjectLiteralExpression(arg)) {
|
|
993
|
+
if (configArg) {
|
|
994
|
+
this.errors.push({
|
|
995
|
+
message: '@decorate() accepts only one config object',
|
|
996
|
+
file: sourceFile.fileName,
|
|
997
|
+
line: line + 1,
|
|
998
|
+
column: character + 1,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
configArg = arg;
|
|
1003
|
+
}
|
|
440
1004
|
}
|
|
441
1005
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
file: sourceFile.fileName,
|
|
447
|
-
line: line + 1,
|
|
448
|
-
column: character + 1,
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
configArg = arg;
|
|
1006
|
+
if (parentArg) {
|
|
1007
|
+
parentNode = this.findParentTypeByIdentifier(parentArg.text);
|
|
1008
|
+
if (parentNode) {
|
|
1009
|
+
parentFullPath = parentNode.fullPath;
|
|
453
1010
|
}
|
|
454
1011
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
parentNode = this.findParentTypeByIdentifier(parentArg.text);
|
|
458
|
-
if (parentNode) {
|
|
459
|
-
parentFullPath = parentNode.fullPath;
|
|
1012
|
+
if (configArg) {
|
|
1013
|
+
decoratorConfig = this.extractConfigFromObjectLiteral(configArg);
|
|
460
1014
|
}
|
|
461
1015
|
}
|
|
462
|
-
if (configArg) {
|
|
463
|
-
decoratorConfig = this.extractConfigFromObjectLiteral(configArg);
|
|
464
|
-
}
|
|
465
1016
|
}
|
|
466
1017
|
// Build full path
|
|
467
1018
|
const fullPath = parentNode ? `${parentNode.fullPath}.${typeName}` : typeName;
|
|
@@ -476,7 +1027,8 @@ class MnemonicaAnalyzer {
|
|
|
476
1027
|
};
|
|
477
1028
|
this.definitions.set(fullPath, definition);
|
|
478
1029
|
// Create type node
|
|
479
|
-
const node = graph_1.TypeGraphImpl.createNode(typeName, parentNode, sourceFile.fileName, line + 1, character + 1);
|
|
1030
|
+
const node = graph_1.TypeGraphImpl.createNode(typeName, parentNode, sourceFile.fileName, line + 1, character + 1, collectionId);
|
|
1031
|
+
node.registryInterfaceName = this.getRegistryInterfaceName(node.collectionId);
|
|
480
1032
|
// Extract properties and constructor parameters from class members
|
|
481
1033
|
node.properties = this.extractClassProperties(classDecl);
|
|
482
1034
|
node.constructorParams = this.extractClassConstructorParams(classDecl);
|
|
@@ -489,15 +1041,23 @@ class MnemonicaAnalyzer {
|
|
|
489
1041
|
}
|
|
490
1042
|
}
|
|
491
1043
|
/**
|
|
492
|
-
* Extract type name from define() call arguments
|
|
1044
|
+
* Extract type name from define() call arguments.
|
|
1045
|
+
* Handles:
|
|
1046
|
+
* define('TypeName', handler)
|
|
1047
|
+
* define(source, 'TypeName', handler) // explicit-source form
|
|
1048
|
+
* define(function TypeName() {})
|
|
1049
|
+
* define(() => class TypeName {})
|
|
493
1050
|
*/
|
|
494
1051
|
extractTypeName(call) {
|
|
495
1052
|
const args = call.arguments;
|
|
496
|
-
// define('TypeName', handler) or define(() => class)
|
|
497
1053
|
if (args.length === 0) {
|
|
498
1054
|
return undefined;
|
|
499
1055
|
}
|
|
500
1056
|
const firstArg = args[0];
|
|
1057
|
+
// Explicit-source form: define(source, 'TypeName', handler)
|
|
1058
|
+
if (args.length >= 2 && ts.isIdentifier(firstArg) && ts.isStringLiteral(args[1])) {
|
|
1059
|
+
return args[1].text;
|
|
1060
|
+
}
|
|
501
1061
|
// String literal: define('TypeName', ...)
|
|
502
1062
|
if (ts.isStringLiteral(firstArg)) {
|
|
503
1063
|
return firstArg.text;
|
|
@@ -508,7 +1068,7 @@ class MnemonicaAnalyzer {
|
|
|
508
1068
|
}
|
|
509
1069
|
// Arrow function returning class: define(() => class TypeName {})
|
|
510
1070
|
if (ts.isArrowFunction(firstArg)) {
|
|
511
|
-
const body = firstArg
|
|
1071
|
+
const { body } = firstArg;
|
|
512
1072
|
if (ts.isClassExpression(body) && body.name) {
|
|
513
1073
|
return body.name.text;
|
|
514
1074
|
}
|
|
@@ -516,74 +1076,213 @@ class MnemonicaAnalyzer {
|
|
|
516
1076
|
return undefined;
|
|
517
1077
|
}
|
|
518
1078
|
/**
|
|
519
|
-
*
|
|
1079
|
+
* Extract the full define() call context: type name, parent type, and collection.
|
|
1080
|
+
* Handles direct calls, property-access calls, chained calls, and the
|
|
1081
|
+
* explicit-source form `define(source, 'TypeName', handler)`.
|
|
520
1082
|
*/
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return undefined;
|
|
1083
|
+
extractDefineContext(call) {
|
|
1084
|
+
const typeName = this.extractTypeName(call);
|
|
1085
|
+
if (!typeName) {
|
|
1086
|
+
return {};
|
|
526
1087
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (ts.isIdentifier(
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1088
|
+
const { expression } = call;
|
|
1089
|
+
// Direct call: define('TypeName', ...) or define(source, 'TypeName', handler)
|
|
1090
|
+
if (ts.isIdentifier(expression) && expression.text === 'define') {
|
|
1091
|
+
// Explicit-source form: define(source, 'TypeName', handler)
|
|
1092
|
+
if (call.arguments.length >= 2 && ts.isIdentifier(call.arguments[0])) {
|
|
1093
|
+
const sourceName = call.arguments[0].text;
|
|
1094
|
+
const sourceContext = this.resolveDefineSource(sourceName);
|
|
1095
|
+
return {
|
|
1096
|
+
typeName,
|
|
1097
|
+
parentType: sourceContext.parentType,
|
|
1098
|
+
collectionId: sourceContext.collectionId,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
// Plain root define in default collection
|
|
1102
|
+
return { typeName };
|
|
1103
|
+
}
|
|
1104
|
+
// Property access: X.define('TypeName', ...)
|
|
1105
|
+
if (ts.isPropertyAccessExpression(expression) && expression.name.text === 'define') {
|
|
1106
|
+
const obj = expression.expression;
|
|
1107
|
+
if (ts.isIdentifier(obj)) {
|
|
1108
|
+
const sourceContext = this.resolveDefineSource(obj.text);
|
|
1109
|
+
return {
|
|
1110
|
+
typeName,
|
|
1111
|
+
parentType: sourceContext.parentType,
|
|
1112
|
+
collectionId: sourceContext.collectionId,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
if (ts.isPropertyAccessExpression(obj)) {
|
|
1116
|
+
// Nested access: instance.Type.define - try to resolve
|
|
1117
|
+
const chain = this.getPropertyChain(obj);
|
|
1118
|
+
if (chain.length > 0) {
|
|
1119
|
+
const parentNode = this.graph.findType(chain.join('.'));
|
|
1120
|
+
return { typeName, parentType: parentNode };
|
|
548
1121
|
}
|
|
549
1122
|
}
|
|
1123
|
+
if (ts.isCallExpression(obj)) {
|
|
1124
|
+
// Determine the collection context from the root of the chain so that
|
|
1125
|
+
// custom-collection types do not get confused with default-collection types.
|
|
1126
|
+
const rootId = this.getRootIdentifier(obj.expression);
|
|
1127
|
+
const expectedCollectionId = rootId
|
|
1128
|
+
? this.resolveDefineSource(rootId.text).collectionId
|
|
1129
|
+
: undefined;
|
|
1130
|
+
// Chained call: define('A').define('B') or mnemonica.define('A').define('B')
|
|
1131
|
+
if (this.isDefineCall(obj)) {
|
|
1132
|
+
this.processDefineCall(obj, call.getSourceFile());
|
|
1133
|
+
const parentTypeName = this.extractTypeName(obj);
|
|
1134
|
+
if (parentTypeName) {
|
|
1135
|
+
const parentNode = this.findParentTypeByName(parentTypeName, expectedCollectionId);
|
|
1136
|
+
// Inherit collection from the parent type (if any)
|
|
1137
|
+
return { typeName, parentType: parentNode, collectionId: parentNode?.collectionId };
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// Chained lazy call: lazy('A').define('B') or Type.lazy('A').define('B')
|
|
1141
|
+
if (this.isLazyCall(obj)) {
|
|
1142
|
+
this.processLazyCall(obj, call.getSourceFile());
|
|
1143
|
+
const parentTypeName = this.extractMnemonicaTypeName(obj);
|
|
1144
|
+
if (parentTypeName) {
|
|
1145
|
+
const parentNode = this.findParentTypeByName(parentTypeName, expectedCollectionId);
|
|
1146
|
+
return { typeName, parentType: parentNode, collectionId: parentNode?.collectionId };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// Builder lookup chain: App.lookup('User').define('Admin')
|
|
1150
|
+
if (this.isLookupCall(obj)) {
|
|
1151
|
+
const lookedUpPath = this.resolveLookupPath(obj);
|
|
1152
|
+
if (lookedUpPath) {
|
|
1153
|
+
const parentNode = this.graph.findType(lookedUpPath);
|
|
1154
|
+
if (parentNode) {
|
|
1155
|
+
return { typeName, parentType: parentNode, collectionId: parentNode.collectionId };
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return { typeName };
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Prefix a dotted type path with a collection identifier so custom-collection
|
|
1165
|
+
* types do not collide with default-collection types in the graph.
|
|
1166
|
+
*/
|
|
1167
|
+
prefixCollectionPath(path, collectionId) {
|
|
1168
|
+
return `${collectionId}::${path}`;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Resolve a define() source identifier to either a parent type, a collection,
|
|
1172
|
+
* or the default (module object) collection.
|
|
1173
|
+
*/
|
|
1174
|
+
resolveDefineSource(sourceName) {
|
|
1175
|
+
// Module object aliases -> root in default collection
|
|
1176
|
+
if (this.moduleObjectVariables.has(sourceName)) {
|
|
1177
|
+
return {};
|
|
1178
|
+
}
|
|
1179
|
+
// Collection variables -> root in that collection
|
|
1180
|
+
const collectionId = this.collectionVariables.get(sourceName);
|
|
1181
|
+
if (collectionId) {
|
|
1182
|
+
return { collectionId };
|
|
1183
|
+
}
|
|
1184
|
+
// Otherwise treat as a type variable reference
|
|
1185
|
+
const parentNode = this.findParentTypeByIdentifier(sourceName);
|
|
1186
|
+
return { parentType: parentNode, collectionId: parentNode?.collectionId };
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Check if a call expression is a lookup() call.
|
|
1190
|
+
*/
|
|
1191
|
+
isLookupCall(node) {
|
|
1192
|
+
const expr = node.expression;
|
|
1193
|
+
if (ts.isIdentifier(expr) && expr.text === 'lookup') {
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'lookup') {
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Resolve a lookup() call to a dotted type path (best effort).
|
|
1203
|
+
* Handles:
|
|
1204
|
+
* lookup('User')
|
|
1205
|
+
* lookup(source, 'User')
|
|
1206
|
+
* App.lookup('User')
|
|
1207
|
+
* collection.lookup('User.Admin')
|
|
1208
|
+
*/
|
|
1209
|
+
resolveLookupPath(call) {
|
|
1210
|
+
const args = call.arguments;
|
|
1211
|
+
if (args.length === 0) {
|
|
550
1212
|
return undefined;
|
|
551
1213
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
556
|
-
|
|
1214
|
+
// Single-arg lookup: lookup('User') or App.lookup('User')
|
|
1215
|
+
if (args.length === 1) {
|
|
1216
|
+
const arg = args[0];
|
|
1217
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
1218
|
+
const path = arg.text;
|
|
1219
|
+
// If this is a method call on a source, resolve relative to that source.
|
|
1220
|
+
if (ts.isPropertyAccessExpression(call.expression)) {
|
|
1221
|
+
const sourceExpr = call.expression.expression;
|
|
1222
|
+
if (ts.isIdentifier(sourceExpr)) {
|
|
1223
|
+
const sourceName = sourceExpr.text;
|
|
1224
|
+
const sourceContext = this.resolveDefineSource(sourceName);
|
|
1225
|
+
if (sourceContext.collectionId) {
|
|
1226
|
+
// Collection lookup: prefix path with the collection id
|
|
1227
|
+
return this.prefixCollectionPath(path, sourceContext.collectionId);
|
|
1228
|
+
}
|
|
1229
|
+
if (sourceContext.parentType) {
|
|
1230
|
+
// Type lookup: relative first, then root fallback
|
|
1231
|
+
const relativePath = `${sourceContext.parentType.fullPath}.${path}`;
|
|
1232
|
+
if (this.graph.findType(relativePath)) {
|
|
1233
|
+
return relativePath;
|
|
1234
|
+
}
|
|
1235
|
+
return path;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return path;
|
|
557
1240
|
}
|
|
1241
|
+
return undefined;
|
|
558
1242
|
}
|
|
559
|
-
//
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1243
|
+
// Two-arg lookup: lookup(source, 'User')
|
|
1244
|
+
if (args.length >= 2) {
|
|
1245
|
+
const sourceArg = args[0];
|
|
1246
|
+
const pathArg = args[1];
|
|
1247
|
+
if (!ts.isIdentifier(sourceArg) || !ts.isStringLiteral(pathArg)) {
|
|
1248
|
+
return undefined;
|
|
1249
|
+
}
|
|
1250
|
+
const sourceName = sourceArg.text;
|
|
1251
|
+
const path = pathArg.text;
|
|
1252
|
+
const sourceContext = this.resolveDefineSource(sourceName);
|
|
1253
|
+
if (sourceContext.collectionId) {
|
|
1254
|
+
return this.prefixCollectionPath(path, sourceContext.collectionId);
|
|
1255
|
+
}
|
|
1256
|
+
if (sourceContext.parentType) {
|
|
1257
|
+
const relativePath = `${sourceContext.parentType.fullPath}.${path}`;
|
|
1258
|
+
if (this.graph.findType(relativePath)) {
|
|
1259
|
+
return relativePath;
|
|
571
1260
|
}
|
|
1261
|
+
return path;
|
|
572
1262
|
}
|
|
1263
|
+
return path;
|
|
573
1264
|
}
|
|
574
1265
|
return undefined;
|
|
575
1266
|
}
|
|
576
1267
|
/**
|
|
577
|
-
* Find a parent type by its name, searching in the graph
|
|
1268
|
+
* Find a parent type by its name, searching in the graph.
|
|
1269
|
+
* When collectionId is provided, only types from that collection are considered.
|
|
578
1270
|
*/
|
|
579
|
-
findParentTypeByName(name) {
|
|
580
|
-
|
|
1271
|
+
findParentTypeByName(name, collectionId) {
|
|
1272
|
+
const matchesCollection = (type) => {
|
|
1273
|
+
if (collectionId === undefined) {
|
|
1274
|
+
return type.collectionId === undefined;
|
|
1275
|
+
}
|
|
1276
|
+
return type.collectionId === collectionId;
|
|
1277
|
+
};
|
|
1278
|
+
// First try exact match (default-collection types use the plain dotted path)
|
|
581
1279
|
const exact = this.graph.findType(name);
|
|
582
|
-
if (exact)
|
|
1280
|
+
if (exact && matchesCollection(exact)) {
|
|
583
1281
|
return exact;
|
|
584
|
-
|
|
1282
|
+
}
|
|
1283
|
+
// Then search through all types for one with matching name and collection
|
|
585
1284
|
for (const type of this.graph.getAllTypes()) {
|
|
586
|
-
if (type.name === name) {
|
|
1285
|
+
if (type.name === name && matchesCollection(type)) {
|
|
587
1286
|
return type;
|
|
588
1287
|
}
|
|
589
1288
|
}
|
|
@@ -605,6 +1304,20 @@ class MnemonicaAnalyzer {
|
|
|
605
1304
|
const parentNode = this.findParentTypeByName(name);
|
|
606
1305
|
return parentNode;
|
|
607
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Get the leftmost identifier of a property-access chain.
|
|
1309
|
+
* For `App.define('User').define('Admin')` this returns the `App` identifier.
|
|
1310
|
+
*/
|
|
1311
|
+
getRootIdentifier(expr) {
|
|
1312
|
+
let current = expr;
|
|
1313
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
1314
|
+
current = current.expression;
|
|
1315
|
+
}
|
|
1316
|
+
if (ts.isIdentifier(current)) {
|
|
1317
|
+
return current;
|
|
1318
|
+
}
|
|
1319
|
+
return undefined;
|
|
1320
|
+
}
|
|
608
1321
|
/**
|
|
609
1322
|
* Get property chain from nested access
|
|
610
1323
|
*/
|
|
@@ -622,23 +1335,61 @@ class MnemonicaAnalyzer {
|
|
|
622
1335
|
}
|
|
623
1336
|
return chain;
|
|
624
1337
|
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Determine the constructor expression for either a define() or lazy() call.
|
|
1340
|
+
* For define() this is the construct handler; for lazy() it is the value
|
|
1341
|
+
* returned by the lazy getter.
|
|
1342
|
+
*/
|
|
1343
|
+
extractConstructorExpression(call) {
|
|
1344
|
+
const expr = call.expression;
|
|
1345
|
+
const name = ts.isIdentifier(expr)
|
|
1346
|
+
? expr.text
|
|
1347
|
+
: ts.isPropertyAccessExpression(expr)
|
|
1348
|
+
? expr.name.text
|
|
1349
|
+
: '';
|
|
1350
|
+
if (name === 'lazy') {
|
|
1351
|
+
const lazyArgs = this.extractLazyCallArgs(call);
|
|
1352
|
+
if (!lazyArgs) {
|
|
1353
|
+
return undefined;
|
|
1354
|
+
}
|
|
1355
|
+
return this.unwrapLazyGetter(lazyArgs.getter);
|
|
1356
|
+
}
|
|
1357
|
+
// define() call
|
|
1358
|
+
const args = call.arguments;
|
|
1359
|
+
if (args.length === 0) {
|
|
1360
|
+
return undefined;
|
|
1361
|
+
}
|
|
1362
|
+
// Modern form: define('Name', handler, config?)
|
|
1363
|
+
if (ts.isStringLiteral(args[0])) {
|
|
1364
|
+
return args[1];
|
|
1365
|
+
}
|
|
1366
|
+
// Legacy form: define(function Name() {}) or define(() => class Name {})
|
|
1367
|
+
return args[0];
|
|
1368
|
+
}
|
|
625
1369
|
/**
|
|
626
1370
|
* Extract properties from constructor function
|
|
627
1371
|
*/
|
|
628
1372
|
extractProperties(call) {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return properties;
|
|
1373
|
+
const constructorExpr = this.extractConstructorExpression(call);
|
|
1374
|
+
if (!constructorExpr) {
|
|
1375
|
+
return new Map();
|
|
633
1376
|
}
|
|
1377
|
+
const result = this.extractPropertiesFromConstructor(constructorExpr);
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Extract properties from a constructor expression (function, arrow, or class).
|
|
1382
|
+
*/
|
|
1383
|
+
extractPropertiesFromConstructor(constructorExpr) {
|
|
1384
|
+
const properties = new Map();
|
|
634
1385
|
// Build type map from data parameter (for this.x = data.x patterns)
|
|
635
|
-
const dataTypeMap = this.buildDataTypeMap(
|
|
1386
|
+
const dataTypeMap = this.buildDataTypeMap(constructorExpr);
|
|
636
1387
|
// Handle function expression
|
|
637
|
-
if (ts.isFunctionExpression(
|
|
638
|
-
const body =
|
|
1388
|
+
if (ts.isFunctionExpression(constructorExpr) || ts.isArrowFunction(constructorExpr)) {
|
|
1389
|
+
const { body } = constructorExpr;
|
|
639
1390
|
// First, extract properties from `this` parameter type annotation
|
|
640
1391
|
// This handles patterns like: function(this: SomeType, data: SomeType) { }
|
|
641
|
-
const thisParamProperties = this.extractThisParamProperties(
|
|
1392
|
+
const thisParamProperties = this.extractThisParamProperties(constructorExpr);
|
|
642
1393
|
for (const [name, propInfo] of thisParamProperties) {
|
|
643
1394
|
properties.set(name, propInfo);
|
|
644
1395
|
}
|
|
@@ -652,16 +1403,18 @@ class MnemonicaAnalyzer {
|
|
|
652
1403
|
}
|
|
653
1404
|
}
|
|
654
1405
|
// Handle class expression
|
|
655
|
-
if (ts.isClassExpression(
|
|
1406
|
+
if (ts.isClassExpression(constructorExpr)) {
|
|
656
1407
|
// First pass: collect all property types for method inference
|
|
657
|
-
const classPropertyTypes = this.extractClassPropertyTypes(
|
|
658
|
-
for (const member of
|
|
1408
|
+
const classPropertyTypes = this.extractClassPropertyTypes(constructorExpr);
|
|
1409
|
+
for (const member of constructorExpr.members) {
|
|
659
1410
|
// Handle property declarations
|
|
660
1411
|
if (ts.isPropertyDeclaration(member) && member.name) {
|
|
661
1412
|
// Skip private and protected properties
|
|
662
1413
|
if (member.modifiers) {
|
|
663
|
-
const hasPrivateOrProtected = member.modifiers.some(m =>
|
|
664
|
-
m.kind === ts.SyntaxKind.
|
|
1414
|
+
const hasPrivateOrProtected = member.modifiers.some(m => {
|
|
1415
|
+
return m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
1416
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword;
|
|
1417
|
+
});
|
|
665
1418
|
if (hasPrivateOrProtected) {
|
|
666
1419
|
continue;
|
|
667
1420
|
}
|
|
@@ -679,8 +1432,10 @@ class MnemonicaAnalyzer {
|
|
|
679
1432
|
if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
|
|
680
1433
|
// Skip private and protected methods
|
|
681
1434
|
if (member.modifiers) {
|
|
682
|
-
const hasPrivateOrProtected = member.modifiers.some(m =>
|
|
683
|
-
m.kind === ts.SyntaxKind.
|
|
1435
|
+
const hasPrivateOrProtected = member.modifiers.some(m => {
|
|
1436
|
+
return m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
1437
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword;
|
|
1438
|
+
});
|
|
684
1439
|
if (hasPrivateOrProtected) {
|
|
685
1440
|
continue;
|
|
686
1441
|
}
|
|
@@ -697,8 +1452,10 @@ class MnemonicaAnalyzer {
|
|
|
697
1452
|
if (ts.isGetAccessor(member) && member.name && ts.isIdentifier(member.name)) {
|
|
698
1453
|
// Skip private and protected getters
|
|
699
1454
|
if (member.modifiers) {
|
|
700
|
-
const hasPrivateOrProtected = member.modifiers.some(m =>
|
|
701
|
-
m.kind === ts.SyntaxKind.
|
|
1455
|
+
const hasPrivateOrProtected = member.modifiers.some(m => {
|
|
1456
|
+
return m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
1457
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword;
|
|
1458
|
+
});
|
|
702
1459
|
if (hasPrivateOrProtected) {
|
|
703
1460
|
continue;
|
|
704
1461
|
}
|
|
@@ -792,7 +1549,7 @@ class MnemonicaAnalyzer {
|
|
|
792
1549
|
// Handle: this.property = value
|
|
793
1550
|
if (ts.isBinaryExpression(expr) &&
|
|
794
1551
|
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
795
|
-
const left = expr
|
|
1552
|
+
const { left } = expr;
|
|
796
1553
|
if (ts.isPropertyAccessExpression(left)) {
|
|
797
1554
|
// Check if accessing 'this' (ThisKeyword)
|
|
798
1555
|
if (left.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
@@ -1037,7 +1794,7 @@ class MnemonicaAnalyzer {
|
|
|
1037
1794
|
case ts.SyntaxKind.VoidKeyword:
|
|
1038
1795
|
return 'void';
|
|
1039
1796
|
case ts.SyntaxKind.ArrayType:
|
|
1040
|
-
return
|
|
1797
|
+
return `Array<${this.inferType(typeNode.elementType)}>`;
|
|
1041
1798
|
case ts.SyntaxKind.TypeLiteral: {
|
|
1042
1799
|
// Inline-expand type literals instead of collapsing to 'object'
|
|
1043
1800
|
const typeLit = typeNode;
|
|
@@ -1054,7 +1811,7 @@ class MnemonicaAnalyzer {
|
|
|
1054
1811
|
}
|
|
1055
1812
|
case ts.SyntaxKind.LiteralType: {
|
|
1056
1813
|
// Handle string literal types like 'user', 'admin', etc.
|
|
1057
|
-
const literal = typeNode
|
|
1814
|
+
const { literal } = typeNode;
|
|
1058
1815
|
if (ts.isStringLiteral(literal)) {
|
|
1059
1816
|
// Return the actual literal value (e.g., 'user' instead of string)
|
|
1060
1817
|
return `'${literal.text}'`;
|
|
@@ -1093,30 +1850,30 @@ class MnemonicaAnalyzer {
|
|
|
1093
1850
|
if (arg.kind === ts.SyntaxKind.TypeQuery) {
|
|
1094
1851
|
const typeQuery = arg;
|
|
1095
1852
|
if (ts.isIdentifier(typeQuery.exprName)) {
|
|
1096
|
-
const
|
|
1853
|
+
const queryTypeName = typeQuery.exprName.text;
|
|
1097
1854
|
// Look up the type in the graph to get full path
|
|
1098
|
-
const
|
|
1099
|
-
if (
|
|
1855
|
+
const matchedType = this.graph.findTypeByName(queryTypeName);
|
|
1856
|
+
if (matchedType) {
|
|
1100
1857
|
// Convert full path with dots to underscores: Usages.UsageEntry -> Usages_UsageEntry
|
|
1101
|
-
return
|
|
1858
|
+
return matchedType.fullPath.replace(/\./g, '_');
|
|
1102
1859
|
}
|
|
1103
1860
|
// Fallback: just use the type name if not found in graph
|
|
1104
|
-
return
|
|
1861
|
+
return queryTypeName;
|
|
1105
1862
|
}
|
|
1106
1863
|
}
|
|
1107
1864
|
}
|
|
1108
1865
|
if (!typeRef.typeArguments || typeRef.typeArguments.length === 0) {
|
|
1109
1866
|
// Check if this type exists in our graph - convert to full path format
|
|
1110
|
-
const
|
|
1111
|
-
if (
|
|
1867
|
+
const matchedType = this.graph.findTypeByName(typeName);
|
|
1868
|
+
if (matchedType) {
|
|
1112
1869
|
// Convert full path with dots to underscores: Usages.UsageEntry -> Usages_UsageEntry
|
|
1113
|
-
return
|
|
1870
|
+
return matchedType.fullPath.replace(/\./g, '_');
|
|
1114
1871
|
}
|
|
1115
1872
|
return typeName;
|
|
1116
1873
|
}
|
|
1117
1874
|
// Build generic type arguments
|
|
1118
|
-
const
|
|
1119
|
-
return `${typeName}<${
|
|
1875
|
+
const typeArgs = typeRef.typeArguments.map(arg => this.inferType(arg));
|
|
1876
|
+
return `${typeName}<${typeArgs.join(', ')}>`;
|
|
1120
1877
|
}
|
|
1121
1878
|
case ts.SyntaxKind.UnionType: {
|
|
1122
1879
|
// Handle union types like 'a' | 'b' | 'c'
|
|
@@ -1139,12 +1896,12 @@ class MnemonicaAnalyzer {
|
|
|
1139
1896
|
case ts.SyntaxKind.OptionalType: {
|
|
1140
1897
|
// Handle optional element in tuple: string?
|
|
1141
1898
|
const optionalType = typeNode;
|
|
1142
|
-
return this.inferType(optionalType.type)
|
|
1899
|
+
return `${this.inferType(optionalType.type)}?`;
|
|
1143
1900
|
}
|
|
1144
1901
|
case ts.SyntaxKind.RestType: {
|
|
1145
1902
|
// Handle rest element: ...T
|
|
1146
1903
|
const restType = typeNode;
|
|
1147
|
-
return
|
|
1904
|
+
return `...${this.inferType(restType.type)}`;
|
|
1148
1905
|
}
|
|
1149
1906
|
case ts.SyntaxKind.ParenthesizedType: {
|
|
1150
1907
|
// Handle parenthesized types: (A | B)
|
|
@@ -1442,8 +2199,8 @@ class MnemonicaAnalyzer {
|
|
|
1442
2199
|
}
|
|
1443
2200
|
}
|
|
1444
2201
|
/**
|
|
1445
|
-
|
|
1446
|
-
|
|
2202
|
+
* Collect usage information for type references
|
|
2203
|
+
*/
|
|
1447
2204
|
collectUsage(node, sourceFile) {
|
|
1448
2205
|
// Check for new Type() instantiation
|
|
1449
2206
|
if (ts.isNewExpression(node) && node.expression) {
|
|
@@ -1489,13 +2246,12 @@ class MnemonicaAnalyzer {
|
|
|
1489
2246
|
}
|
|
1490
2247
|
}
|
|
1491
2248
|
}
|
|
1492
|
-
// Check for lookup('TypeName') calls
|
|
2249
|
+
// Check for lookup('TypeName') or lookup(source, 'TypeName') calls
|
|
1493
2250
|
if (ts.isCallExpression(node) && node.expression) {
|
|
1494
2251
|
const funcName = this.getFunctionName(node.expression);
|
|
1495
2252
|
if (funcName === 'lookup' && node.arguments.length > 0) {
|
|
1496
|
-
const
|
|
1497
|
-
if (
|
|
1498
|
-
const typePath = firstArg.text;
|
|
2253
|
+
const typePath = this.resolveLookupPath(node);
|
|
2254
|
+
if (typePath) {
|
|
1499
2255
|
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1500
2256
|
this.addUsage(typePath, {
|
|
1501
2257
|
location: `${sourceFile.fileName}:${line + 1}:${character + 1}`,
|
|
@@ -1509,8 +2265,8 @@ class MnemonicaAnalyzer {
|
|
|
1509
2265
|
}
|
|
1510
2266
|
}
|
|
1511
2267
|
/**
|
|
1512
|
-
|
|
1513
|
-
|
|
2268
|
+
* Get function name from expression (identifier or property access)
|
|
2269
|
+
*/
|
|
1514
2270
|
getFunctionName(expr) {
|
|
1515
2271
|
if (ts.isIdentifier(expr)) {
|
|
1516
2272
|
return expr.text;
|
|
@@ -1521,8 +2277,8 @@ class MnemonicaAnalyzer {
|
|
|
1521
2277
|
return undefined;
|
|
1522
2278
|
}
|
|
1523
2279
|
/**
|
|
1524
|
-
|
|
1525
|
-
|
|
2280
|
+
* Add a usage to the collection
|
|
2281
|
+
*/
|
|
1526
2282
|
addUsage(typePath, usage) {
|
|
1527
2283
|
// Only track usages of mnemonica-defined types
|
|
1528
2284
|
if (!this.definitions.has(typePath)) {
|
|
@@ -1624,7 +2380,9 @@ class MnemonicaAnalyzer {
|
|
|
1624
2380
|
return;
|
|
1625
2381
|
}
|
|
1626
2382
|
// Adapters: createDiveInterceptor, createDivePlugin, createDiveMiddleware
|
|
1627
|
-
if (funcName === 'createDiveInterceptor' ||
|
|
2383
|
+
if (funcName === 'createDiveInterceptor' ||
|
|
2384
|
+
funcName === 'createDivePlugin' ||
|
|
2385
|
+
funcName === 'createDiveMiddleware') {
|
|
1628
2386
|
this.addEDS('unknown', {
|
|
1629
2387
|
location,
|
|
1630
2388
|
kind: 'adapterUse',
|
|
@@ -1670,7 +2428,11 @@ class MnemonicaAnalyzer {
|
|
|
1670
2428
|
this.edsUsages.set(typePath, []);
|
|
1671
2429
|
}
|
|
1672
2430
|
const existing = this.edsUsages.get(typePath);
|
|
1673
|
-
const isDuplicate = existing.some(e =>
|
|
2431
|
+
const isDuplicate = existing.some(e => {
|
|
2432
|
+
return e.location === info.location &&
|
|
2433
|
+
e.kind === info.kind &&
|
|
2434
|
+
e.code === info.code;
|
|
2435
|
+
});
|
|
1674
2436
|
if (!isDuplicate) {
|
|
1675
2437
|
existing.push(info);
|
|
1676
2438
|
}
|
|
@@ -1730,7 +2492,7 @@ class MnemonicaAnalyzer {
|
|
|
1730
2492
|
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1731
2493
|
const code = node.getText(sourceFile).slice(0, 100);
|
|
1732
2494
|
// Skip if this is a type constructor access (e.g., UserType.define)
|
|
1733
|
-
if (propName === 'define') {
|
|
2495
|
+
if (propName === 'define' || propName === 'lazy') {
|
|
1734
2496
|
return;
|
|
1735
2497
|
}
|
|
1736
2498
|
this.addFlow(objectType, {
|
|
@@ -1816,7 +2578,7 @@ class MnemonicaAnalyzer {
|
|
|
1816
2578
|
const location = `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
|
1817
2579
|
const code = node.getText(sourceFile).slice(0, 100);
|
|
1818
2580
|
// Skip if this is a type constructor call (e.g., new UserType())
|
|
1819
|
-
if (methodName === 'define') {
|
|
2581
|
+
if (methodName === 'define' || methodName === 'lazy') {
|
|
1820
2582
|
return;
|
|
1821
2583
|
}
|
|
1822
2584
|
this.addFlow(objectType, {
|
|
@@ -1945,7 +2707,11 @@ class MnemonicaAnalyzer {
|
|
|
1945
2707
|
this.flowUsages.set(typePath, []);
|
|
1946
2708
|
}
|
|
1947
2709
|
const existing = this.flowUsages.get(typePath);
|
|
1948
|
-
const isDuplicate = existing.some(e =>
|
|
2710
|
+
const isDuplicate = existing.some(e => {
|
|
2711
|
+
return e.location === info.location &&
|
|
2712
|
+
e.kind === info.kind &&
|
|
2713
|
+
e.code === info.code;
|
|
2714
|
+
});
|
|
1949
2715
|
if (!isDuplicate) {
|
|
1950
2716
|
existing.push(info);
|
|
1951
2717
|
}
|
|
@@ -1970,8 +2736,8 @@ class MnemonicaAnalyzer {
|
|
|
1970
2736
|
return undefined;
|
|
1971
2737
|
}
|
|
1972
2738
|
/**
|
|
1973
|
-
|
|
1974
|
-
|
|
2739
|
+
* Resolve full type path from property access
|
|
2740
|
+
*/
|
|
1975
2741
|
resolveTypePath(expr) {
|
|
1976
2742
|
const chain = this.getPropertyChain(expr);
|
|
1977
2743
|
if (chain.length === 0)
|
|
@@ -1991,15 +2757,15 @@ class MnemonicaAnalyzer {
|
|
|
1991
2757
|
return fullPath;
|
|
1992
2758
|
}
|
|
1993
2759
|
/**
|
|
1994
|
-
|
|
1995
|
-
|
|
2760
|
+
* Check if a name looks like a type (starts with uppercase)
|
|
2761
|
+
*/
|
|
1996
2762
|
isLikelyTypeName(name) {
|
|
1997
2763
|
return name[0] >= 'A' && name[0] <= 'Z';
|
|
1998
2764
|
}
|
|
1999
2765
|
/**
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2766
|
+
* Resolve a constructor parameter type, expanding inline object literals
|
|
2767
|
+
* and type aliases where possible.
|
|
2768
|
+
*/
|
|
2003
2769
|
resolveConstructorParamType(typeNode) {
|
|
2004
2770
|
if (!typeNode)
|
|
2005
2771
|
return undefined;
|
|
@@ -2028,15 +2794,15 @@ class MnemonicaAnalyzer {
|
|
|
2028
2794
|
// If not an object type alias, return the type name with args
|
|
2029
2795
|
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
2030
2796
|
const args = typeNode.typeArguments.map(arg => this.inferType(arg));
|
|
2031
|
-
return typeName
|
|
2797
|
+
return `${typeName}<${args.join(', ')}>`;
|
|
2032
2798
|
}
|
|
2033
2799
|
return typeName;
|
|
2034
2800
|
}
|
|
2035
2801
|
return undefined;
|
|
2036
2802
|
}
|
|
2037
2803
|
/**
|
|
2038
|
-
|
|
2039
|
-
|
|
2804
|
+
* Extract constructor parameters from a class-like node.
|
|
2805
|
+
*/
|
|
2040
2806
|
extractClassConstructorParams(classLike) {
|
|
2041
2807
|
const params = [];
|
|
2042
2808
|
for (const member of classLike.members) {
|
|
@@ -2061,25 +2827,35 @@ class MnemonicaAnalyzer {
|
|
|
2061
2827
|
return params;
|
|
2062
2828
|
}
|
|
2063
2829
|
/**
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2830
|
+
* Extract constructor parameters from define() call
|
|
2831
|
+
* This is used for TypeRegistry constructor signatures
|
|
2832
|
+
* Preserves parameter names and expands object types to their structure
|
|
2833
|
+
*/
|
|
2068
2834
|
extractConstructorParams(call) {
|
|
2835
|
+
const constructorExpr = this.extractConstructorExpression(call);
|
|
2836
|
+
if (!constructorExpr) {
|
|
2837
|
+
return [];
|
|
2838
|
+
}
|
|
2839
|
+
const result = this.extractConstructorParamsFromConstructor(constructorExpr);
|
|
2840
|
+
return result;
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Extract constructor parameters from a constructor expression.
|
|
2844
|
+
*/
|
|
2845
|
+
extractConstructorParamsFromConstructor(constructorExpr) {
|
|
2069
2846
|
const params = [];
|
|
2070
|
-
const handlerArg = call.arguments[1];
|
|
2071
|
-
if (!handlerArg)
|
|
2072
|
-
return params;
|
|
2073
2847
|
// Handle function expression or arrow function
|
|
2074
|
-
if (ts.isFunctionExpression(
|
|
2848
|
+
if (ts.isFunctionExpression(constructorExpr) || ts.isArrowFunction(constructorExpr)) {
|
|
2075
2849
|
// Look for constructor parameters (second param after `this`)
|
|
2076
2850
|
// Patterns: function(this: Type, data: { ... }) or (this: Type, data: { ... }) =>
|
|
2077
|
-
for (let i = 0; i <
|
|
2078
|
-
const param =
|
|
2851
|
+
for (let i = 0; i < constructorExpr.parameters.length; i++) {
|
|
2852
|
+
const param = constructorExpr.parameters[i];
|
|
2079
2853
|
if (!param.type)
|
|
2080
2854
|
continue;
|
|
2081
2855
|
// Skip `this` parameter (first param)
|
|
2082
|
-
if (i === 0 &&
|
|
2856
|
+
if (i === 0 &&
|
|
2857
|
+
param.name.kind === ts.SyntaxKind.Identifier &&
|
|
2858
|
+
param.name.text === 'this') {
|
|
2083
2859
|
continue;
|
|
2084
2860
|
}
|
|
2085
2861
|
// Get parameter name and expand its type
|
|
@@ -2093,8 +2869,8 @@ class MnemonicaAnalyzer {
|
|
|
2093
2869
|
}
|
|
2094
2870
|
}
|
|
2095
2871
|
// Handle class expression - check constructor method
|
|
2096
|
-
if (ts.isClassExpression(
|
|
2097
|
-
const classParams = this.extractClassConstructorParams(
|
|
2872
|
+
if (ts.isClassExpression(constructorExpr)) {
|
|
2873
|
+
const classParams = this.extractClassConstructorParams(constructorExpr);
|
|
2098
2874
|
for (const param of classParams) {
|
|
2099
2875
|
params.push(param);
|
|
2100
2876
|
}
|