gitnexus 1.4.6 → 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 +4 -3
- 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 +55 -2
- package/dist/core/ingestion/call-processor.js +673 -108
- package/dist/core/ingestion/call-routing.d.ts +23 -2
- package/dist/core/ingestion/call-routing.js +21 -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 +3 -2
- package/dist/core/ingestion/parsing-processor.js +27 -60
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +425 -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 +29 -3
- package/dist/core/ingestion/symbol-table.js +42 -9
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +243 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +451 -72
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
- package/dist/core/ingestion/type-extractors/csharp.js +189 -16
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- 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 +244 -69
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +72 -4
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
- package/dist/core/ingestion/type-extractors/shared.js +115 -13
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
- package/dist/core/ingestion/type-extractors/typescript.js +171 -9
- package/dist/core/ingestion/utils.d.ts +2 -95
- package/dist/core/ingestion/utils.js +3 -892
- package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
- package/dist/core/ingestion/workers/parse-worker.js +116 -95
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +12 -0
- package/dist/core/lbug/lbug-adapter.js +71 -4
- package/dist/core/lbug/schema.d.ts +6 -4
- package/dist/core/lbug/schema.js +27 -3
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +178 -23
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +136 -32
- package/dist/mcp/resources.js +13 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +17 -7
- 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 +12 -4
|
@@ -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`.
|
|
@@ -296,38 +319,315 @@ const SKIP_SUBTREE_TYPES = new Set([
|
|
|
296
319
|
// Regex
|
|
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
|
+
};
|
|
441
|
+
/** Resolve a field's declared type given a receiver variable and field name.
|
|
442
|
+
* Uses SymbolTable to find the class nodeId for the receiver's type, then
|
|
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) => {
|
|
446
|
+
if (!symbolTable)
|
|
447
|
+
return undefined;
|
|
448
|
+
const receiverType = scopeEnv.get(receiver);
|
|
449
|
+
if (!receiverType)
|
|
450
|
+
return undefined;
|
|
451
|
+
const lookup = getClassDefs
|
|
452
|
+
?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
|
453
|
+
const classDefs = lookup(receiverType);
|
|
454
|
+
if (classDefs.length !== 1)
|
|
455
|
+
return undefined;
|
|
456
|
+
// Direct lookup first
|
|
457
|
+
const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
|
|
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;
|
|
466
|
+
};
|
|
467
|
+
/** Resolve a method's return type given a receiver variable and method name.
|
|
468
|
+
* Uses SymbolTable to find class nodeIds for the receiver's type, then
|
|
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) => {
|
|
472
|
+
if (!symbolTable)
|
|
473
|
+
return undefined;
|
|
474
|
+
const receiverType = scopeEnv.get(receiver);
|
|
475
|
+
if (!receiverType)
|
|
476
|
+
return undefined;
|
|
477
|
+
const lookup = getClassDefs
|
|
478
|
+
?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
|
479
|
+
const classDefs = lookup(receiverType);
|
|
480
|
+
if (classDefs.length === 0)
|
|
481
|
+
return undefined;
|
|
482
|
+
// Direct lookup first
|
|
483
|
+
const classNodeIds = new Set(classDefs.map(d => d.nodeId));
|
|
484
|
+
const methods = symbolTable.lookupFuzzyCallable(method)
|
|
485
|
+
.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
|
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;
|
|
501
|
+
};
|
|
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;
|
|
300
585
|
const env = new Map();
|
|
301
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();
|
|
302
591
|
const localClassNames = new Set();
|
|
303
592
|
const classNames = createClassNameLookup(localClassNames, symbolTable);
|
|
304
593
|
const config = typeConfigs[language];
|
|
305
594
|
const bindings = [];
|
|
306
|
-
// Build ReturnTypeLookup
|
|
307
|
-
//
|
|
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).
|
|
308
598
|
const returnTypeLookup = {
|
|
309
599
|
lookupReturnType(callee) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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);
|
|
321
616
|
},
|
|
322
617
|
lookupRawReturnType(callee) {
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return undefined
|
|
330
|
-
|
|
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);
|
|
331
631
|
}
|
|
332
632
|
};
|
|
333
633
|
// Pre-compute combined set of node types that need extractTypeBinding.
|
|
@@ -336,12 +636,13 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
336
636
|
TYPED_PARAMETER_TYPES.forEach(t => interestingNodeTypes.add(t));
|
|
337
637
|
config.declarationNodeTypes.forEach(t => interestingNodeTypes.add(t));
|
|
338
638
|
config.forLoopNodeTypes?.forEach(t => interestingNodeTypes.add(t));
|
|
339
|
-
// Tier 2:
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
|
|
639
|
+
// Tier 2: unified fixpoint propagation — collects copy, callResult, fieldAccess, and
|
|
640
|
+
// methodCallResult items during walk(), then iterates until no new bindings are produced.
|
|
641
|
+
// Handles arbitrary-depth mixed chains: callResult → fieldAccess → methodCallResult → copy.
|
|
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 = [];
|
|
345
646
|
// Maps `scope\0varName` → the type annotation AST node from the original declaration.
|
|
346
647
|
// Allows pattern extractors to navigate back to the declaration's generic type arguments
|
|
347
648
|
// (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
|
|
@@ -371,7 +672,9 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
371
672
|
let typeNode = node.childForFieldName('type');
|
|
372
673
|
if (typeNode) {
|
|
373
674
|
const nameNode = node.childForFieldName('name')
|
|
374
|
-
?? node.childForFieldName('pattern')
|
|
675
|
+
?? node.childForFieldName('pattern')
|
|
676
|
+
// Python typed_parameter: name is a positional child (identifier), not a named field
|
|
677
|
+
?? (node.firstNamedChild?.type === 'identifier' ? node.firstNamedChild : null);
|
|
375
678
|
if (nameNode) {
|
|
376
679
|
const varName = extractVarName(nameNode);
|
|
377
680
|
if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
|
@@ -391,7 +694,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
391
694
|
fallbackName = child;
|
|
392
695
|
}
|
|
393
696
|
if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
|
|
394
|
-
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
|
697
|
+
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
|
698
|
+
|| child.type === 'nullable_type')) {
|
|
395
699
|
fallbackType = child;
|
|
396
700
|
}
|
|
397
701
|
}
|
|
@@ -409,8 +713,14 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
409
713
|
// Checked before declarationNodeTypes — loop variables are not declarations.
|
|
410
714
|
if (config.forLoopNodeTypes?.has(node.type)) {
|
|
411
715
|
if (config.extractForLoopBinding) {
|
|
716
|
+
const sizeBefore = scopeEnv.size;
|
|
412
717
|
const forLoopCtx = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
|
|
413
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
|
+
}
|
|
414
724
|
}
|
|
415
725
|
return;
|
|
416
726
|
}
|
|
@@ -436,8 +746,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
436
746
|
}
|
|
437
747
|
}
|
|
438
748
|
}
|
|
439
|
-
if (wrapped)
|
|
749
|
+
if (wrapped) {
|
|
440
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
|
+
}
|
|
441
763
|
}
|
|
442
764
|
if (typeNode) {
|
|
443
765
|
const nameNode = node.childForFieldName('name')
|
|
@@ -451,13 +773,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
451
773
|
}
|
|
452
774
|
}
|
|
453
775
|
// Run the language-specific declaration extractor (may or may not add to scopeEnv).
|
|
454
|
-
const
|
|
776
|
+
const sizeBefore = typeNode ? scopeEnv.size : -1;
|
|
455
777
|
config.extractDeclaration(node, scopeEnv);
|
|
456
778
|
// Fallback: for multi-declarator languages (TS, C#, Java) where the type field
|
|
457
|
-
// is on variable_declarator children, capture
|
|
458
|
-
|
|
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;
|
|
459
784
|
for (const varName of scopeEnv.keys()) {
|
|
460
|
-
if (
|
|
785
|
+
if (skip > 0) {
|
|
786
|
+
skip--;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (!declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
|
461
790
|
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
|
462
791
|
}
|
|
463
792
|
}
|
|
@@ -469,6 +798,35 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
469
798
|
if (config.extractInitializer) {
|
|
470
799
|
config.extractInitializer(node, scopeEnv, classNames);
|
|
471
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
|
+
}
|
|
472
830
|
}
|
|
473
831
|
};
|
|
474
832
|
const walk = (node, currentScope) => {
|
|
@@ -501,7 +859,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
501
859
|
extractTypeBinding(node, scopeEnv, scope);
|
|
502
860
|
}
|
|
503
861
|
// Pattern binding extraction: handles constructs that introduce NEW typed variables
|
|
504
|
-
// 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).
|
|
505
864
|
// Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
|
|
506
865
|
// Conservative: extractor returns undefined when source type is unknown.
|
|
507
866
|
if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
|
|
@@ -511,11 +870,25 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
511
870
|
const scopeEnv = env.get(scope);
|
|
512
871
|
const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
|
|
513
872
|
if (patternBinding) {
|
|
514
|
-
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) {
|
|
515
888
|
// Position-indexed: store per-branch binding for smart-cast narrowing.
|
|
516
889
|
// Each when arm / switch case gets its own type for the variable,
|
|
517
890
|
// preventing cross-arm contamination (e.g., Kotlin when/is).
|
|
518
|
-
const branchNode =
|
|
891
|
+
const branchNode = findNarrowingBranchScope(node);
|
|
519
892
|
if (branchNode) {
|
|
520
893
|
if (!patternOverrides.has(scope))
|
|
521
894
|
patternOverrides.set(scope, new Map());
|
|
@@ -542,6 +915,7 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
542
915
|
// Delegates to per-language extractPendingAssignment — AST shapes differ widely
|
|
543
916
|
// (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
|
|
544
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).
|
|
545
919
|
if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
|
|
546
920
|
// scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
|
|
547
921
|
// of interestingNodeTypes, so extractTypeBinding already created the scope map above.
|
|
@@ -549,11 +923,11 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
549
923
|
if (scopeEnv) {
|
|
550
924
|
const pending = config.extractPendingAssignment(node, scopeEnv);
|
|
551
925
|
if (pending) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 });
|
|
557
931
|
}
|
|
558
932
|
}
|
|
559
933
|
}
|
|
@@ -577,35 +951,40 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
577
951
|
}
|
|
578
952
|
};
|
|
579
953
|
walk(tree.rootNode, FILE_SCOPE);
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
for (const { scope, lhs, rhs } of pendingCopies) {
|
|
585
|
-
const scopeEnv = env.get(scope);
|
|
586
|
-
if (!scopeEnv || scopeEnv.has(lhs))
|
|
587
|
-
continue;
|
|
588
|
-
const rhsType = scopeEnv.get(rhs) ?? env.get(FILE_SCOPE)?.get(rhs);
|
|
589
|
-
if (rhsType)
|
|
590
|
-
scopeEnv.set(lhs, rhsType);
|
|
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);
|
|
591
958
|
}
|
|
592
|
-
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
scopeEnv.
|
|
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) => {
|
|
977
|
+
const scopeEnv = env.get(item.scope);
|
|
978
|
+
return scopeEnv && !scopeEnv.has(item.lhs);
|
|
979
|
+
});
|
|
980
|
+
if (unresolvedBefore.length > 0) {
|
|
981
|
+
resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
|
|
982
|
+
}
|
|
605
983
|
}
|
|
606
984
|
return {
|
|
607
985
|
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
|
|
608
986
|
constructorBindings: bindings,
|
|
609
987
|
env,
|
|
988
|
+
constructorTypeMap,
|
|
610
989
|
};
|
|
611
990
|
};
|