gitnexus 1.4.7 → 1.4.8
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 +22 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup.js +78 -1
- package/dist/config/supported-languages.d.ts +30 -0
- package/dist/config/supported-languages.js +30 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +2 -1
- package/dist/core/ingestion/ast-helpers.d.ts +80 -0
- package/dist/core/ingestion/ast-helpers.js +738 -0
- package/dist/core/ingestion/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/call-analysis.js +490 -0
- package/dist/core/ingestion/call-processor.d.ts +48 -1
- package/dist/core/ingestion/call-processor.js +368 -7
- package/dist/core/ingestion/call-routing.d.ts +6 -0
- package/dist/core/ingestion/entry-point-scoring.js +36 -26
- package/dist/core/ingestion/framework-detection.d.ts +10 -2
- package/dist/core/ingestion/framework-detection.js +49 -12
- package/dist/core/ingestion/heritage-processor.js +47 -49
- package/dist/core/ingestion/import-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +103 -194
- package/dist/core/ingestion/import-resolution.d.ts +101 -0
- package/dist/core/ingestion/import-resolution.js +251 -0
- package/dist/core/ingestion/language-config.d.ts +3 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +8 -3
- package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
- package/dist/core/ingestion/named-binding-extraction.js +89 -79
- package/dist/core/ingestion/parsing-processor.d.ts +2 -2
- package/dist/core/ingestion/parsing-processor.js +14 -73
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +421 -4
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +7 -4
- package/dist/core/ingestion/resolvers/index.d.ts +1 -1
- package/dist/core/ingestion/resolvers/index.js +1 -1
- package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
- package/dist/core/ingestion/resolvers/jvm.js +25 -9
- package/dist/core/ingestion/resolvers/php.d.ts +14 -0
- package/dist/core/ingestion/resolvers/php.js +43 -3
- package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
- package/dist/core/ingestion/resolvers/utils.js +16 -0
- package/dist/core/ingestion/symbol-table.d.ts +16 -0
- package/dist/core/ingestion/symbol-table.js +20 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
- package/dist/core/ingestion/tree-sitter-queries.js +43 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +419 -96
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
- package/dist/core/ingestion/type-extractors/csharp.js +149 -16
- package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/index.js +1 -1
- package/dist/core/ingestion/type-extractors/jvm.js +169 -66
- package/dist/core/ingestion/type-extractors/rust.js +35 -1
- package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/shared.js +5 -10
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
- package/dist/core/ingestion/type-extractors/typescript.js +141 -9
- package/dist/core/ingestion/utils.d.ts +2 -120
- package/dist/core/ingestion/utils.js +3 -1051
- package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
- package/dist/core/ingestion/workers/parse-worker.js +66 -87
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +10 -0
- package/dist/core/lbug/lbug-adapter.js +69 -4
- package/dist/core/lbug/schema.d.ts +5 -3
- package/dist/core/lbug/schema.js +26 -2
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.js +12 -1
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +133 -29
- package/dist/mcp/resources.js +2 -0
- package/dist/mcp/tools.js +2 -2
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- package/package.json +10 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, isBuiltInOrNoise } from './utils.js';
|
|
1
|
+
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, CALL_EXPRESSION_TYPES, isBuiltInOrNoise } from './utils.js';
|
|
2
2
|
import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
|
|
3
3
|
import { extractSimpleTypeName, extractVarName, stripNullable, extractReturnTypeName } from './type-extractors/shared.js';
|
|
4
4
|
/** File-level scope key */
|
|
@@ -12,16 +12,21 @@ const findTypeIdentifierChild = (node) => {
|
|
|
12
12
|
}
|
|
13
13
|
return null;
|
|
14
14
|
};
|
|
15
|
-
/** AST node types that represent mutually exclusive branch containers for pattern bindings.
|
|
16
|
-
|
|
15
|
+
/** AST node types that represent mutually exclusive branch containers for pattern bindings.
|
|
16
|
+
* Includes both multi-arm pattern-match branches AND if-statement bodies for null-check narrowing. */
|
|
17
|
+
const NARROWING_BRANCH_TYPES = new Set([
|
|
17
18
|
'when_entry', // Kotlin when
|
|
18
19
|
'switch_block_label', // Java switch (enhanced)
|
|
20
|
+
'if_statement', // TS/JS, Java, C/C++
|
|
21
|
+
'if_expression', // Kotlin (if is an expression)
|
|
22
|
+
'statement_block', // TS/JS: { ... } body of if
|
|
23
|
+
'control_structure_body', // Kotlin: body of if
|
|
19
24
|
]);
|
|
20
25
|
/** Walk up the AST from a pattern node to find the enclosing branch container. */
|
|
21
|
-
const
|
|
26
|
+
const findNarrowingBranchScope = (node) => {
|
|
22
27
|
let current = node.parent;
|
|
23
28
|
while (current) {
|
|
24
|
-
if (
|
|
29
|
+
if (NARROWING_BRANCH_TYPES.has(current.type))
|
|
25
30
|
return current;
|
|
26
31
|
if (FUNCTION_NODE_TYPES.has(current.type))
|
|
27
32
|
return undefined;
|
|
@@ -100,6 +105,24 @@ const findEnclosingClassName = (node) => {
|
|
|
100
105
|
}
|
|
101
106
|
return undefined;
|
|
102
107
|
};
|
|
108
|
+
/** Keywords that refer to the current instance across languages. */
|
|
109
|
+
const THIS_RECEIVERS = new Set(['this', 'self', '$this', 'Me']);
|
|
110
|
+
/**
|
|
111
|
+
* If a pending assignment's receiver is this/self/$this/Me, substitute the
|
|
112
|
+
* enclosing class name. Returns the item unchanged for non-receiver kinds
|
|
113
|
+
* or when the receiver is not a this-keyword. Properties are readonly in the
|
|
114
|
+
* discriminated union, so a new object is returned when substitution occurs.
|
|
115
|
+
*/
|
|
116
|
+
const substituteThisReceiver = (item, node) => {
|
|
117
|
+
if (item.kind !== 'fieldAccess' && item.kind !== 'methodCallResult')
|
|
118
|
+
return item;
|
|
119
|
+
if (!THIS_RECEIVERS.has(item.receiver))
|
|
120
|
+
return item;
|
|
121
|
+
const className = findEnclosingClassName(node);
|
|
122
|
+
if (!className)
|
|
123
|
+
return item;
|
|
124
|
+
return { ...item, receiver: className };
|
|
125
|
+
};
|
|
103
126
|
/**
|
|
104
127
|
* Walk up the AST to find the enclosing class, then extract its parent class name
|
|
105
128
|
* from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
|
|
@@ -297,78 +320,314 @@ const SKIP_SUBTREE_TYPES = new Set([
|
|
|
297
320
|
'regex', 'regex_pattern',
|
|
298
321
|
]);
|
|
299
322
|
const CLASS_LIKE_TYPES = new Set(['Class', 'Struct', 'Interface']);
|
|
323
|
+
/** Memoize class definition lookups during fixpoint iteration.
|
|
324
|
+
* SymbolTable is immutable during type resolution, so results never change.
|
|
325
|
+
* Eliminates redundant array allocations + filter scans across iterations. */
|
|
326
|
+
const createClassDefCache = (symbolTable) => {
|
|
327
|
+
const cache = new Map();
|
|
328
|
+
return (typeName) => {
|
|
329
|
+
let result = cache.get(typeName);
|
|
330
|
+
if (result === undefined) {
|
|
331
|
+
result = symbolTable
|
|
332
|
+
? symbolTable.lookupFuzzy(typeName).filter(d => CLASS_LIKE_TYPES.has(d.type))
|
|
333
|
+
: [];
|
|
334
|
+
cache.set(typeName, result);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
/** AST node types representing constructor expressions across languages.
|
|
340
|
+
* Note: C# also has `implicit_object_creation_expression` (`new()` with type
|
|
341
|
+
* inference) which is NOT captured — the type is inferred, not explicit.
|
|
342
|
+
* Kotlin constructors use `call_expression` (no `new` keyword) — not detected. */
|
|
343
|
+
const CONSTRUCTOR_EXPR_TYPES = new Set([
|
|
344
|
+
'new_expression', // TS/JS/C++: new Dog()
|
|
345
|
+
'object_creation_expression', // Java/C#: new Dog()
|
|
346
|
+
]);
|
|
347
|
+
/** Extract the constructor class name from a declaration node's initializer.
|
|
348
|
+
* Searches for new_expression / object_creation_expression in the node's subtree.
|
|
349
|
+
* Returns the class name or undefined if no constructor is found.
|
|
350
|
+
* Depth-limited to 5 to avoid expensive traversals. */
|
|
351
|
+
const extractConstructorTypeName = (node, depth = 0) => {
|
|
352
|
+
if (depth > 5)
|
|
353
|
+
return undefined;
|
|
354
|
+
if (CONSTRUCTOR_EXPR_TYPES.has(node.type)) {
|
|
355
|
+
// Java/C#: object_creation_expression has 'type' field
|
|
356
|
+
const typeField = node.childForFieldName('type');
|
|
357
|
+
if (typeField)
|
|
358
|
+
return extractSimpleTypeName(typeField);
|
|
359
|
+
// TS/JS: new_expression has 'constructor' field (but tree-sitter often just has identifier child)
|
|
360
|
+
const ctorField = node.childForFieldName('constructor');
|
|
361
|
+
if (ctorField)
|
|
362
|
+
return extractSimpleTypeName(ctorField);
|
|
363
|
+
// Fallback: first named child is often the class identifier
|
|
364
|
+
if (node.firstNamedChild)
|
|
365
|
+
return extractSimpleTypeName(node.firstNamedChild);
|
|
366
|
+
}
|
|
367
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
368
|
+
const child = node.namedChild(i);
|
|
369
|
+
if (!child)
|
|
370
|
+
continue;
|
|
371
|
+
// Don't descend into nested functions/classes or call expressions (prevents
|
|
372
|
+
// finding constructor args inside method calls, e.g. processAll(new Dog()))
|
|
373
|
+
if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)
|
|
374
|
+
|| CALL_EXPRESSION_TYPES.has(child.type))
|
|
375
|
+
continue;
|
|
376
|
+
const result = extractConstructorTypeName(child, depth + 1);
|
|
377
|
+
if (result)
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
};
|
|
382
|
+
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
|
|
383
|
+
const MAX_MRO_DEPTH = 5;
|
|
384
|
+
/** Check if `child` is a subclass of `parent` using the parentMap.
|
|
385
|
+
* BFS up from child, depth-limited (5), cycle-safe. */
|
|
386
|
+
export const isSubclassOf = (child, parent, parentMap) => {
|
|
387
|
+
if (!parentMap || child === parent)
|
|
388
|
+
return false;
|
|
389
|
+
const visited = new Set([child]);
|
|
390
|
+
let current = [child];
|
|
391
|
+
for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
|
|
392
|
+
const next = [];
|
|
393
|
+
for (const cls of current) {
|
|
394
|
+
const parents = parentMap.get(cls);
|
|
395
|
+
if (!parents)
|
|
396
|
+
continue;
|
|
397
|
+
for (const p of parents) {
|
|
398
|
+
if (p === parent)
|
|
399
|
+
return true;
|
|
400
|
+
if (!visited.has(p)) {
|
|
401
|
+
visited.add(p);
|
|
402
|
+
next.push(p);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
current = next;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
};
|
|
410
|
+
/** Walk up the parent class chain to find a field or method on an ancestor.
|
|
411
|
+
* BFS-like traversal with depth limit and cycle detection. First match wins.
|
|
412
|
+
* Used by resolveFieldType and resolveMethodReturnType when direct lookup fails. */
|
|
413
|
+
const walkParentChain = (typeName, parentMap, getClassDefs, lookupOnClass) => {
|
|
414
|
+
if (!parentMap)
|
|
415
|
+
return undefined;
|
|
416
|
+
const visited = new Set([typeName]);
|
|
417
|
+
let current = [typeName];
|
|
418
|
+
for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
|
|
419
|
+
const next = [];
|
|
420
|
+
for (const cls of current) {
|
|
421
|
+
const parents = parentMap.get(cls);
|
|
422
|
+
if (!parents)
|
|
423
|
+
continue;
|
|
424
|
+
for (const parent of parents) {
|
|
425
|
+
if (visited.has(parent))
|
|
426
|
+
continue;
|
|
427
|
+
visited.add(parent);
|
|
428
|
+
const parentDefs = getClassDefs(parent);
|
|
429
|
+
if (parentDefs.length === 1) {
|
|
430
|
+
const result = lookupOnClass(parentDefs[0].nodeId);
|
|
431
|
+
if (result !== undefined)
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
next.push(parent);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
current = next;
|
|
438
|
+
}
|
|
439
|
+
return undefined;
|
|
440
|
+
};
|
|
300
441
|
/** Resolve a field's declared type given a receiver variable and field name.
|
|
301
442
|
* Uses SymbolTable to find the class nodeId for the receiver's type, then
|
|
302
|
-
* looks up the field via the eagerly-populated fieldByOwner index.
|
|
303
|
-
|
|
443
|
+
* looks up the field via the eagerly-populated fieldByOwner index.
|
|
444
|
+
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
|
445
|
+
const resolveFieldType = (receiver, field, scopeEnv, symbolTable, getClassDefs, parentMap) => {
|
|
304
446
|
if (!symbolTable)
|
|
305
447
|
return undefined;
|
|
306
448
|
const receiverType = scopeEnv.get(receiver);
|
|
307
449
|
if (!receiverType)
|
|
308
450
|
return undefined;
|
|
309
|
-
const
|
|
310
|
-
.filter(d => CLASS_LIKE_TYPES.has(d.type));
|
|
451
|
+
const lookup = getClassDefs
|
|
452
|
+
?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
|
453
|
+
const classDefs = lookup(receiverType);
|
|
311
454
|
if (classDefs.length !== 1)
|
|
312
455
|
return undefined;
|
|
456
|
+
// Direct lookup first
|
|
313
457
|
const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
|
|
314
|
-
if (
|
|
315
|
-
return
|
|
316
|
-
|
|
458
|
+
if (fieldDef?.declaredType)
|
|
459
|
+
return extractReturnTypeName(fieldDef.declaredType);
|
|
460
|
+
// MRO parent chain walking on miss
|
|
461
|
+
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
|
462
|
+
const f = symbolTable.lookupFieldByOwner(nodeId, field);
|
|
463
|
+
return f?.declaredType ? extractReturnTypeName(f.declaredType) : undefined;
|
|
464
|
+
});
|
|
465
|
+
return inherited;
|
|
317
466
|
};
|
|
318
467
|
/** Resolve a method's return type given a receiver variable and method name.
|
|
319
468
|
* Uses SymbolTable to find class nodeIds for the receiver's type, then
|
|
320
|
-
* looks up the method via lookupFuzzyCallable filtered by ownerId.
|
|
321
|
-
|
|
469
|
+
* looks up the method via lookupFuzzyCallable filtered by ownerId.
|
|
470
|
+
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
|
471
|
+
const resolveMethodReturnType = (receiver, method, scopeEnv, symbolTable, getClassDefs, parentMap) => {
|
|
322
472
|
if (!symbolTable)
|
|
323
473
|
return undefined;
|
|
324
474
|
const receiverType = scopeEnv.get(receiver);
|
|
325
475
|
if (!receiverType)
|
|
326
476
|
return undefined;
|
|
327
|
-
const
|
|
328
|
-
.filter(d => CLASS_LIKE_TYPES.has(d.type));
|
|
477
|
+
const lookup = getClassDefs
|
|
478
|
+
?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
|
479
|
+
const classDefs = lookup(receiverType);
|
|
329
480
|
if (classDefs.length === 0)
|
|
330
481
|
return undefined;
|
|
482
|
+
// Direct lookup first
|
|
331
483
|
const classNodeIds = new Set(classDefs.map(d => d.nodeId));
|
|
332
484
|
const methods = symbolTable.lookupFuzzyCallable(method)
|
|
333
485
|
.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
|
334
|
-
if (methods.length
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
486
|
+
if (methods.length === 1 && methods[0].returnType) {
|
|
487
|
+
return extractReturnTypeName(methods[0].returnType);
|
|
488
|
+
}
|
|
489
|
+
// MRO parent chain walking on miss
|
|
490
|
+
if (methods.length === 0) {
|
|
491
|
+
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
|
492
|
+
const parentMethods = symbolTable.lookupFuzzyCallable(method)
|
|
493
|
+
.filter(d => d.ownerId === nodeId);
|
|
494
|
+
if (parentMethods.length !== 1 || !parentMethods[0].returnType)
|
|
495
|
+
return undefined;
|
|
496
|
+
return extractReturnTypeName(parentMethods[0].returnType);
|
|
497
|
+
});
|
|
498
|
+
return inherited;
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
339
501
|
};
|
|
340
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
|
|
504
|
+
* fieldAccess, methodCallResult) until no new bindings are produced.
|
|
505
|
+
* Handles arbitrary-depth mixed chains:
|
|
506
|
+
* const user = getUser(); // callResult → User
|
|
507
|
+
* const addr = user.address; // fieldAccess → Address (depends on user)
|
|
508
|
+
* const city = addr.getCity(); // methodCallResult → City (depends on addr)
|
|
509
|
+
* const alias = city; // copy → City (depends on city)
|
|
510
|
+
* Data flow: SymbolTable (immutable) + scopeEnv → resolve → scopeEnv.
|
|
511
|
+
* Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
|
|
512
|
+
*/
|
|
513
|
+
const MAX_FIXPOINT_ITERATIONS = 10;
|
|
514
|
+
const resolveFixpointBindings = (pendingItems, env, returnTypeLookup, symbolTable, parentMap) => {
|
|
515
|
+
if (pendingItems.length === 0)
|
|
516
|
+
return;
|
|
517
|
+
const getClassDefs = createClassDefCache(symbolTable);
|
|
518
|
+
const resolved = new Set();
|
|
519
|
+
for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
|
|
520
|
+
let changed = false;
|
|
521
|
+
for (let i = 0; i < pendingItems.length; i++) {
|
|
522
|
+
if (resolved.has(i))
|
|
523
|
+
continue;
|
|
524
|
+
const item = pendingItems[i];
|
|
525
|
+
const scopeEnv = env.get(item.scope);
|
|
526
|
+
if (!scopeEnv || scopeEnv.has(item.lhs)) {
|
|
527
|
+
resolved.add(i);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
let typeName;
|
|
531
|
+
switch (item.kind) {
|
|
532
|
+
case 'callResult':
|
|
533
|
+
typeName = returnTypeLookup.lookupReturnType(item.callee);
|
|
534
|
+
break;
|
|
535
|
+
case 'copy':
|
|
536
|
+
typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
|
|
537
|
+
break;
|
|
538
|
+
case 'fieldAccess':
|
|
539
|
+
typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable, getClassDefs, parentMap);
|
|
540
|
+
break;
|
|
541
|
+
case 'methodCallResult':
|
|
542
|
+
typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable, getClassDefs, parentMap);
|
|
543
|
+
break;
|
|
544
|
+
default: {
|
|
545
|
+
// Exhaustive check: TypeScript will error here if a new PendingAssignment
|
|
546
|
+
// kind is added without handling it in the switch.
|
|
547
|
+
const _exhaustive = item;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (typeName) {
|
|
552
|
+
scopeEnv.set(item.lhs, typeName);
|
|
553
|
+
resolved.add(i);
|
|
554
|
+
changed = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!changed)
|
|
558
|
+
break;
|
|
559
|
+
if (iter === MAX_FIXPOINT_ITERATIONS - 1 && process.env.GITNEXUS_DEBUG) {
|
|
560
|
+
const unresolved = pendingItems.length - resolved.size;
|
|
561
|
+
if (unresolved > 0) {
|
|
562
|
+
console.warn(`[type-env] fixpoint hit iteration cap (${MAX_FIXPOINT_ITERATIONS}), ${unresolved} items unresolved`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
/** Seed cross-file type bindings into the file scope.
|
|
568
|
+
* MUST be called AFTER walk() completes so that local declarations
|
|
569
|
+
* (Tier 0/1) always take precedence over imported bindings (first-writer-wins). */
|
|
570
|
+
function seedImportedBindings(env, importedBindings) {
|
|
571
|
+
let fileEnv = env.get(FILE_SCOPE);
|
|
572
|
+
if (!fileEnv) {
|
|
573
|
+
fileEnv = new Map();
|
|
574
|
+
env.set(FILE_SCOPE, fileEnv);
|
|
575
|
+
}
|
|
576
|
+
for (const [name, type] of importedBindings) {
|
|
577
|
+
if (!fileEnv.has(name)) {
|
|
578
|
+
fileEnv.set(name, type);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
export const buildTypeEnv = (tree, language, options) => {
|
|
583
|
+
const symbolTable = options?.symbolTable;
|
|
584
|
+
const parentMap = options?.parentMap;
|
|
341
585
|
const env = new Map();
|
|
342
586
|
const patternOverrides = new Map();
|
|
587
|
+
// Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH
|
|
588
|
+
// a base type annotation AND a more specific constructor initializer.
|
|
589
|
+
// e.g., `Animal a = new Dog()` → constructorTypeMap.set('func@42\0a', 'Dog')
|
|
590
|
+
const constructorTypeMap = new Map();
|
|
343
591
|
const localClassNames = new Set();
|
|
344
592
|
const classNames = createClassNameLookup(localClassNames, symbolTable);
|
|
345
593
|
const config = typeConfigs[language];
|
|
346
594
|
const bindings = [];
|
|
347
|
-
// Build ReturnTypeLookup
|
|
348
|
-
//
|
|
595
|
+
// Build ReturnTypeLookup: SymbolTable is authoritative when it has an unambiguous match.
|
|
596
|
+
// Cross-file importedReturnTypes are consulted ONLY when SymbolTable has 0 matches.
|
|
597
|
+
// Ambiguous (2+) → undefined, no cross-file fallback (conservative, local-first principle).
|
|
349
598
|
const returnTypeLookup = {
|
|
350
599
|
lookupReturnType(callee) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
600
|
+
// SymbolTable is authoritative when it has an unambiguous match
|
|
601
|
+
if (symbolTable) {
|
|
602
|
+
if (isBuiltInOrNoise(callee))
|
|
603
|
+
return undefined;
|
|
604
|
+
const callables = symbolTable.lookupFuzzyCallable(callee);
|
|
605
|
+
if (callables.length === 1) {
|
|
606
|
+
const rawReturn = callables[0].returnType;
|
|
607
|
+
if (rawReturn)
|
|
608
|
+
return extractReturnTypeName(rawReturn);
|
|
609
|
+
}
|
|
610
|
+
// Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
|
|
611
|
+
if (callables.length > 1)
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
// No match (0 results or no symbolTable) → fall back to cross-file
|
|
615
|
+
return options?.importedReturnTypes?.get(callee);
|
|
362
616
|
},
|
|
363
617
|
lookupRawReturnType(callee) {
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return undefined
|
|
371
|
-
|
|
618
|
+
if (symbolTable) {
|
|
619
|
+
if (isBuiltInOrNoise(callee))
|
|
620
|
+
return undefined;
|
|
621
|
+
const callables = symbolTable.lookupFuzzyCallable(callee);
|
|
622
|
+
if (callables.length === 1)
|
|
623
|
+
return callables[0].returnType;
|
|
624
|
+
// Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
|
|
625
|
+
if (callables.length > 1)
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
// Cross-file fallback uses importedRawReturnTypes (raw declared types, e.g., 'User[]')
|
|
629
|
+
// NOT importedReturnTypes (which contains processed/simple types via extractReturnTypeName)
|
|
630
|
+
return options?.importedRawReturnTypes?.get(callee);
|
|
372
631
|
}
|
|
373
632
|
};
|
|
374
633
|
// Pre-compute combined set of node types that need extractTypeBinding.
|
|
@@ -381,6 +640,9 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
381
640
|
// methodCallResult items during walk(), then iterates until no new bindings are produced.
|
|
382
641
|
// Handles arbitrary-depth mixed chains: callResult → fieldAccess → methodCallResult → copy.
|
|
383
642
|
const pendingItems = [];
|
|
643
|
+
// For-loop nodes whose iterable was unresolved at walk-time. Replayed after the fixpoint
|
|
644
|
+
// resolves the iterable's type, bridging the walk-time/fixpoint gap (Phase 10 / ex-9B).
|
|
645
|
+
const pendingForLoops = [];
|
|
384
646
|
// Maps `scope\0varName` → the type annotation AST node from the original declaration.
|
|
385
647
|
// Allows pattern extractors to navigate back to the declaration's generic type arguments
|
|
386
648
|
// (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
|
|
@@ -432,7 +694,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
432
694
|
fallbackName = child;
|
|
433
695
|
}
|
|
434
696
|
if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
|
|
435
|
-
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
|
697
|
+
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
|
698
|
+
|| child.type === 'nullable_type')) {
|
|
436
699
|
fallbackType = child;
|
|
437
700
|
}
|
|
438
701
|
}
|
|
@@ -450,8 +713,14 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
450
713
|
// Checked before declarationNodeTypes — loop variables are not declarations.
|
|
451
714
|
if (config.forLoopNodeTypes?.has(node.type)) {
|
|
452
715
|
if (config.extractForLoopBinding) {
|
|
716
|
+
const sizeBefore = scopeEnv.size;
|
|
453
717
|
const forLoopCtx = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
|
|
454
718
|
config.extractForLoopBinding(node, forLoopCtx);
|
|
719
|
+
// If no new binding was produced, the iterable's type may not yet be resolved.
|
|
720
|
+
// Store for post-fixpoint replay (Phase 10 / ex-9B loop-fixpoint bridge).
|
|
721
|
+
if (scopeEnv.size === sizeBefore) {
|
|
722
|
+
pendingForLoops.push({ node, scope });
|
|
723
|
+
}
|
|
455
724
|
}
|
|
456
725
|
return;
|
|
457
726
|
}
|
|
@@ -477,8 +746,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
477
746
|
}
|
|
478
747
|
}
|
|
479
748
|
}
|
|
480
|
-
if (wrapped)
|
|
749
|
+
if (wrapped) {
|
|
481
750
|
typeNode = wrapped.childForFieldName('type');
|
|
751
|
+
// Kotlin: variable_declaration stores the type as user_type / nullable_type
|
|
752
|
+
// child rather than a named 'type' field.
|
|
753
|
+
if (!typeNode) {
|
|
754
|
+
for (let i = 0; i < wrapped.namedChildCount; i++) {
|
|
755
|
+
const c = wrapped.namedChild(i);
|
|
756
|
+
if (c && (c.type === 'user_type' || c.type === 'nullable_type')) {
|
|
757
|
+
typeNode = c;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
482
763
|
}
|
|
483
764
|
if (typeNode) {
|
|
484
765
|
const nameNode = node.childForFieldName('name')
|
|
@@ -492,13 +773,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
492
773
|
}
|
|
493
774
|
}
|
|
494
775
|
// Run the language-specific declaration extractor (may or may not add to scopeEnv).
|
|
495
|
-
const
|
|
776
|
+
const sizeBefore = typeNode ? scopeEnv.size : -1;
|
|
496
777
|
config.extractDeclaration(node, scopeEnv);
|
|
497
778
|
// Fallback: for multi-declarator languages (TS, C#, Java) where the type field
|
|
498
|
-
// is on variable_declarator children, capture
|
|
499
|
-
|
|
779
|
+
// is on variable_declarator children, capture newly-added keys.
|
|
780
|
+
// Map preserves insertion order, so new keys are always at the end —
|
|
781
|
+
// skip the first sizeBefore entries to find only newly-added variables.
|
|
782
|
+
if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
|
|
783
|
+
let skip = sizeBefore;
|
|
500
784
|
for (const varName of scopeEnv.keys()) {
|
|
501
|
-
if (
|
|
785
|
+
if (skip > 0) {
|
|
786
|
+
skip--;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (!declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
|
502
790
|
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
|
503
791
|
}
|
|
504
792
|
}
|
|
@@ -510,6 +798,35 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
510
798
|
if (config.extractInitializer) {
|
|
511
799
|
config.extractInitializer(node, scopeEnv, classNames);
|
|
512
800
|
}
|
|
801
|
+
// Phase P: detect constructor-visible virtual dispatch.
|
|
802
|
+
// When a declaration has BOTH a type annotation AND a constructor initializer,
|
|
803
|
+
// record the constructor type for receiver override at call resolution time.
|
|
804
|
+
// e.g., `Animal a = new Dog()` → constructorTypeMap.set('scope\0a', 'Dog')
|
|
805
|
+
if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
|
|
806
|
+
let ctorSkip = sizeBefore;
|
|
807
|
+
for (const varName of scopeEnv.keys()) {
|
|
808
|
+
if (ctorSkip > 0) {
|
|
809
|
+
ctorSkip--;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const declaredType = scopeEnv.get(varName);
|
|
813
|
+
if (!declaredType)
|
|
814
|
+
continue;
|
|
815
|
+
const ctorType = extractConstructorTypeName(node)
|
|
816
|
+
?? config.detectConstructorType?.(node, classNames);
|
|
817
|
+
if (!ctorType || ctorType === declaredType)
|
|
818
|
+
continue;
|
|
819
|
+
// Unwrap wrapper types (e.g., C++ shared_ptr<Animal> → Animal) for an
|
|
820
|
+
// accurate isSubclassOf comparison. Language-specific via config hook.
|
|
821
|
+
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
|
822
|
+
const effectiveDeclaredType = (declTypeNode && config.unwrapDeclaredType)
|
|
823
|
+
? (config.unwrapDeclaredType(declaredType, declTypeNode) ?? declaredType)
|
|
824
|
+
: declaredType;
|
|
825
|
+
if (ctorType !== effectiveDeclaredType) {
|
|
826
|
+
constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
513
830
|
}
|
|
514
831
|
};
|
|
515
832
|
const walk = (node, currentScope) => {
|
|
@@ -542,7 +859,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
542
859
|
extractTypeBinding(node, scopeEnv, scope);
|
|
543
860
|
}
|
|
544
861
|
// Pattern binding extraction: handles constructs that introduce NEW typed variables
|
|
545
|
-
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
|
|
862
|
+
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
|
|
863
|
+
// or narrow existing variables within a branch (null-check narrowing).
|
|
546
864
|
// Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
|
|
547
865
|
// Conservative: extractor returns undefined when source type is unknown.
|
|
548
866
|
if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
|
|
@@ -552,11 +870,25 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
552
870
|
const scopeEnv = env.get(scope);
|
|
553
871
|
const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
|
|
554
872
|
if (patternBinding) {
|
|
555
|
-
if (
|
|
873
|
+
if (patternBinding.narrowingRange) {
|
|
874
|
+
// Explicit narrowing range (null-check narrowing): always store in patternOverrides
|
|
875
|
+
// using the extractor-provided range (typically the if-body block).
|
|
876
|
+
if (!patternOverrides.has(scope))
|
|
877
|
+
patternOverrides.set(scope, new Map());
|
|
878
|
+
const varMap = patternOverrides.get(scope);
|
|
879
|
+
if (!varMap.has(patternBinding.varName))
|
|
880
|
+
varMap.set(patternBinding.varName, []);
|
|
881
|
+
varMap.get(patternBinding.varName).push({
|
|
882
|
+
rangeStart: patternBinding.narrowingRange.startIndex,
|
|
883
|
+
rangeEnd: patternBinding.narrowingRange.endIndex,
|
|
884
|
+
typeName: patternBinding.typeName,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
else if (config.allowPatternBindingOverwrite) {
|
|
556
888
|
// Position-indexed: store per-branch binding for smart-cast narrowing.
|
|
557
889
|
// Each when arm / switch case gets its own type for the variable,
|
|
558
890
|
// preventing cross-arm contamination (e.g., Kotlin when/is).
|
|
559
|
-
const branchNode =
|
|
891
|
+
const branchNode = findNarrowingBranchScope(node);
|
|
560
892
|
if (branchNode) {
|
|
561
893
|
if (!patternOverrides.has(scope))
|
|
562
894
|
patternOverrides.set(scope, new Map());
|
|
@@ -583,6 +915,7 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
583
915
|
// Delegates to per-language extractPendingAssignment — AST shapes differ widely
|
|
584
916
|
// (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
|
|
585
917
|
// Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
|
|
918
|
+
// May return a single item or an array (for destructuring: N fieldAccess items).
|
|
586
919
|
if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
|
|
587
920
|
// scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
|
|
588
921
|
// of interestingNodeTypes, so extractTypeBinding already created the scope map above.
|
|
@@ -590,7 +923,12 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
590
923
|
if (scopeEnv) {
|
|
591
924
|
const pending = config.extractPendingAssignment(node, scopeEnv);
|
|
592
925
|
if (pending) {
|
|
593
|
-
|
|
926
|
+
const items = Array.isArray(pending) ? pending : [pending];
|
|
927
|
+
for (const item of items) {
|
|
928
|
+
// Substitute this/self/$this/Me receivers with enclosing class name
|
|
929
|
+
const resolved = substituteThisReceiver(item, node);
|
|
930
|
+
pendingItems.push({ scope, ...resolved });
|
|
931
|
+
}
|
|
594
932
|
}
|
|
595
933
|
}
|
|
596
934
|
}
|
|
@@ -613,55 +951,40 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
613
951
|
}
|
|
614
952
|
};
|
|
615
953
|
walk(tree.rootNode, FILE_SCOPE);
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
954
|
+
// Phase 14: Seed cross-file bindings from upstream files AFTER walk
|
|
955
|
+
// (local declarations from walk() take precedence — first-writer-wins)
|
|
956
|
+
if (options?.importedBindings && options.importedBindings.size > 0) {
|
|
957
|
+
seedImportedBindings(env, options.importedBindings);
|
|
958
|
+
}
|
|
959
|
+
resolveFixpointBindings(pendingItems, env, returnTypeLookup, symbolTable, parentMap);
|
|
960
|
+
// Post-fixpoint for-loop replay (Phase 10 / ex-9B loop-fixpoint bridge):
|
|
961
|
+
// For-loop nodes whose iterables were unresolved at walk-time may now be
|
|
962
|
+
// resolvable because the fixpoint bound the iterable's type.
|
|
963
|
+
// Example: `const users = getUsers(); for (const u of users) { u.save(); }`
|
|
964
|
+
// - walk-time: users untyped → u unresolved
|
|
965
|
+
// - fixpoint: users → User[]
|
|
966
|
+
// - replay: users now typed → u → User
|
|
967
|
+
if (pendingForLoops.length > 0 && config.extractForLoopBinding) {
|
|
968
|
+
for (const { node, scope } of pendingForLoops) {
|
|
969
|
+
if (!env.has(scope))
|
|
970
|
+
env.set(scope, new Map());
|
|
971
|
+
const scopeEnv = env.get(scope);
|
|
972
|
+
config.extractForLoopBinding(node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup });
|
|
973
|
+
}
|
|
974
|
+
// Re-run the main fixpoint to resolve items that depended on loop variables.
|
|
975
|
+
// Only needed if replay actually produced new bindings.
|
|
976
|
+
const unresolvedBefore = pendingItems.filter((item) => {
|
|
633
977
|
const scopeEnv = env.get(item.scope);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
let typeName;
|
|
639
|
-
switch (item.kind) {
|
|
640
|
-
case 'callResult':
|
|
641
|
-
typeName = returnTypeLookup.lookupReturnType(item.callee);
|
|
642
|
-
break;
|
|
643
|
-
case 'copy':
|
|
644
|
-
typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
|
|
645
|
-
break;
|
|
646
|
-
case 'fieldAccess':
|
|
647
|
-
typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable);
|
|
648
|
-
break;
|
|
649
|
-
case 'methodCallResult':
|
|
650
|
-
typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable);
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
if (typeName) {
|
|
654
|
-
scopeEnv.set(item.lhs, typeName);
|
|
655
|
-
resolved.add(i);
|
|
656
|
-
changed = true;
|
|
657
|
-
}
|
|
978
|
+
return scopeEnv && !scopeEnv.has(item.lhs);
|
|
979
|
+
});
|
|
980
|
+
if (unresolvedBefore.length > 0) {
|
|
981
|
+
resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
|
|
658
982
|
}
|
|
659
|
-
if (!changed)
|
|
660
|
-
break;
|
|
661
983
|
}
|
|
662
984
|
return {
|
|
663
985
|
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
|
|
664
986
|
constructorBindings: bindings,
|
|
665
987
|
env,
|
|
988
|
+
constructorTypeMap,
|
|
666
989
|
};
|
|
667
990
|
};
|