gitnexus 1.4.5 → 1.4.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/dist/cli/eval-server.js +13 -5
- package/dist/cli/index.js +0 -0
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +48 -13
- package/dist/core/graph/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -2
- package/dist/core/ingestion/call-processor.js +308 -235
- package/dist/core/ingestion/call-routing.d.ts +17 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +37 -8
- package/dist/core/ingestion/pipeline.js +5 -1
- package/dist/core/ingestion/symbol-table.d.ts +19 -3
- package/dist/core/ingestion/symbol-table.js +41 -2
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +200 -0
- package/dist/core/ingestion/type-env.js +126 -18
- package/dist/core/ingestion/type-extractors/c-cpp.js +28 -3
- package/dist/core/ingestion/type-extractors/csharp.js +61 -7
- package/dist/core/ingestion/type-extractors/go.js +86 -10
- package/dist/core/ingestion/type-extractors/jvm.js +122 -23
- package/dist/core/ingestion/type-extractors/php.js +172 -7
- package/dist/core/ingestion/type-extractors/python.js +107 -21
- package/dist/core/ingestion/type-extractors/ruby.js +18 -3
- package/dist/core/ingestion/type-extractors/rust.js +61 -14
- package/dist/core/ingestion/type-extractors/shared.d.ts +13 -0
- package/dist/core/ingestion/type-extractors/shared.js +243 -4
- package/dist/core/ingestion/type-extractors/types.d.ts +57 -12
- package/dist/core/ingestion/type-extractors/typescript.js +52 -8
- package/dist/core/ingestion/utils.d.ts +25 -0
- package/dist/core/ingestion/utils.js +160 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
- package/dist/core/ingestion/workers/parse-worker.js +73 -28
- package/dist/core/lbug/lbug-adapter.d.ts +2 -0
- package/dist/core/lbug/lbug-adapter.js +2 -0
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +167 -23
- package/dist/mcp/local/local-backend.d.ts +1 -0
- package/dist/mcp/local/local-backend.js +25 -3
- package/dist/mcp/resources.js +11 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +15 -5
- package/hooks/claude/gitnexus-hook.cjs +0 -0
- package/hooks/claude/pre-tool-use.sh +0 -0
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +6 -5
- package/scripts/patch-tree-sitter-swift.cjs +0 -0
|
@@ -132,7 +132,7 @@ export function resolveIterableElementType(iterableName, node, scopeEnv, declara
|
|
|
132
132
|
/** Known single-arg nullable wrapper types that unwrap to their inner type
|
|
133
133
|
* for receiver resolution. Optional<User> → "User", Option<User> → "User".
|
|
134
134
|
* Only nullable wrappers — NOT containers (List, Vec) or async wrappers (Promise, Future).
|
|
135
|
-
* See
|
|
135
|
+
* See WRAPPER_GENERICS below for the full set used in return-type inference. */
|
|
136
136
|
const NULLABLE_WRAPPER_TYPES = new Set([
|
|
137
137
|
'Optional', // Java
|
|
138
138
|
'Option', // Rust, Scala
|
|
@@ -226,9 +226,14 @@ export const extractSimpleTypeName = (typeNode, depth = 0) => {
|
|
|
226
226
|
}
|
|
227
227
|
// Pointer/reference types (C++, Rust): User*, &User, &mut User
|
|
228
228
|
if (typeNode.type === 'pointer_type' || typeNode.type === 'reference_type') {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
// Skip mutable_specifier for Rust &mut references — firstNamedChild would be
|
|
230
|
+
// `mutable_specifier` not the actual type. Walk named children to find the type.
|
|
231
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
232
|
+
const child = typeNode.namedChild(i);
|
|
233
|
+
if (child && child.type !== 'mutable_specifier') {
|
|
234
|
+
return extractSimpleTypeName(child, depth + 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
232
237
|
}
|
|
233
238
|
// Primitive/predefined types: string, int, float, bool, number, unknown, any
|
|
234
239
|
// PHP: primitive_type; TS/JS: predefined_type
|
|
@@ -569,3 +574,237 @@ export function extractElementTypeFromString(typeStr, pos = 'last') {
|
|
|
569
574
|
}
|
|
570
575
|
return undefined;
|
|
571
576
|
}
|
|
577
|
+
// ── Return type text helpers ─────────────────────────────────────────────
|
|
578
|
+
// extractReturnTypeName works on raw return-type text already stored in
|
|
579
|
+
// SymbolDefinition (e.g. "User", "Promise<User>", "User | null", "*User").
|
|
580
|
+
// Extracts the base user-defined type name.
|
|
581
|
+
/** Primitive / built-in types that should NOT produce a receiver binding. */
|
|
582
|
+
const PRIMITIVE_TYPES = new Set([
|
|
583
|
+
'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
|
|
584
|
+
'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
|
|
585
|
+
'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
|
|
586
|
+
'undefined', 'null', 'None', 'nil',
|
|
587
|
+
]);
|
|
588
|
+
/**
|
|
589
|
+
* Extract a simple type name from raw return-type text.
|
|
590
|
+
* Handles common patterns:
|
|
591
|
+
* "User" → "User"
|
|
592
|
+
* "Promise<User>" → "User" (unwrap wrapper generics)
|
|
593
|
+
* "Option<User>" → "User"
|
|
594
|
+
* "Result<User, Error>" → "User" (first type arg)
|
|
595
|
+
* "User | null" → "User" (strip nullable union)
|
|
596
|
+
* "User?" → "User" (strip nullable suffix)
|
|
597
|
+
* "*User" → "User" (Go pointer)
|
|
598
|
+
* "&User" → "User" (Rust reference)
|
|
599
|
+
* Returns undefined for complex types or primitives.
|
|
600
|
+
*/
|
|
601
|
+
const WRAPPER_GENERICS = new Set([
|
|
602
|
+
'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers
|
|
603
|
+
'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers
|
|
604
|
+
'Result', 'Either', // result wrappers
|
|
605
|
+
// Rust smart pointers (Deref to inner type)
|
|
606
|
+
'Rc', 'Arc', 'Weak', // pointer types
|
|
607
|
+
'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types
|
|
608
|
+
'Ref', 'RefMut', // RefCell guards
|
|
609
|
+
'Cow', // copy-on-write
|
|
610
|
+
// Containers (List, Array, Vec, Set, etc.) are intentionally excluded —
|
|
611
|
+
// methods are called on the container, not the element type.
|
|
612
|
+
// Non-wrapper generics return the base type (e.g., List) via the else branch.
|
|
613
|
+
]);
|
|
614
|
+
/**
|
|
615
|
+
* Extracts the first type argument from a comma-separated generic argument string,
|
|
616
|
+
* respecting nested angle brackets. For example:
|
|
617
|
+
* "Result<User, Error>" → "Result<User, Error>" (no top-level comma)
|
|
618
|
+
* "User, Error" → "User"
|
|
619
|
+
* "Map<K, V>, string" → "Map<K, V>"
|
|
620
|
+
*/
|
|
621
|
+
function extractFirstGenericArg(args) {
|
|
622
|
+
let depth = 0;
|
|
623
|
+
for (let i = 0; i < args.length; i++) {
|
|
624
|
+
if (args[i] === '<')
|
|
625
|
+
depth++;
|
|
626
|
+
else if (args[i] === '>')
|
|
627
|
+
depth--;
|
|
628
|
+
else if (args[i] === ',' && depth === 0)
|
|
629
|
+
return args.slice(0, i).trim();
|
|
630
|
+
}
|
|
631
|
+
return args.trim();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Extract the first non-lifetime type argument from a generic argument string.
|
|
635
|
+
* Skips Rust lifetime parameters (e.g., `'a`, `'_`) to find the actual type.
|
|
636
|
+
* "'_, User" → "User"
|
|
637
|
+
* "'a, User" → "User"
|
|
638
|
+
* "User, Error" → "User" (no lifetime — delegates to extractFirstGenericArg)
|
|
639
|
+
*/
|
|
640
|
+
function extractFirstTypeArg(args) {
|
|
641
|
+
let remaining = args;
|
|
642
|
+
while (remaining) {
|
|
643
|
+
const first = extractFirstGenericArg(remaining);
|
|
644
|
+
if (!first.startsWith("'"))
|
|
645
|
+
return first;
|
|
646
|
+
// Skip past this lifetime arg + the comma separator
|
|
647
|
+
const commaIdx = remaining.indexOf(',', first.length);
|
|
648
|
+
if (commaIdx < 0)
|
|
649
|
+
return first; // only lifetimes — fall through
|
|
650
|
+
remaining = remaining.slice(commaIdx + 1).trim();
|
|
651
|
+
}
|
|
652
|
+
return args.trim();
|
|
653
|
+
}
|
|
654
|
+
const MAX_RETURN_TYPE_INPUT_LENGTH = 2048;
|
|
655
|
+
const MAX_RETURN_TYPE_LENGTH = 512;
|
|
656
|
+
export const extractReturnTypeName = (raw, depth = 0) => {
|
|
657
|
+
if (depth > 10)
|
|
658
|
+
return undefined;
|
|
659
|
+
if (raw.length > MAX_RETURN_TYPE_INPUT_LENGTH)
|
|
660
|
+
return undefined;
|
|
661
|
+
let text = raw.trim();
|
|
662
|
+
if (!text)
|
|
663
|
+
return undefined;
|
|
664
|
+
// Strip pointer/reference prefixes: *User, &User, &mut User
|
|
665
|
+
text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
|
|
666
|
+
// Strip nullable suffix: User?
|
|
667
|
+
text = text.replace(/\?$/, '');
|
|
668
|
+
// Handle union types: "User | null" → "User"
|
|
669
|
+
if (text.includes('|')) {
|
|
670
|
+
const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
|
|
671
|
+
if (parts.length === 1)
|
|
672
|
+
text = parts[0];
|
|
673
|
+
else
|
|
674
|
+
return undefined; // genuine union — too complex
|
|
675
|
+
}
|
|
676
|
+
// Handle generics: Promise<User> → unwrap if wrapper, else take base
|
|
677
|
+
const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
|
|
678
|
+
if (genericMatch) {
|
|
679
|
+
const [, base, args] = genericMatch;
|
|
680
|
+
if (WRAPPER_GENERICS.has(base)) {
|
|
681
|
+
// Take the first non-lifetime type argument, using bracket-balanced splitting
|
|
682
|
+
// so that nested generics like Result<User, Error> are not split at the inner
|
|
683
|
+
// comma. Lifetime parameters (Rust 'a, '_) are skipped.
|
|
684
|
+
const firstArg = extractFirstTypeArg(args);
|
|
685
|
+
return extractReturnTypeName(firstArg, depth + 1);
|
|
686
|
+
}
|
|
687
|
+
// Non-wrapper generic: return the base type (e.g., Map<K,V> → Map)
|
|
688
|
+
return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
|
|
689
|
+
}
|
|
690
|
+
// Bare wrapper type without generic argument (e.g. Task, Promise, Option)
|
|
691
|
+
// should not produce a binding — these are meaningless without a type parameter
|
|
692
|
+
if (WRAPPER_GENERICS.has(text))
|
|
693
|
+
return undefined;
|
|
694
|
+
// Handle qualified names: models.User → User, Models::User → User, \App\Models\User → User
|
|
695
|
+
if (text.includes('::') || text.includes('.') || text.includes('\\')) {
|
|
696
|
+
text = text.split(/::|[.\\]/).pop();
|
|
697
|
+
}
|
|
698
|
+
// Final check: skip primitives
|
|
699
|
+
if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
|
|
700
|
+
return undefined;
|
|
701
|
+
// Must start with uppercase (class/type convention) or be a valid identifier
|
|
702
|
+
if (!/^[A-Z_]\w*$/.test(text))
|
|
703
|
+
return undefined;
|
|
704
|
+
// If the final extracted type name is too long, reject it
|
|
705
|
+
if (text.length > MAX_RETURN_TYPE_LENGTH)
|
|
706
|
+
return undefined;
|
|
707
|
+
return text;
|
|
708
|
+
};
|
|
709
|
+
// ── Property declared-type extraction ────────────────────────────────────
|
|
710
|
+
// Shared between parse-worker (worker path) and parsing-processor (sequential path).
|
|
711
|
+
/**
|
|
712
|
+
* Extract the declared type of a property/field from its AST definition node.
|
|
713
|
+
* Handles cross-language patterns:
|
|
714
|
+
* - TypeScript: `name: Type` → type_annotation child
|
|
715
|
+
* - Java: `Type name` → type child on field_declaration
|
|
716
|
+
* - C#: `Type Name { get; set; }` → type child on property_declaration
|
|
717
|
+
* - Go: `Name Type` → type child on field_declaration
|
|
718
|
+
* - Kotlin: `var name: Type` → variable_declaration child with type field
|
|
719
|
+
*
|
|
720
|
+
* Returns the normalized type name, or undefined if no type can be extracted.
|
|
721
|
+
*/
|
|
722
|
+
export const extractPropertyDeclaredType = (definitionNode) => {
|
|
723
|
+
if (!definitionNode)
|
|
724
|
+
return undefined;
|
|
725
|
+
// Strategy 1: Look for a `type` or `type_annotation` named field
|
|
726
|
+
const typeNode = definitionNode.childForFieldName?.('type');
|
|
727
|
+
if (typeNode) {
|
|
728
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
729
|
+
if (typeName)
|
|
730
|
+
return typeName;
|
|
731
|
+
// Fallback: use the raw text (for complex types like User[] or List<User>)
|
|
732
|
+
const text = typeNode.text?.trim();
|
|
733
|
+
if (text && text.length < 100)
|
|
734
|
+
return text;
|
|
735
|
+
}
|
|
736
|
+
// Strategy 2: Walk children looking for type_annotation (TypeScript pattern)
|
|
737
|
+
for (let i = 0; i < definitionNode.childCount; i++) {
|
|
738
|
+
const child = definitionNode.child(i);
|
|
739
|
+
if (!child)
|
|
740
|
+
continue;
|
|
741
|
+
if (child.type === 'type_annotation') {
|
|
742
|
+
// Type annotation has the actual type as a child
|
|
743
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
744
|
+
const typeChild = child.child(j);
|
|
745
|
+
if (typeChild && typeChild.type !== ':') {
|
|
746
|
+
const typeName = extractSimpleTypeName(typeChild);
|
|
747
|
+
if (typeName)
|
|
748
|
+
return typeName;
|
|
749
|
+
const text = typeChild.text?.trim();
|
|
750
|
+
if (text && text.length < 100)
|
|
751
|
+
return text;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Strategy 3: For Java field_declaration, the type is a sibling of variable_declarator
|
|
757
|
+
// AST: (field_declaration type: (type_identifier) declarator: (variable_declarator ...))
|
|
758
|
+
const parentDecl = definitionNode.parent;
|
|
759
|
+
if (parentDecl) {
|
|
760
|
+
const parentType = parentDecl.childForFieldName?.('type');
|
|
761
|
+
if (parentType) {
|
|
762
|
+
const typeName = extractSimpleTypeName(parentType);
|
|
763
|
+
if (typeName)
|
|
764
|
+
return typeName;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Strategy 4: Kotlin property_declaration — type is nested inside variable_declaration child
|
|
768
|
+
// AST: (property_declaration (variable_declaration (simple_identifier) ":" (user_type (type_identifier))))
|
|
769
|
+
// Kotlin's variable_declaration has NO named 'type' field — children are all positional.
|
|
770
|
+
for (let i = 0; i < definitionNode.childCount; i++) {
|
|
771
|
+
const child = definitionNode.child(i);
|
|
772
|
+
if (child?.type === 'variable_declaration') {
|
|
773
|
+
// Try named field first (works for other languages sharing this strategy)
|
|
774
|
+
const varType = child.childForFieldName?.('type');
|
|
775
|
+
if (varType) {
|
|
776
|
+
const typeName = extractSimpleTypeName(varType);
|
|
777
|
+
if (typeName)
|
|
778
|
+
return typeName;
|
|
779
|
+
const text = varType.text?.trim();
|
|
780
|
+
if (text && text.length < 100)
|
|
781
|
+
return text;
|
|
782
|
+
}
|
|
783
|
+
// Fallback: walk unnamed children for user_type / type_identifier (Kotlin)
|
|
784
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
785
|
+
const varChild = child.namedChild(j);
|
|
786
|
+
if (varChild && (varChild.type === 'user_type' || varChild.type === 'type_identifier'
|
|
787
|
+
|| varChild.type === 'nullable_type' || varChild.type === 'generic_type')) {
|
|
788
|
+
const typeName = extractSimpleTypeName(varChild);
|
|
789
|
+
if (typeName)
|
|
790
|
+
return typeName;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// Strategy 5: PHP @var PHPDoc — look for preceding comment with @var Type
|
|
796
|
+
// Handles pre-PHP-7.4 code: /** @var Address */ public $address;
|
|
797
|
+
const prevSibling = definitionNode.previousNamedSibling ?? definitionNode.parent?.previousNamedSibling;
|
|
798
|
+
if (prevSibling?.type === 'comment') {
|
|
799
|
+
const commentText = prevSibling.text;
|
|
800
|
+
const varMatch = commentText?.match(/@var\s+([A-Z][\w\\]*)/);
|
|
801
|
+
if (varMatch) {
|
|
802
|
+
// Strip namespace prefix: \App\Models\User → User
|
|
803
|
+
const raw = varMatch[1];
|
|
804
|
+
const base = raw.includes('\\') ? raw.split('\\').pop() : raw;
|
|
805
|
+
if (base && /^[A-Z]\w*$/.test(base))
|
|
806
|
+
return base;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
};
|
|
@@ -23,17 +23,61 @@ export type ConstructorBindingScanner = (node: SyntaxNode) => {
|
|
|
23
23
|
* Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
|
|
24
24
|
* rather than in AST fields. Returns undefined if no return type can be determined. */
|
|
25
25
|
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
export
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
/** Narrow lookup interface for resolving a callee name → return type name.
|
|
27
|
+
* Backed by SymbolTable.lookupFuzzyCallable; passed via ForLoopExtractorContext.
|
|
28
|
+
* Conservative: returns undefined when the callee is ambiguous (0 or 2+ matches). */
|
|
29
|
+
export interface ReturnTypeLookup {
|
|
30
|
+
/** Processed type name after stripping wrappers (e.g., 'User' from 'Promise<User>').
|
|
31
|
+
* Use for call-result variable bindings (`const b = foo()`). */
|
|
32
|
+
lookupReturnType(callee: string): string | undefined;
|
|
33
|
+
/** Raw return type as declared in the symbol (e.g., '[]User', 'List<User>').
|
|
34
|
+
* Use for iterable-element extraction (`for v := range foo()`). */
|
|
35
|
+
lookupRawReturnType(callee: string): string | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Context object passed to ForLoopExtractor.
|
|
38
|
+
* Groups the four parameters that were previously positional. */
|
|
39
|
+
export interface ForLoopExtractorContext {
|
|
40
|
+
/** Mutable type-env for the current scope — extractor writes bindings here */
|
|
41
|
+
scopeEnv: Map<string, string>;
|
|
42
|
+
/** Maps `scope\0varName` to the declaration's type annotation AST node */
|
|
43
|
+
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>;
|
|
44
|
+
/** Current scope key, e.g. `"process@42"` */
|
|
45
|
+
scope: string;
|
|
46
|
+
/** Resolves a callee name to its declared return type (undefined = unknown/ambiguous) */
|
|
47
|
+
returnTypeLookup: ReturnTypeLookup;
|
|
48
|
+
}
|
|
49
|
+
/** Extracts loop variable type binding from a for-each statement. */
|
|
50
|
+
export type ForLoopExtractor = (node: SyntaxNode, ctx: ForLoopExtractorContext) => void;
|
|
51
|
+
/** Discriminated union for pending Tier-2 propagation items.
|
|
52
|
+
* - `copy` — `const b = a` (identifier alias, propagate a's type to b)
|
|
53
|
+
* - `callResult` — `const b = foo()` (bind b to foo's declared return type)
|
|
54
|
+
* - `fieldAccess` — `const b = a.field` (bind b to field's declaredType on a's type)
|
|
55
|
+
* - `methodCallResult` — `const b = a.method()` (bind b to method's returnType on a's type) */
|
|
56
|
+
export type PendingAssignment = {
|
|
57
|
+
kind: 'copy';
|
|
34
58
|
lhs: string;
|
|
35
59
|
rhs: string;
|
|
36
|
-
} |
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'callResult';
|
|
62
|
+
lhs: string;
|
|
63
|
+
callee: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'fieldAccess';
|
|
66
|
+
lhs: string;
|
|
67
|
+
receiver: string;
|
|
68
|
+
field: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'methodCallResult';
|
|
71
|
+
lhs: string;
|
|
72
|
+
receiver: string;
|
|
73
|
+
method: string;
|
|
74
|
+
};
|
|
75
|
+
/** Extracts a pending assignment for Tier 2 propagation.
|
|
76
|
+
* Returns a PendingAssignment when the RHS is a bare identifier (`copy`), a
|
|
77
|
+
* call expression (`callResult`), a field access (`fieldAccess`), or a
|
|
78
|
+
* method call with receiver (`methodCallResult`) and the LHS has no resolved type yet.
|
|
79
|
+
* Returns undefined if the node is not a matching assignment. */
|
|
80
|
+
export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => PendingAssignment | undefined;
|
|
37
81
|
/** Extracts a typed variable binding from a pattern-matching construct.
|
|
38
82
|
* Returns { varName, typeName } for patterns that introduce NEW variables.
|
|
39
83
|
* Examples: `if let Some(user) = opt` (Rust), `x instanceof User user` (Java).
|
|
@@ -82,9 +126,10 @@ export interface LanguageTypeConfig {
|
|
|
82
126
|
extractReturnType?: ReturnTypeExtractor;
|
|
83
127
|
/** Extract loop variable → type binding from a for-each AST node. */
|
|
84
128
|
extractForLoopBinding?: ForLoopExtractor;
|
|
85
|
-
/** Extract
|
|
86
|
-
* Called on declaration/assignment nodes; returns
|
|
87
|
-
*
|
|
129
|
+
/** Extract pending assignment for Tier 2 propagation.
|
|
130
|
+
* Called on declaration/assignment nodes; returns a PendingAssignment when the RHS
|
|
131
|
+
* is a bare identifier (copy) or call expression (callResult) and the LHS has no
|
|
132
|
+
* resolved type yet. Language-specific because AST shapes differ widely. */
|
|
88
133
|
extractPendingAssignment?: PendingAssignmentExtractor;
|
|
89
134
|
/** Extract a typed variable binding from a pattern-matching construct.
|
|
90
135
|
* Called on every AST node; returns { varName, typeName } when the node introduces a new
|
|
@@ -345,7 +345,7 @@ const findTsIterableElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
345
345
|
* 3. AST walk — walks up to the enclosing function's parameters to read User[] annotations directly
|
|
346
346
|
* Only handles `for...of`; `for...in` produces string keys, not element types.
|
|
347
347
|
*/
|
|
348
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
348
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
349
349
|
if (node.type !== 'for_in_statement')
|
|
350
350
|
return;
|
|
351
351
|
// Confirm this is `for...of`, not `for...in`, by scanning unnamed children for the keyword text.
|
|
@@ -359,10 +359,11 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
359
359
|
}
|
|
360
360
|
if (!isForOf)
|
|
361
361
|
return;
|
|
362
|
-
// The iterable is the `right` field — may be identifier or call_expression.
|
|
362
|
+
// The iterable is the `right` field — may be identifier, member_expression, or call_expression.
|
|
363
363
|
const rightNode = node.childForFieldName('right');
|
|
364
364
|
let iterableName;
|
|
365
365
|
let methodName;
|
|
366
|
+
let callExprElementType;
|
|
366
367
|
if (rightNode?.type === 'identifier') {
|
|
367
368
|
iterableName = rightNode.text;
|
|
368
369
|
}
|
|
@@ -374,6 +375,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
374
375
|
else if (rightNode?.type === 'call_expression') {
|
|
375
376
|
// entries.values() → call_expression > function: member_expression > object + property
|
|
376
377
|
// this.repos.values() → nested member_expression: extract property from inner member
|
|
378
|
+
// getUsers() → call_expression > function: identifier (Phase 7.3 — return-type path)
|
|
377
379
|
const fn = rightNode.childForFieldName('function');
|
|
378
380
|
if (fn?.type === 'member_expression') {
|
|
379
381
|
const obj = fn.childForFieldName('object');
|
|
@@ -390,13 +392,25 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
390
392
|
if (prop?.type === 'property_identifier')
|
|
391
393
|
methodName = prop.text;
|
|
392
394
|
}
|
|
395
|
+
else if (fn?.type === 'identifier') {
|
|
396
|
+
// Direct function call: for (const user of getUsers())
|
|
397
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
|
|
398
|
+
if (rawReturn)
|
|
399
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
400
|
+
}
|
|
393
401
|
}
|
|
394
|
-
if (!iterableName)
|
|
402
|
+
if (!iterableName && !callExprElementType)
|
|
395
403
|
return;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
404
|
+
let elementType;
|
|
405
|
+
if (callExprElementType) {
|
|
406
|
+
elementType = callExprElementType;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
// Look up the container's base type name for descriptor-aware resolution
|
|
410
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
411
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
412
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractTsElementTypeFromAnnotation, findTsIterableElementType, typeArgPos);
|
|
413
|
+
}
|
|
400
414
|
if (!elementType)
|
|
401
415
|
return;
|
|
402
416
|
// The loop variable is the `left` field.
|
|
@@ -444,7 +458,37 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
444
458
|
if (scopeEnv.has(lhs))
|
|
445
459
|
continue;
|
|
446
460
|
if (valueNode.type === 'identifier')
|
|
447
|
-
return { lhs, rhs: valueNode.text };
|
|
461
|
+
return { kind: 'copy', lhs, rhs: valueNode.text };
|
|
462
|
+
// member_expression RHS → fieldAccess (a.field, this.field)
|
|
463
|
+
if (valueNode.type === 'member_expression') {
|
|
464
|
+
const obj = valueNode.childForFieldName('object');
|
|
465
|
+
const prop = valueNode.childForFieldName('property');
|
|
466
|
+
if (obj && prop?.type === 'property_identifier' &&
|
|
467
|
+
(obj.type === 'identifier' || obj.type === 'this')) {
|
|
468
|
+
return { kind: 'fieldAccess', lhs, receiver: obj.text, field: prop.text };
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Unwrap await: `const user = await fetchUser()` or `await a.getC()`
|
|
473
|
+
const callNode = unwrapAwait(valueNode);
|
|
474
|
+
if (!callNode || callNode.type !== 'call_expression')
|
|
475
|
+
continue;
|
|
476
|
+
const funcNode = callNode.childForFieldName('function');
|
|
477
|
+
if (!funcNode)
|
|
478
|
+
continue;
|
|
479
|
+
// Simple call → callResult: getUser()
|
|
480
|
+
if (funcNode.type === 'identifier') {
|
|
481
|
+
return { kind: 'callResult', lhs, callee: funcNode.text };
|
|
482
|
+
}
|
|
483
|
+
// Method call with receiver → methodCallResult: a.getC()
|
|
484
|
+
if (funcNode.type === 'member_expression') {
|
|
485
|
+
const obj = funcNode.childForFieldName('object');
|
|
486
|
+
const prop = funcNode.childForFieldName('property');
|
|
487
|
+
if (obj && prop?.type === 'property_identifier' &&
|
|
488
|
+
(obj.type === 'identifier' || obj.type === 'this')) {
|
|
489
|
+
return { kind: 'methodCallResult', lhs, receiver: obj.text, method: prop.text };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
448
492
|
}
|
|
449
493
|
return undefined;
|
|
450
494
|
};
|
|
@@ -110,4 +110,29 @@ export declare function extractCallChain(receiverCallNode: SyntaxNode): {
|
|
|
110
110
|
chain: string[];
|
|
111
111
|
baseReceiverName: string | undefined;
|
|
112
112
|
} | undefined;
|
|
113
|
+
/** One step in a mixed receiver chain. */
|
|
114
|
+
export type MixedChainStep = {
|
|
115
|
+
kind: 'field' | 'call';
|
|
116
|
+
name: string;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Walk a receiver AST node that may interleave field accesses and method calls,
|
|
120
|
+
* building a unified chain of steps up to MAX_CHAIN_DEPTH.
|
|
121
|
+
*
|
|
122
|
+
* For `svc.getUser().address.save()`, called with the receiver of `save`
|
|
123
|
+
* (`svc.getUser().address`, a field access node):
|
|
124
|
+
* returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
|
|
125
|
+
* baseReceiverName: 'svc' }
|
|
126
|
+
*
|
|
127
|
+
* For `user.getAddress().city.getName()`, called with receiver of `getName`
|
|
128
|
+
* (`user.getAddress().city`):
|
|
129
|
+
* returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
|
|
130
|
+
* baseReceiverName: 'user' }
|
|
131
|
+
*
|
|
132
|
+
* Pure field chains and pure call chains are special cases (all steps same kind).
|
|
133
|
+
*/
|
|
134
|
+
export declare function extractMixedChain(receiverNode: SyntaxNode): {
|
|
135
|
+
chain: MixedChainStep[];
|
|
136
|
+
baseReceiverName: string | undefined;
|
|
137
|
+
} | undefined;
|
|
113
138
|
export {};
|
|
@@ -254,7 +254,7 @@ export const CLASS_CONTAINER_TYPES = new Set([
|
|
|
254
254
|
'class_declaration', 'abstract_class_declaration',
|
|
255
255
|
'interface_declaration', 'struct_declaration', 'record_declaration',
|
|
256
256
|
'class_specifier', 'struct_specifier',
|
|
257
|
-
'impl_item', 'trait_item',
|
|
257
|
+
'impl_item', 'trait_item', 'struct_item', 'enum_item',
|
|
258
258
|
'class_definition',
|
|
259
259
|
'trait_declaration',
|
|
260
260
|
'protocol_declaration',
|
|
@@ -275,6 +275,8 @@ export const CONTAINER_TYPE_TO_LABEL = {
|
|
|
275
275
|
class_definition: 'Class',
|
|
276
276
|
impl_item: 'Impl',
|
|
277
277
|
trait_item: 'Trait',
|
|
278
|
+
struct_item: 'Struct',
|
|
279
|
+
enum_item: 'Enum',
|
|
278
280
|
trait_declaration: 'Trait',
|
|
279
281
|
record_declaration: 'Record',
|
|
280
282
|
protocol_declaration: 'Interface',
|
|
@@ -306,6 +308,21 @@ export const findEnclosingClassId = (node, filePath) => {
|
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
}
|
|
311
|
+
// Go: type_declaration wrapping a struct_type (type User struct { ... })
|
|
312
|
+
// field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
|
|
313
|
+
if (current.type === 'type_declaration') {
|
|
314
|
+
const typeSpec = current.children?.find((c) => c.type === 'type_spec');
|
|
315
|
+
if (typeSpec) {
|
|
316
|
+
const typeBody = typeSpec.childForFieldName?.('type');
|
|
317
|
+
if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
|
|
318
|
+
const nameNode = typeSpec.childForFieldName?.('name');
|
|
319
|
+
if (nameNode) {
|
|
320
|
+
const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
|
|
321
|
+
return generateId(label, `${filePath}:${nameNode.text}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
309
326
|
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
|
310
327
|
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
|
|
311
328
|
if (current.type === 'impl_item') {
|
|
@@ -1129,3 +1146,145 @@ export function extractCallChain(receiverCallNode) {
|
|
|
1129
1146
|
}
|
|
1130
1147
|
return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
|
|
1131
1148
|
}
|
|
1149
|
+
/** Node types representing member/field access across languages. */
|
|
1150
|
+
const FIELD_ACCESS_NODE_TYPES = new Set([
|
|
1151
|
+
'member_expression', // TS/JS
|
|
1152
|
+
'member_access_expression', // C#
|
|
1153
|
+
'selector_expression', // Go
|
|
1154
|
+
'field_expression', // Rust/C++
|
|
1155
|
+
'field_access', // Java
|
|
1156
|
+
'attribute', // Python
|
|
1157
|
+
'navigation_expression', // Kotlin/Swift
|
|
1158
|
+
'member_binding_expression', // C# null-conditional (user?.Address)
|
|
1159
|
+
]);
|
|
1160
|
+
/**
|
|
1161
|
+
* Walk a receiver AST node that may interleave field accesses and method calls,
|
|
1162
|
+
* building a unified chain of steps up to MAX_CHAIN_DEPTH.
|
|
1163
|
+
*
|
|
1164
|
+
* For `svc.getUser().address.save()`, called with the receiver of `save`
|
|
1165
|
+
* (`svc.getUser().address`, a field access node):
|
|
1166
|
+
* returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
|
|
1167
|
+
* baseReceiverName: 'svc' }
|
|
1168
|
+
*
|
|
1169
|
+
* For `user.getAddress().city.getName()`, called with receiver of `getName`
|
|
1170
|
+
* (`user.getAddress().city`):
|
|
1171
|
+
* returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
|
|
1172
|
+
* baseReceiverName: 'user' }
|
|
1173
|
+
*
|
|
1174
|
+
* Pure field chains and pure call chains are special cases (all steps same kind).
|
|
1175
|
+
*/
|
|
1176
|
+
export function extractMixedChain(receiverNode) {
|
|
1177
|
+
const chain = [];
|
|
1178
|
+
let current = receiverNode;
|
|
1179
|
+
while (chain.length < MAX_CHAIN_DEPTH) {
|
|
1180
|
+
if (CALL_EXPRESSION_TYPES.has(current.type)) {
|
|
1181
|
+
// ── Call expression: extract method name + inner receiver ────────────
|
|
1182
|
+
const funcNode = current.childForFieldName?.('function')
|
|
1183
|
+
?? current.childForFieldName?.('name')
|
|
1184
|
+
?? current.childForFieldName?.('method');
|
|
1185
|
+
let methodName;
|
|
1186
|
+
let innerReceiver = null;
|
|
1187
|
+
if (funcNode) {
|
|
1188
|
+
methodName = funcNode.lastNamedChild?.text ?? funcNode.text;
|
|
1189
|
+
}
|
|
1190
|
+
// Kotlin/Swift: call_expression → navigation_expression
|
|
1191
|
+
if (!funcNode && current.type === 'call_expression') {
|
|
1192
|
+
const callee = current.firstNamedChild;
|
|
1193
|
+
if (callee?.type === 'navigation_expression') {
|
|
1194
|
+
const suffix = callee.lastNamedChild;
|
|
1195
|
+
if (suffix?.type === 'navigation_suffix') {
|
|
1196
|
+
methodName = suffix.lastNamedChild?.text;
|
|
1197
|
+
for (let i = 0; i < callee.namedChildCount; i++) {
|
|
1198
|
+
const child = callee.namedChild(i);
|
|
1199
|
+
if (child && child.type !== 'navigation_suffix') {
|
|
1200
|
+
innerReceiver = child;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (!methodName)
|
|
1208
|
+
break;
|
|
1209
|
+
chain.unshift({ kind: 'call', name: methodName });
|
|
1210
|
+
if (!innerReceiver && funcNode) {
|
|
1211
|
+
innerReceiver = funcNode.childForFieldName?.('object')
|
|
1212
|
+
?? funcNode.childForFieldName?.('value')
|
|
1213
|
+
?? funcNode.childForFieldName?.('operand')
|
|
1214
|
+
?? funcNode.childForFieldName?.('argument') // C/C++ field_expression
|
|
1215
|
+
?? funcNode.childForFieldName?.('expression')
|
|
1216
|
+
?? null;
|
|
1217
|
+
}
|
|
1218
|
+
if (!innerReceiver && current.type === 'method_invocation') {
|
|
1219
|
+
innerReceiver = current.childForFieldName?.('object') ?? null;
|
|
1220
|
+
}
|
|
1221
|
+
if (!innerReceiver && (current.type === 'member_call_expression' || current.type === 'nullsafe_member_call_expression')) {
|
|
1222
|
+
innerReceiver = current.childForFieldName?.('object') ?? null;
|
|
1223
|
+
}
|
|
1224
|
+
if (!innerReceiver && current.type === 'call') {
|
|
1225
|
+
innerReceiver = current.childForFieldName?.('receiver') ?? null;
|
|
1226
|
+
}
|
|
1227
|
+
if (!innerReceiver)
|
|
1228
|
+
break;
|
|
1229
|
+
if (CALL_EXPRESSION_TYPES.has(innerReceiver.type) || FIELD_ACCESS_NODE_TYPES.has(innerReceiver.type)) {
|
|
1230
|
+
current = innerReceiver;
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
return { chain, baseReceiverName: innerReceiver.text || undefined };
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
else if (FIELD_ACCESS_NODE_TYPES.has(current.type)) {
|
|
1237
|
+
// ── Field/member access: extract property name + inner object ─────────
|
|
1238
|
+
let propertyName;
|
|
1239
|
+
let innerObject = null;
|
|
1240
|
+
if (current.type === 'navigation_expression') {
|
|
1241
|
+
for (const child of current.children ?? []) {
|
|
1242
|
+
if (child.type === 'navigation_suffix') {
|
|
1243
|
+
for (const sc of child.children ?? []) {
|
|
1244
|
+
if (sc.isNamed && sc.type !== '.') {
|
|
1245
|
+
propertyName = sc.text;
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
else if (child.isNamed && !innerObject) {
|
|
1251
|
+
innerObject = child;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
else if (current.type === 'attribute') {
|
|
1256
|
+
innerObject = current.childForFieldName?.('object') ?? null;
|
|
1257
|
+
propertyName = current.childForFieldName?.('attribute')?.text;
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
innerObject = current.childForFieldName?.('object')
|
|
1261
|
+
?? current.childForFieldName?.('value')
|
|
1262
|
+
?? current.childForFieldName?.('operand')
|
|
1263
|
+
?? current.childForFieldName?.('argument') // C/C++ field_expression
|
|
1264
|
+
?? current.childForFieldName?.('expression')
|
|
1265
|
+
?? null;
|
|
1266
|
+
propertyName = (current.childForFieldName?.('property')
|
|
1267
|
+
?? current.childForFieldName?.('field')
|
|
1268
|
+
?? current.childForFieldName?.('name'))?.text;
|
|
1269
|
+
}
|
|
1270
|
+
if (!propertyName)
|
|
1271
|
+
break;
|
|
1272
|
+
chain.unshift({ kind: 'field', name: propertyName });
|
|
1273
|
+
if (!innerObject)
|
|
1274
|
+
break;
|
|
1275
|
+
if (CALL_EXPRESSION_TYPES.has(innerObject.type) || FIELD_ACCESS_NODE_TYPES.has(innerObject.type)) {
|
|
1276
|
+
current = innerObject;
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
return { chain, baseReceiverName: innerObject.text || undefined };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
// Simple identifier — this is the base receiver
|
|
1284
|
+
return chain.length > 0
|
|
1285
|
+
? { chain, baseReceiverName: current.text || undefined }
|
|
1286
|
+
: undefined;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
|
|
1290
|
+
}
|