From 4a99c7fe9adc960b8ef6c56ee48ddac5a481d2f1 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 15 Mar 2026 09:44:51 +0000 Subject: [PATCH] refactor: merge CONSTRUCTOR_BINDING_SCANNERS into per-language LanguageTypeConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates the parallel dispatch map in type-env.ts by moving all 11 constructor binding scanners into their respective type-extractors/*.ts files as `scanConstructorBinding` on LanguageTypeConfig. - Add ConstructorBindingScanner type to types.ts - Add shared helpers: hasTypeAnnotation, unwrapAwait, extractCalleeName - Move scanners to typescript.ts, jvm.ts, python.ts, php.ts, go.ts, rust.ts, swift.ts, c-cpp.ts, csharp.ts, ruby.ts - Fix `any` types in C# scanner → SyntaxNode | null - Delete ~300 lines from type-env.ts (CONSTRUCTOR_BINDING_SCANNERS map) - Update buildTypeEnv to use config.scanConstructorBinding All 143 type-env unit tests and all 10 language integration suites pass. --- ...4-feat-type-resolution-gap-closure-plan.md | 259 -------------- ...-feat-return-type-inference-phase4-plan.md | 328 ------------------ gitnexus/src/core/ingestion/type-env.ts | 316 +---------------- .../core/ingestion/type-extractors/c-cpp.ts | 37 +- .../core/ingestion/type-extractors/csharp.ts | 37 +- .../src/core/ingestion/type-extractors/go.ts | 23 +- .../core/ingestion/type-extractors/index.ts | 2 +- .../src/core/ingestion/type-extractors/jvm.ts | 39 ++- .../src/core/ingestion/type-extractors/php.ts | 23 +- .../core/ingestion/type-extractors/python.ts | 30 +- .../core/ingestion/type-extractors/ruby.ts | 1 + .../core/ingestion/type-extractors/rust.ts | 33 +- .../core/ingestion/type-extractors/shared.ts | 32 ++ .../core/ingestion/type-extractors/swift.ts | 34 +- .../core/ingestion/type-extractors/types.ts | 8 + .../ingestion/type-extractors/typescript.ts | 22 +- 16 files changed, 312 insertions(+), 912 deletions(-) delete mode 100644 gitnexus/docs/plans/2026-03-14-feat-type-resolution-gap-closure-plan.md delete mode 100644 gitnexus/docs/plans/2026-03-15-feat-return-type-inference-phase4-plan.md diff --git a/gitnexus/docs/plans/2026-03-14-feat-type-resolution-gap-closure-plan.md b/gitnexus/docs/plans/2026-03-14-feat-type-resolution-gap-closure-plan.md deleted file mode 100644 index 68c73729e..000000000 --- a/gitnexus/docs/plans/2026-03-14-feat-type-resolution-gap-closure-plan.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: "feat: Close type resolution gaps across all 12 languages" -type: feat -status: active -date: 2026-03-14 ---- - -# Close Type Resolution Gaps Across All 12 Languages - -## Overview - -The type resolution system (TypeEnv + call-processor) has been hardened through 6 rounds of review on `feat/type-resolution-constructor-inference`. This plan closes the remaining gaps identified by the comprehensive 4-agent gap analysis covering all 12 supported languages. - -Every task MUST include per-language integration tests following the established pattern: -- Fixture directory: `test/fixtures/lang-resolution/-/` -- Integration test: `test/integration/resolvers/.test.ts` -- Unit tests: `test/unit/type-env.test.ts` where applicable - -## Problem Statement - -The current TypeEnv extracts types from explicit annotations (Tier 0) and constructor calls (Tier 1). Many common patterns across all languages produce no type binding, causing receiver-type resolution to fail silently. The gap analysis identified 14 categories of missing patterns affecting call resolution accuracy. - -## Implementation Phases - -### Phase 1: Quick Wins (Independent — can run in parallel) - -Each task is self-contained. No dependencies between them. - -#### Task 1.1: Python walrus operator `:=` -- **File**: `src/core/ingestion/type-extractors/python.ts` -- **Change**: Add `named_expression` to `DECLARATION_NODE_TYPES` -- **Why**: `if (user := get_user()):` is common Python 3.8+ pattern; `user` gets no type binding -- **Pattern**: `named_expression` has `name` (identifier) and `value` (call) children -- **Tests**: - - Unit: `type-env.test.ts` — walrus with call, walrus with annotated type - - Integration fixture: `test/fixtures/lang-resolution/python-walrus-operator/` - - Integration test: `python.test.ts` — `user := User(); user.save()` resolves - -#### Task 1.2: PHP typed class properties -- **File**: `src/core/ingestion/type-extractors/php.ts` -- **Change**: Add `property_declaration` to `DECLARATION_NODE_TYPES`, implement `extractDeclaration` for it -- **Why**: `private User $repo;` is the primary PHP 7.4+ property declaration pattern -- **Pattern**: `property_declaration` has `type` (named_type) and child `property_element` with `name` (variable_name) -- **Tests**: - - Unit: `type-env.test.ts` — typed property extraction - - Integration fixture: `test/fixtures/lang-resolution/php-typed-properties/` - - Integration test: `php.test.ts` — `private UserRepo $repo; $repo->save()` resolves - -#### Task 1.3: Nullable receiver unwrapping -- **File**: `src/core/ingestion/utils.ts` — `extractReceiverName` function (~line 800) -- **Change**: Before returning `undefined` for non-simple receivers, check if the receiver node wraps a simple identifier with an optional chain operator -- **Patterns to handle**: - - TS/JS: `optional_chain_expression` wrapping `member_expression` - - Kotlin: `safe_navigation_expression` - - C#: `conditional_access_expression` - - Swift: `optional_chaining_expression` -- **Approach**: Add the wrapped expression types to `SIMPLE_RECEIVER_TYPES` or unwrap one level before checking -- **Tests**: - - Integration fixtures per language: `ts-nullable-receiver/`, `kotlin-nullable-receiver/`, etc. - - Integration tests: `user?.save()` resolves same as `user.save()` - -#### Task 1.4: Go `make()` builtin -- **File**: `src/core/ingestion/type-extractors/go.ts` -- **Change**: In `extractGoShortVarDeclaration`, add a `call_expression` branch for `make` (similar to existing `new` branch) -- **Pattern**: `make([]User, 0)` — first arg is `slice_type`/`map_type` with element type -- **Approach**: Extract element type from the first argument's type node -- **Tests**: - - Unit: `type-env.test.ts` — `make([]User, 0)`, `make(map[string]User)` - - Integration fixture: `test/fixtures/lang-resolution/go-make-builtin/` - - Integration test: `go.test.ts` - -#### Task 1.5: Go type assertions -- **File**: `src/core/ingestion/type-extractors/go.ts` -- **Change**: Handle `type_assertion_expression` in short var declarations -- **Pattern**: `user, ok := iface.(User)` — `type_assertion_expression` has a `type` field -- **Tests**: - - Unit: `type-env.test.ts` — type assertion single and comma-ok forms - - Integration fixture: `test/fixtures/lang-resolution/go-type-assertion/` - - Integration test: `go.test.ts` - ---- - -### Phase 2: Medium Effort (Some dependencies noted) - -#### Task 2.1: C++ range-for loop variables -- **File**: `src/core/ingestion/type-extractors/c-cpp.ts` -- **Change**: Add `for_range_loop` to `DECLARATION_NODE_TYPES`, extract the type from the declaration part -- **Pattern**: `for (auto& user : users)` — the `for_range_loop` has a `type` and `declarator` child -- **Note**: When type is `auto`, would need collection element type inference (deferred to Phase 3). For explicit types `for (User& u : users)` this works now. -- **Tests**: - - Unit: explicit type in range-for - - Integration fixture: `test/fixtures/lang-resolution/cpp-range-for/` - - Integration test: `cpp.test.ts` - -#### Task 2.2: Rust `if let` / `while let` bindings -- **File**: `src/core/ingestion/type-extractors/rust.ts` -- **Change**: Add `if_let_expression` and `while_let_expression` to `DECLARATION_NODE_TYPES` or handle in `extractDeclaration` -- **Pattern**: `if let Some(user) = opt { user.save() }` — pattern binding inside conditional -- **Approach**: Extract the pattern variable and the matched type from the source expression -- **Note**: Full generic unwrapping (Some → User) is Phase 3. Initial version extracts the binding variable with no type — still useful for scope tracking. -- **Tests**: - - Unit: if-let with annotated type, while-let - - Integration fixture: `test/fixtures/lang-resolution/rust-if-let/` - - Integration test: `rust.test.ts` - -#### Task 2.3: Swift `guard let` / `if let` bindings -- **File**: `src/core/ingestion/type-extractors/swift.ts` -- **Change**: Add `guard_statement` and `if_statement` with `optional_binding_condition` to declaration handling -- **Pattern**: `guard let user = fetchUser() else { return }` — `optional_binding_condition` has `pattern` and `value` -- **Note**: Swift parser availability varies (Node 22 issue). Tests must use `describe.skipIf(!swiftAvailable)`. -- **Tests**: - - Integration fixture: `test/fixtures/lang-resolution/swift-guard-let/` - - Integration test: `swift.test.ts` with skipIf guard - -#### Task 2.4: C# pattern matching `is Type variable` -- **File**: `src/core/ingestion/type-extractors/csharp.ts` -- **Change**: Handle `is_pattern_expression` or `declaration_pattern` in the type extractor -- **Pattern**: `if (obj is User user) { user.Save(); }` — `declaration_pattern` has `type` and `name` -- **Tests**: - - Unit: `type-env.test.ts` — is-pattern with type - - Integration fixture: `test/fixtures/lang-resolution/csharp-pattern-matching/` - - Integration test: `csharp.test.ts` - -#### Task 2.5: Python class-level type annotations -- **File**: `src/core/ingestion/type-extractors/python.ts` -- **Change**: Ensure `assignment` declarationNodeTypes also captures class body assignments with type annotations -- **Pattern**: `class User: name: str = "default"` — tree-sitter may use `expression_statement` > `assignment` inside class body -- **Note**: Check if this already works via existing `assignment` handling — may just need scope key fix -- **Tests**: - - Unit: class-level annotation - - Integration fixture: `test/fixtures/lang-resolution/python-class-annotations/` - ---- - -### Phase 3: Architecture Changes (Sequential — requires design decisions) - -#### Task 3.1: Return type inference -- **Files**: `src/core/ingestion/type-env.ts`, `src/core/ingestion/call-processor.ts` -- **Change**: When processing calls, look up the callee's return type from SymbolTable and bind the assignment target -- **Approach**: - 1. `extractMethodSignature` already stores `returnType` in symbol metadata - 2. In `buildTypeEnv` or as a post-processing step, for assignments like `let x = foo()`, look up `foo` in SymbolTable - 3. If `foo.returnType` exists, add binding `x → returnType` - 4. Must strip nullable wrappers and generic containers to get base type -- **Risk**: Circular dependencies if two functions return each other's types. Mitigate with depth limit. -- **Tests**: Per-language integration tests for return-type-inferred receiver resolution - -#### Task 3.2: Chained property access resolution -- **Files**: `src/core/ingestion/utils.ts` (`extractReceiverName`), `src/core/ingestion/call-processor.ts` -- **Change**: When receiver is `this.repo`, resolve `this` → class type, then look up `repo` property type on that class -- **Approach**: - 1. Extend `extractReceiverName` to return structured data: `{ chain: ['this', 'repo'] }` for multi-level access - 2. In call-processor, resolve chain iteratively: lookup first element, find property type, continue - 3. Requires property-type tracking: store class property types in TypeEnv or SymbolTable -- **Risk**: Performance impact from recursive lookups. Limit chain depth to 3. -- **Tests**: Per-language `this.repo.save()` / `self.db.query()` patterns - -#### Task 3.3: For-loop variable typing -- **Files**: Per-language type extractors + `src/core/ingestion/type-env.ts` -- **Change**: Extract loop variable type from explicit annotations or inferred from collection type -- **Patterns per language**: - - TS/JS: `for (const x of collection)` — `for_in_statement` with `left` and `right` - - Java/C#: `for (Type x : collection)` — explicit type already works - - Go: `for _, v := range slice` — `range_clause` with identifier - - Python: `for x in collection` — `for_statement` with `left` - - Rust: `for x in iter` — `for_expression` with `pattern` -- **Depends on**: Task 3.1 (return type inference) for inferring collection element types -- **Tests**: Per-language integration tests - -#### Task 3.4: Generic type parameter extraction -- **File**: `src/core/ingestion/type-extractors/shared.ts` — `extractSimpleTypeName` -- **Change**: Add optional `extractGenericArgs` mode that returns type parameters alongside base type -- **Current**: `List` → `List` (base only) -- **New**: `List` → `{ base: 'List', args: ['User'] }` when requested -- **Use case**: For-loop element type inference, collection method resolution -- **Tests**: Unit tests for each language's generic syntax - -#### Task 3.5: Block-level type narrowing -- **File**: `src/core/ingestion/type-env.ts` — scope handling -- **Change**: Add sub-function scope support for if/match/guard blocks -- **Current**: Scope keys are `funcName@startIndex` or `''` (file) -- **New**: Add block scope keys like `funcName@startIndex#if@lineN` -- **Risk**: Scope lookup complexity increases. Need to walk up scope chain on miss. -- **Tests**: Per-language pattern matching, instanceof, type guards - -#### Task 3.6: Ruby dedicated type extractor -- **File**: New `src/core/ingestion/type-extractors/ruby.ts` -- **Change**: Replace the stub in `index.ts` with a real type extractor -- **Features**: - - YARD annotation parsing (`@param name [Type]`, `@return [Type]`) - - Instance variable type inference from constructor assignments - - Block parameter typing heuristics -- **Tests**: Integration fixture: `test/fixtures/lang-resolution/ruby-typed-methods/` - ---- - -## Acceptance Criteria - -### Functional Requirements -- [ ] Each Phase 1 task has unit tests AND per-language integration tests with fixtures -- [ ] Each Phase 2 task has per-language integration tests with fixtures -- [ ] Each Phase 3 task has cross-language integration tests -- [ ] All existing 101 unit tests continue to pass -- [ ] All existing integration tests continue to pass -- [ ] No regressions in existing call resolution - -### Quality Gates -- [ ] `npx vitest run test/unit/type-env.test.ts` — all pass -- [ ] `npx vitest run test/integration/resolvers/ --no-file-parallelism` — all pass -- [ ] No new TypeScript compilation errors - -### Testing Pattern (MANDATORY for every task) -``` -1. Create fixture: test/fixtures/lang-resolution/-/ - - Source file with the pattern (e.g., main.py, models.py) - - Class/struct definitions with methods to resolve against -2. Add describe block: test/integration/resolvers/.test.ts - - beforeAll: runPipelineFromRepo(fixture_path) - - Tests: verify CALLS edges resolve to correct file/method -3. Add unit tests: test/unit/type-env.test.ts (where applicable) - - Parse code snippet, verify TypeEnv bindings -``` - -## Task Dependency Graph - -``` -Phase 1 (all parallel, no dependencies): - 1.1 Python walrus ─┐ - 1.2 PHP properties ├─ All independent - 1.3 Nullable unwrap ├─ Can run as parallel swarm agents - 1.4 Go make() │ - 1.5 Go type assert ─┘ - -Phase 2 (mostly parallel): - 2.1 C++ range-for ─┐ - 2.2 Rust if-let ├─ Independent of each other - 2.3 Swift guard-let ├─ Can run as parallel swarm agents - 2.4 C# pattern match │ - 2.5 Python class ann ─┘ - -Phase 3 (sequential dependencies): - 3.1 Return type inference ──→ 3.3 For-loop typing - ↑ - 3.4 Generic extraction ────────────┘ - - 3.2 Chained property access (independent) - 3.5 Block-level scoping (independent) - 3.6 Ruby type extractor (independent) -``` - -## Sources & References - -- PR #274: `feat/type-resolution-constructor-inference` — 6 rounds of review fixes -- Existing patterns: `test/fixtures/lang-resolution/` (150 fixture dirs) -- Integration tests: `test/integration/resolvers/*.test.ts` (12 languages) -- Type extractors: `src/core/ingestion/type-extractors/*.ts` (9 files + index) -- TypeEnv: `src/core/ingestion/type-env.ts` -- Call processor: `src/core/ingestion/call-processor.ts` -- Receiver extraction: `src/core/ingestion/utils.ts:extractReceiverName` diff --git a/gitnexus/docs/plans/2026-03-15-feat-return-type-inference-phase4-plan.md b/gitnexus/docs/plans/2026-03-15-feat-return-type-inference-phase4-plan.md deleted file mode 100644 index 2b09a2e68..000000000 --- a/gitnexus/docs/plans/2026-03-15-feat-return-type-inference-phase4-plan.md +++ /dev/null @@ -1,328 +0,0 @@ ---- -title: "feat: Phase 4 — Complete Return Type Inference for All Languages" -type: feat -status: active -date: 2026-03-15 ---- - -# Phase 4: Complete Return Type Inference for All Languages - -## Overview - -Phase 3 established the return type inference pipeline: when a function call result is assigned to a variable (`val user = getUser()`), and the called function has a known return type in the SymbolTable, subsequent member calls on that variable (`user.save()`) resolve to the return type's methods. This works via `CONSTRUCTOR_BINDING_SCANNERS` in `type-env.ts` which capture `var = callee()` assignments, and `processCallsFromExtracted` in `call-processor.ts` which looks up the callee's return type. - -**Problem:** Only 5 of 12 languages have scanners (Kotlin, Python, Swift, C++, Ruby). The remaining 7 languages (TypeScript/JS, Go, Java, C#, Rust, PHP) can only resolve member calls when variables have explicit type annotations — they miss the common `var x = func()` pattern. - -## Problem Statement - -Without scanners, this code produces no CALLS edge for `user.save()`: - -```typescript -// TypeScript — no scanner, so `user` type is unknown -const user = getUser("alice"); // ← not captured -user.save(); // ← unresolved -``` - -But this works because extractDeclaration handles explicit types: -```typescript -const user: User = getUser("alice"); // ← extractDeclaration captures User -user.save(); // ← resolves to User#save -``` - -## Proposed Solution - -Add `CONSTRUCTOR_BINDING_SCANNERS` for all missing languages. Each scanner captures `var = callee()` patterns where the variable has no explicit type annotation, emitting a `{ varName, calleeName }` binding that `processCallsFromExtracted` resolves via the SymbolTable. - -## Technical Approach - -### Architecture - -The pipeline is already built — we just need per-language AST pattern matchers: - -``` -AST walk (type-env.ts buildTypeEnv) - ↓ scanner(node) → { varName, calleeName } - ↓ collected in constructorBindings[] - ↓ passed to processCallsFromExtracted - ↓ ctx.resolve(calleeName) → SymbolDefinition - ↓ if Class → bind varName to calleeName - ↓ if Function/Method with returnType → extractReturnTypeName → bind varName to return type - ↓ receiverMap[varName] = typeName - ↓ resolveCallTarget uses receiverTypeName for member calls -``` - -### Phase 4.1: TypeScript/JavaScript Scanner - -**AST pattern:** `lexical_declaration` → `variable_declarator` with no type_annotation and call_expression value - -```typescript -// const user = getUser("alice") -// AST: lexical_declaration > variable_declarator[name=identifier, value=call_expression] -[SupportedLanguages.TypeScript]: (node) => { - if (node.type !== 'variable_declarator') return undefined; - // Skip if parent is not a lexical/variable declaration - const parent = node.parent; - if (!parent || (parent.type !== 'lexical_declaration' && parent.type !== 'variable_declaration')) return undefined; - // Skip if has type annotation - if (node.childForFieldName('type')) return undefined; - for (const child of node.children) { - if (child.type === 'type_annotation') return undefined; - } - const nameNode = node.childForFieldName('name'); - if (!nameNode || nameNode.type !== 'identifier') return undefined; - const value = node.childForFieldName('value'); - if (!value || value.type !== 'call_expression') return undefined; - const func = value.childForFieldName('function'); - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: nameNode.text, calleeName }; -}, -// JavaScript shares the same config -[SupportedLanguages.JavaScript]: /* same as TypeScript */ -``` - -**Key files:** -- `src/core/ingestion/type-env.ts:424` — add to CONSTRUCTOR_BINDING_SCANNERS -- `test/unit/type-env.test.ts` — unit tests for the scanner -- `test/integration/resolvers/typescript.test.ts` — update existing return type tests - -**Node types to handle:** -- `variable_declarator` inside `lexical_declaration` (const/let) -- `variable_declarator` inside `variable_declaration` (var) -- Must skip destructuring patterns (`array_pattern`, `object_pattern`) - -### Phase 4.2: Go Scanner - -**AST pattern:** `short_var_declaration` → left identifier, right call_expression - -```go -// user := GetUser("alice") -// AST: short_var_declaration[left=expression_list>identifier, right=expression_list>call_expression] -[SupportedLanguages.Go]: (node) => { - if (node.type !== 'short_var_declaration') return undefined; - const left = node.childForFieldName('left'); - const right = node.childForFieldName('right'); - if (!left || !right) return undefined; - // Single assignment only (skip multi-assign like `a, b := ...`) - const leftIds = left.namedChildren.filter(c => c.type === 'identifier'); - if (leftIds.length !== 1) return undefined; - // Right must be a single call_expression - const rightExprs = right.namedChildren; - if (rightExprs.length !== 1 || rightExprs[0].type !== 'call_expression') return undefined; - const func = rightExprs[0].childForFieldName('function'); - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: leftIds[0].text, calleeName }; -}, -``` - -**Key files:** -- `src/core/ingestion/type-env.ts:424` — add Go scanner -- `test/integration/resolvers/go.test.ts` — update return type tests - -**Edge cases:** -- Multi-return: `user, err := GetUser()` — skip (multiple left identifiers) -- Composite literal: `user := User{Name: "alice"}` — already handled by extractInitializer -- Selector call: `user := models.GetUser()` — extractSimpleTypeName handles `selector_expression` - -### Phase 4.3: Java Scanner - -**AST pattern:** `local_variable_declaration` with `var` type → `variable_declarator` with call value - -```java -// var user = getUser("alice"); -// AST: local_variable_declaration[type=void_type("var"), declarator=variable_declarator[value=method_invocation]] -[SupportedLanguages.Java]: (node) => { - if (node.type !== 'local_variable_declaration') return undefined; - const typeNode = node.childForFieldName('type'); - if (!typeNode) return undefined; - // Only handle `var` — explicitly typed declarations are handled by extractDeclaration - if (typeNode.text !== 'var') return undefined; - const declarator = node.namedChildren.find(c => c.type === 'variable_declarator'); - if (!declarator) return undefined; - const nameNode = declarator.childForFieldName('name'); - const value = declarator.childForFieldName('value'); - if (!nameNode || !value) return undefined; - if (value.type !== 'method_invocation') return undefined; - const methodName = value.childForFieldName('name'); - if (!methodName) return undefined; - // For qualified calls (obj.getUser()), take the method name - // For unqualified calls (getUser()), take the function name - const calleeName = methodName.text; - if (!calleeName) return undefined; - return { varName: nameNode.text, calleeName }; -}, -``` - -**Key files:** -- `src/core/ingestion/type-env.ts:424` — add Java scanner -- `test/integration/resolvers/java.test.ts` — update return type tests - -**Edge cases:** -- Java 10+ `var` only — pre-10 declarations always have explicit types -- `var list = List.of(...)` — method_invocation with object reference -- `var user = new User()` — object_creation_expression (already handled by extractInitializer) - -### Phase 4.4: C# Scanner - -**AST pattern:** `local_declaration_statement` → `variable_declaration` with `implicit_type` (var) - -```csharp -// var user = GetUser("alice"); -// AST: local_declaration_statement > variable_declaration[type=implicit_type, declarator=variable_declarator[value=invocation_expression]] -[SupportedLanguages.CSharp]: (node) => { - if (node.type !== 'variable_declaration') return undefined; - const typeNode = node.childForFieldName('type'); - if (!typeNode || typeNode.type !== 'implicit_type') return undefined; - const declarator = node.namedChildren.find(c => c.type === 'variable_declarator'); - if (!declarator) return undefined; - const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild; - if (!nameNode) return undefined; - const eqClause = declarator.namedChildren.find(c => c.type === 'equals_value_clause'); - if (!eqClause) return undefined; - const value = eqClause.firstNamedChild; - if (!value || value.type !== 'invocation_expression') return undefined; - const func = value.firstNamedChild; - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: nameNode.text, calleeName }; -}, -``` - -**Key files:** -- `src/core/ingestion/type-env.ts:424` — add C# scanner -- `test/integration/resolvers/csharp.test.ts` — add return type tests - -### Phase 4.5: Rust Scanner - -**AST pattern:** `let_declaration` with no type annotation and call_expression value - -```rust -// let user = get_user("alice"); -// AST: let_declaration[pattern=identifier, value=call_expression] -[SupportedLanguages.Rust]: (node) => { - if (node.type !== 'let_declaration') return undefined; - // Skip if has type annotation - if (node.childForFieldName('type')) return undefined; - for (const child of node.children) { - if (child.type === 'type_annotation') return undefined; - } - const pattern = node.childForFieldName('pattern'); - if (!pattern || pattern.type !== 'identifier') return undefined; - const value = node.childForFieldName('value'); - if (!value || value.type !== 'call_expression') return undefined; - const func = value.childForFieldName('function'); - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: pattern.text, calleeName }; -}, -``` - -**Key files:** -- `src/core/ingestion/type-env.ts:424` — add Rust scanner -- `test/integration/resolvers/rust.test.ts` — add return type tests - -### Phase 4.6: PHP Scanner - -**AST pattern:** `expression_statement` → `assignment_expression` with function call - -```php -// $user = getUser("alice"); -// AST: expression_statement > assignment_expression[left=variable_name, right=function_call_expression] -[SupportedLanguages.PHP]: (node) => { - if (node.type !== 'assignment_expression') return undefined; - const left = node.childForFieldName('left'); - const right = node.childForFieldName('right'); - if (!left || !right) return undefined; - if (left.type !== 'variable_name') return undefined; - if (right.type !== 'function_call_expression' && right.type !== 'member_call_expression') return undefined; - const func = right.childForFieldName('function') ?? right.childForFieldName('name'); - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - // Strip leading $ from PHP variable names - const varName = left.text.startsWith('$') ? left.text.slice(1) : left.text; - return { varName, calleeName }; -}, -``` - -**Key files:** -- `src/core/ingestion/type-env.ts:424` — add PHP scanner -- `test/integration/resolvers/php.test.ts` — add return type tests (if exists) - -### Phase 4.7: Ruby YARD @return Annotation - -**Current gap:** Ruby's `extractMethodSignature` in `utils.ts:531-650` doesn't check YARD doc comments for `@return [Type]`. Python has a similar gap with `:rtype:` but Python type hints are more common. - -**Approach:** Add a per-language `extractReturnTypeFromComment` hook called from `extractMethodSignature` when no AST return type is found. - -```ruby -# @return [User] the found user -def get_user(name) - User.find_by(name: name) -end -``` - -**Implementation:** -1. After the existing return type extraction in `extractMethodSignature` (line ~640), if `returnType` is still undefined, check preceding sibling comment nodes -2. Parse `@return [Type]` (Ruby YARD) and `:rtype: Type` (Python docstring in comment form) -3. This is a cross-cutting concern — add a `RETURN_TYPE_COMMENT_PATTERNS` map in `type-env.ts` keyed by language - -**Key files:** -- `src/core/ingestion/utils.ts:531-650` — add comment-based return type extraction -- `src/core/ingestion/type-env.ts` — add RETURN_TYPE_COMMENT_PATTERNS - -**Pattern per language:** -- Ruby YARD: `# @return [User]` or `# @return [Array]` -- Python: `""":rtype: User"""` (in docstring) — may need special AST handling -- JSDoc: `/** @returns {User} */` — already works via TS type annotations mostly - -## Acceptance Criteria - -- [ ] TypeScript/JavaScript: `const user = getUser()` → `user.save()` resolves via return type -- [ ] Go: `user := GetUser()` → `user.Save()` resolves via return type -- [ ] Java: `var user = getUser()` → `user.save()` resolves via return type -- [ ] C#: `var user = GetUser()` → `user.Save()` resolves via return type -- [ ] Rust: `let user = get_user()` → `user.save()` resolves via return type -- [ ] PHP: `$user = getUser()` → `$user->save()` resolves via return type -- [ ] Ruby YARD: `@return [User]` populates returnType in SymbolDefinition -- [ ] All existing tests continue to pass (no regressions) -- [ ] Integration tests for each new scanner -- [ ] Unit tests for each scanner's AST pattern matching - -## Dependencies & Risks - -**Dependencies:** -- Phase 3 changes must be committed first (WRAPPER_GENERICS fix, Ruby `::` handling) -- `extractSimpleTypeName` in `shared.ts` must handle each language's AST node types - -**Risks:** -- **Low:** Scanner false positives — a function call assigned to a variable where the function has no return type is harmless (no binding created) -- **Medium:** Go multi-return — `user, err := GetUser()` must be skipped (handled by leftIds.length check) -- **Medium:** PHP variable name collision — `$user` in different scopes could collide (scope-aware keying already handles this) -- **Low:** YARD parsing — regex on comment text is fragile but good enough for the common `@return [Type]` pattern - -## Implementation Order - -Recommended order (easiest → hardest, most impactful first): - -1. **TypeScript/JavaScript** — largest user base, simplest AST pattern -2. **Go** — common `:=` pattern, straightforward -3. **Java** — `var` keyword, well-defined AST -4. **C#** — `var` keyword, similar to Java -5. **Rust** — `let` without annotation, clean AST -6. **PHP** — `$var = func()`, needs `$` stripping -7. **Ruby YARD** — different mechanism (comment parsing, not scanner) - -## Sources - -- `src/core/ingestion/type-env.ts:424-512` — existing CONSTRUCTOR_BINDING_SCANNERS -- `src/core/ingestion/call-processor.ts:462-485` — return type inference in processCallsFromExtracted -- `src/core/ingestion/call-processor.ts:381-425` — extractReturnTypeName -- `src/core/ingestion/utils.ts:531-650` — extractMethodSignature -- `src/core/ingestion/type-extractors/types.ts` — LanguageTypeConfig interface -- Phase 3 plan: `docs/plans/2026-03-14-feat-type-resolution-gap-closure-plan.md` diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index 6016bfd55..0d2505e8b 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -3,7 +3,7 @@ import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES } from import { SupportedLanguages } from '../../config/supported-languages.js'; import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js'; import type { ClassNameLookup } from './type-extractors/types.js'; -import { extractSimpleTypeName, extractRubyConstructorAssignment } from './type-extractors/shared.js'; +import { extractSimpleTypeName } from './type-extractors/shared.js'; import type { SymbolTable } from './symbol-table.js'; /** @@ -270,20 +270,14 @@ const createClassNameLookup = ( }; /** - * Build a scoped TypeEnv from a tree-sitter AST for a given language. - * Single-pass: collects class/struct names AND type bindings in one walk. - * Class names are accumulated incrementally — this is safe because no - * language allows constructing a class before its definition. + * Build a TypeEnvironment from a tree-sitter AST for a given language. + * Single-pass: collects class/struct names, type bindings, AND constructor + * bindings that couldn't be resolved locally — all in one AST walk. * * When a symbolTable is provided (call-processor path), class names from across * the project are available for constructor inference in languages like Kotlin * where constructors are syntactically identical to function calls. */ -/** - * Build a TypeEnvironment from a tree-sitter AST for a given language. - * Single-pass: collects class/struct names, type bindings, AND constructor - * bindings that couldn't be resolved locally — all in one AST walk. - */ export const buildTypeEnv = ( tree: { rootNode: SyntaxNode }, language: SupportedLanguages, @@ -293,7 +287,6 @@ export const buildTypeEnv = ( const localClassNames = new Set(); const classNames = createClassNameLookup(localClassNames, symbolTable); const config = typeConfigs[language]; - const scanner = CONSTRUCTOR_BINDING_SCANNERS[language]; const bindings: ConstructorBinding[] = []; /** @@ -347,8 +340,8 @@ export const buildTypeEnv = ( // Scan for constructor bindings that couldn't be resolved locally. // Only collect if TypeEnv didn't already resolve this binding. - if (scanner) { - const result = scanner(node); + if (config.scanConstructorBinding) { + const result = config.scanConstructorBinding(node); if (result && !scopeEnv.has(result.varName)) { bindings.push({ scope, ...result }); } @@ -383,301 +376,4 @@ export interface ConstructorBinding { calleeName: string; } -/** C/C++: auto x = User() where function is an identifier (not type_identifier) */ -const extractCppConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => { - if (node.type !== 'declaration') return undefined; - const typeNode = node.childForFieldName('type'); - if (!typeNode) return undefined; - const typeText = typeNode.text; - if (typeText !== 'auto' && typeText !== 'decltype(auto)' && typeNode.type !== 'placeholder_type_specifier') return undefined; - const declarator = node.childForFieldName('declarator'); - if (!declarator || declarator.type !== 'init_declarator') return undefined; - const value = declarator.childForFieldName('value'); - if (!value || value.type !== 'call_expression') return undefined; - const func = value.childForFieldName('function'); - // Match plain identifiers (type_identifier is already resolved by extractInitializer) - // and qualified/scoped identifiers for namespaced calls like ns::HttpClient() - if (!func) return undefined; - if (func.type === 'qualified_identifier' || func.type === 'scoped_identifier') { - // ns::HttpClient → extract "HttpClient" (last segment) - const last = func.lastNamedChild; - if (!last) return undefined; - const nameNode = declarator.childForFieldName('declarator'); - if (!nameNode) return undefined; - const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator' - ? nameNode.firstNamedChild : nameNode; - if (!finalName) return undefined; - return { varName: finalName.text, calleeName: last.text }; - } - if (func.type !== 'identifier') return undefined; - const nameNode = declarator.childForFieldName('declarator'); - if (!nameNode) return undefined; - const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator' - ? nameNode.firstNamedChild : nameNode; - if (!finalName) return undefined; - const varName = finalName.text; - if (!varName) return undefined; - return { varName, calleeName: func.text }; -}; - -/** - * TypeScript/JavaScript: const user = getUser() — variable_declarator with call_expression value. - * Only matches unannotated declarators; annotated ones are handled by extractDeclaration. - * await is unwrapped: const user = await fetchUser() → callee = 'fetchUser'. - */ -const extractTsJsConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => { - if (node.type !== 'variable_declarator') return undefined; - // Skip if has an explicit type annotation — extractDeclaration handles those - if (node.childForFieldName('type')) return undefined; - for (const child of node.children) { - if (child.type === 'type_annotation') return undefined; - } - const nameNode = node.childForFieldName('name'); - if (!nameNode || nameNode.type !== 'identifier') return undefined; - let value = node.childForFieldName('value'); - if (!value) return undefined; - // Unwrap await expressions: const user = await fetchUser() - if (value.type === 'await_expression') { - value = value.firstNamedChild; - if (!value) return undefined; - } - if (value.type !== 'call_expression') return undefined; - // Skip new_expression — extractInitializer handles constructor calls - const func = value.childForFieldName('function'); - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: nameNode.text, calleeName }; -}; - -/** Language-specific constructor-binding scanners. */ -const CONSTRUCTOR_BINDING_SCANNERS: Partial { varName: string; calleeName: string } | undefined>> = { - // TypeScript/JavaScript share the same variable_declarator scanner - [SupportedLanguages.TypeScript]: extractTsJsConstructorBinding, - [SupportedLanguages.JavaScript]: extractTsJsConstructorBinding, - - // Kotlin: val x = User(...) — property_declaration with call_expression - [SupportedLanguages.Kotlin]: (node) => { - if (node.type !== 'property_declaration') return undefined; - const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration'); - if (!varDecl) return undefined; - if (varDecl.namedChildren.some(c => c.type === 'user_type')) return undefined; - const callExpr = node.namedChildren.find(c => c.type === 'call_expression'); - if (!callExpr) return undefined; - const callee = callExpr.firstNamedChild; - if (!callee || callee.type !== 'simple_identifier') return undefined; - const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier'); - if (!nameNode) return undefined; - return { varName: nameNode.text, calleeName: callee.text }; - }, - - // Python: user = User("alice") — assignment with call - // Also handles walrus operator: (user := User("alice")) - [SupportedLanguages.Python]: (node) => { - let left: SyntaxNode | null; - let right: SyntaxNode | null; - - if (node.type === 'named_expression') { - // Walrus operator: (user := User("alice")) - left = node.childForFieldName('name'); - right = node.childForFieldName('value'); - } else if (node.type === 'assignment') { - left = node.childForFieldName('left'); - right = node.childForFieldName('right'); - // Skip annotated assignments — extractDeclaration handles those - if (node.childForFieldName('type')) return undefined; - } else { - return undefined; - } - - if (!left || !right) return undefined; - if (left.type !== 'identifier') return undefined; - if (right.type !== 'call') return undefined; - const func = right.childForFieldName('function'); - if (!func) return undefined; - // Support both direct calls (User()) and qualified calls (models.User()) - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: left.text, calleeName }; - }, - - // Swift: let user = User(name: "alice") — property_declaration with call_expression - [SupportedLanguages.Swift]: (node) => { - if (node.type !== 'property_declaration') return undefined; - // Skip if has type annotation - if (node.childForFieldName('type')) return undefined; - for (let i = 0; i < node.namedChildCount; i++) { - if (node.namedChild(i)?.type === 'type_annotation') return undefined; - } - const pattern = node.childForFieldName('pattern'); - if (!pattern) return undefined; - const varName = pattern.text; - if (!varName) return undefined; - // Find call_expression child - let callExpr: SyntaxNode | null = null; - for (let i = 0; i < node.namedChildCount; i++) { - const child = node.namedChild(i); - if (child?.type === 'call_expression') { callExpr = child; break; } - } - if (!callExpr) return undefined; - const callee = callExpr.firstNamedChild; - if (!callee) return undefined; - // Direct call: User(name: "alice") — simple_identifier callee - if (callee.type === 'simple_identifier') { - return { varName, calleeName: callee.text }; - } - // Explicit init: User.init(name: "alice") — navigation_expression with .init suffix - if (callee.type === 'navigation_expression') { - const receiver = callee.firstNamedChild; - const suffix = callee.lastNamedChild; - if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') { - return { varName, calleeName: receiver.text }; - } - } - return undefined; - }, - - // C++: auto x = User() where User is parsed as identifier (cross-file) - // Note: C is excluded — C has no constructors and `auto` is a storage-class specifier, not type inference. - [SupportedLanguages.CPlusPlus]: extractCppConstructorBinding, - - // Ruby: user = User.new — uses shared helper that also handles Models::User.new - [SupportedLanguages.Ruby]: extractRubyConstructorAssignment, - - // Rust: let user = get_user("alice") — let_declaration with call_expression value, no type annotation. - // Skips `let user: User = ...` (explicit type annotation — handled by extractDeclaration). - // Skips `let user = User::new()` (scoped_identifier callee named "new" — handled by extractInitializer). - // Unwraps `let mut user = get_user()` by looking inside mut_pattern for the inner identifier. - [SupportedLanguages.Rust]: (node) => { - if (node.type !== 'let_declaration') return undefined; - // Skip if has explicit type annotation — extractDeclaration handles those - if (node.childForFieldName('type')) return undefined; - for (const child of node.children) { - if (child.type === 'type_annotation') return undefined; - } - let patternNode = node.childForFieldName('pattern'); - if (!patternNode) return undefined; - // Unwrap mut: `let mut user` → mut_pattern > identifier - if (patternNode.type === 'mut_pattern') { - patternNode = patternNode.firstNamedChild; - if (!patternNode) return undefined; - } - if (patternNode.type !== 'identifier') return undefined; - const value = node.childForFieldName('value'); - if (!value || value.type !== 'call_expression') return undefined; - const func = value.childForFieldName('function'); - if (!func) return undefined; - // Skip Struct::new() patterns — handled by extractInitializer in rust.ts - if (func.type === 'scoped_identifier') { - const methodName = func.lastNamedChild; - if (methodName?.text === 'new') return undefined; - } - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: patternNode.text, calleeName }; - }, - - // PHP: $user = getUser() — assignment_expression with variable_name left and function_call_expression right - // object_creation_expression ($user = new User()) is handled by extractInitializer. - // Explicit typed properties (private UserRepo $repo) are handled by extractDeclaration. - // PHP variable names include the $ sigil — kept as-is to match what extractVarName stores in the env. - [SupportedLanguages.PHP]: (node) => { - if (node.type !== 'assignment_expression') return undefined; - const left = node.childForFieldName('left'); - const right = node.childForFieldName('right'); - if (!left || !right) return undefined; - if (left.type !== 'variable_name') return undefined; - // Skip object_creation_expression (new User()) — handled by extractInitializer - if (right.type === 'object_creation_expression') return undefined; - if (right.type !== 'function_call_expression') return undefined; - const func = right.childForFieldName('function') ?? right.firstNamedChild; - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - // Keep the $ sigil — PHP env keys are stored with $ (e.g. "$user") by extractVarName - const varName = left.text; - if (!varName) return undefined; - return { varName, calleeName }; - }, - - // Java: var user = getUser() — local_variable_declaration with `var` type and method_invocation value - // Explicit types (User user = getUser()) are handled by extractDeclaration. - // object_creation_expression (new User()) is handled by extractJavaInitializer. - [SupportedLanguages.Java]: (node) => { - if (node.type !== 'local_variable_declaration') return undefined; - const typeNode = node.childForFieldName('type'); - if (!typeNode) return undefined; - // Only handle `var` — explicit types are handled by extractDeclaration - if (typeNode.text !== 'var') return undefined; - const declarator = node.namedChildren.find((c: any) => c.type === 'variable_declarator'); - if (!declarator) return undefined; - const nameNode = declarator.childForFieldName('name'); - const value = declarator.childForFieldName('value'); - if (!nameNode || !value) return undefined; - // Skip object_creation_expression (new User()) — handled by extractInitializer - if (value.type === 'object_creation_expression') return undefined; - if (value.type !== 'method_invocation') return undefined; - const methodName = value.childForFieldName('name'); - if (!methodName) return undefined; - return { varName: nameNode.text, calleeName: methodName.text }; - }, - - // C#: var user = GetUser() — variable_declaration with implicit_type and invocation_expression value. - // Explicit types (User user = GetUser()) are handled by extractDeclaration. - // object_creation_expression (new User()) is handled by extractInitializer. - [SupportedLanguages.CSharp]: (node) => { - if (node.type !== 'variable_declaration') return undefined; - const typeNode = node.childForFieldName('type'); - // Only handle implicit_type (var) — explicit types handled by extractDeclaration - if (!typeNode || typeNode.type !== 'implicit_type') return undefined; - // Find first variable_declarator child - let declarator: any = null; - for (let i = 0; i < node.namedChildCount; i++) { - const child = node.namedChild(i); - if (child?.type === 'variable_declarator') { declarator = child; break; } - } - if (!declarator) return undefined; - const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild; - if (!nameNode || nameNode.type !== 'identifier') return undefined; - // Find equals_value_clause - let eqClause: any = null; - for (let i = 0; i < declarator.namedChildCount; i++) { - const child = declarator.namedChild(i); - if (child?.type === 'equals_value_clause') { eqClause = child; break; } - } - if (!eqClause) return undefined; - const value = eqClause.firstNamedChild; - if (!value) return undefined; - // Skip object_creation_expression (new User()) — handled by extractInitializer - if (value.type === 'object_creation_expression') return undefined; - if (value.type !== 'invocation_expression') return undefined; - const func = value.firstNamedChild; - if (!func) return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: nameNode.text, calleeName }; - }, - - // Go: user := GetUser("alice") — short_var_declaration with single call_expression on the right. - // Multi-return (`user, err := GetUser()`) is intentionally skipped. - // new() and make() are already handled by extractDeclaration in go.ts. - [SupportedLanguages.Go]: (node) => { - if (node.type !== 'short_var_declaration') return undefined; - const left = node.childForFieldName('left'); - const right = node.childForFieldName('right'); - if (!left || !right) return undefined; - // Single assignment only — skip multi-return like `user, err := GetUser()` - const leftIds = left.type === 'expression_list' ? left.namedChildren : [left]; - if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined; - const rightExprs = right.type === 'expression_list' ? right.namedChildren : [right]; - if (rightExprs.length !== 1 || rightExprs[0].type !== 'call_expression') return undefined; - const func = rightExprs[0].childForFieldName('function'); - if (!func) return undefined; - // Skip new() and make() — already handled by extractDeclaration - if (func.text === 'new' || func.text === 'make') return undefined; - const calleeName = extractSimpleTypeName(func); - if (!calleeName) return undefined; - return { varName: leftIds[0].text, calleeName }; - }, -}; diff --git a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts index ede01b280..4113b0eac 100644 --- a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts @@ -1,5 +1,5 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; import { extractSimpleTypeName, extractVarName } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ @@ -126,9 +126,44 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'declaration') return undefined; + const typeNode = node.childForFieldName('type'); + if (!typeNode) return undefined; + const typeText = typeNode.text; + if (typeText !== 'auto' && typeText !== 'decltype(auto)' && typeNode.type !== 'placeholder_type_specifier') return undefined; + const declarator = node.childForFieldName('declarator'); + if (!declarator || declarator.type !== 'init_declarator') return undefined; + const value = declarator.childForFieldName('value'); + if (!value || value.type !== 'call_expression') return undefined; + const func = value.childForFieldName('function'); + if (!func) return undefined; + if (func.type === 'qualified_identifier' || func.type === 'scoped_identifier') { + const last = func.lastNamedChild; + if (!last) return undefined; + const nameNode = declarator.childForFieldName('declarator'); + if (!nameNode) return undefined; + const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator' + ? nameNode.firstNamedChild : nameNode; + if (!finalName) return undefined; + return { varName: finalName.text, calleeName: last.text }; + } + if (func.type !== 'identifier') return undefined; + const nameNode = declarator.childForFieldName('declarator'); + if (!nameNode) return undefined; + const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator' + ? nameNode.firstNamedChild : nameNode; + if (!finalName) return undefined; + const varName = finalName.text; + if (!varName) return undefined; + return { varName, calleeName: func.text }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, extractInitializer, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/csharp.ts b/gitnexus/src/core/ingestion/type-extractors/csharp.ts index af5f09345..906e644e5 100644 --- a/gitnexus/src/core/ingestion/type-extractors/csharp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/csharp.ts @@ -1,5 +1,5 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js'; +import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js'; import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ @@ -103,8 +103,43 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'variable_declaration') return undefined; + const typeNode = node.childForFieldName('type'); + // Only handle implicit_type (var) — explicit types handled by extractDeclaration + if (!typeNode || typeNode.type !== 'implicit_type') return undefined; + // Find first variable_declarator child + let declarator: SyntaxNode | null = null; + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (child?.type === 'variable_declarator') { declarator = child; break; } + } + if (!declarator) return undefined; + const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild; + if (!nameNode || nameNode.type !== 'identifier') return undefined; + // Find equals_value_clause + let eqClause: SyntaxNode | null = null; + for (let i = 0; i < declarator.namedChildCount; i++) { + const child = declarator.namedChild(i); + if (child?.type === 'equals_value_clause') { eqClause = child; break; } + } + if (!eqClause) return undefined; + const value = eqClause.firstNamedChild; + if (!value) return undefined; + // Skip object_creation_expression (new User()) — handled by extractInitializer + if (value.type === 'object_creation_expression') return undefined; + if (value.type !== 'invocation_expression') return undefined; + const func = value.firstNamedChild; + if (!func) return undefined; + const calleeName = extractSimpleTypeName(func); + if (!calleeName) return undefined; + return { varName: nameNode.text, calleeName }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/go.ts b/gitnexus/src/core/ingestion/type-extractors/go.ts index 72090ab67..0b3456ced 100644 --- a/gitnexus/src/core/ingestion/type-extractors/go.ts +++ b/gitnexus/src/core/ingestion/type-extractors/go.ts @@ -1,5 +1,5 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js'; +import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js'; import { extractSimpleTypeName, extractVarName } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ @@ -141,8 +141,29 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'short_var_declaration') return undefined; + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (!left || !right) return undefined; + // Single assignment only — skip multi-return like `user, err := GetUser()` + const leftIds = left.type === 'expression_list' ? left.namedChildren : [left]; + if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined; + const rightExprs = right.type === 'expression_list' ? right.namedChildren : [right]; + if (rightExprs.length !== 1 || rightExprs[0].type !== 'call_expression') return undefined; + const func = rightExprs[0].childForFieldName('function'); + if (!func) return undefined; + // Skip new() and make() — already handled by extractDeclaration + if (func.text === 'new' || func.text === 'make') return undefined; + const calleeName = extractSimpleTypeName(func); + if (!calleeName) return undefined; + return { varName: leftIds[0].text, calleeName }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/index.ts b/gitnexus/src/core/ingestion/type-extractors/index.ts index 551931bf9..98f62bdf7 100644 --- a/gitnexus/src/core/ingestion/type-extractors/index.ts +++ b/gitnexus/src/core/ingestion/type-extractors/index.ts @@ -33,7 +33,7 @@ export const typeConfigs = { [SupportedLanguages.Ruby]: rubyConfig, } satisfies Record; -export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor } from './types.js'; +export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor, ConstructorBindingScanner } from './types.js'; export { TYPED_PARAMETER_TYPES, extractSimpleTypeName, diff --git a/gitnexus/src/core/ingestion/type-extractors/jvm.ts b/gitnexus/src/core/ingestion/type-extractors/jvm.ts index bc8281ca6..978eba0f4 100644 --- a/gitnexus/src/core/ingestion/type-extractors/jvm.ts +++ b/gitnexus/src/core/ingestion/type-extractors/jvm.ts @@ -1,6 +1,6 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; -import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; +import { extractSimpleTypeName, extractVarName, extractCalleeName, findChildByType } from './shared.js'; // ── Java ────────────────────────────────────────────────────────────────── @@ -67,11 +67,30 @@ const extractJavaParameter: ParameterExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'local_variable_declaration') return undefined; + const typeNode = node.childForFieldName('type'); + if (!typeNode) return undefined; + if (typeNode.text !== 'var') return undefined; + const declarator = node.namedChildren.find((c: any) => c.type === 'variable_declarator'); + if (!declarator) return undefined; + const nameNode = declarator.childForFieldName('name'); + const value = declarator.childForFieldName('value'); + if (!nameNode || !value) return undefined; + if (value.type === 'object_creation_expression') return undefined; + if (value.type !== 'method_invocation') return undefined; + const methodName = value.childForFieldName('name'); + if (!methodName) return undefined; + return { varName: nameNode.text, calleeName: methodName.text }; +}; + export const javaTypeConfig: LanguageTypeConfig = { declarationNodeTypes: JAVA_DECLARATION_NODE_TYPES, extractDeclaration: extractJavaDeclaration, extractParameter: extractJavaParameter, extractInitializer: extractJavaInitializer, + scanConstructorBinding: scanJavaConstructorBinding, }; // ── Kotlin ──────────────────────────────────────────────────────────────── @@ -166,9 +185,25 @@ const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: M if (varName) env.set(varName, calleeName); }; +/** Kotlin: val x = User(...) — constructor binding for property_declaration with call_expression */ +const scanKotlinConstructorBinding: ConstructorBindingScanner = (node) => { + if (node.type !== 'property_declaration') return undefined; + const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration'); + if (!varDecl) return undefined; + if (varDecl.namedChildren.some(c => c.type === 'user_type')) return undefined; + const callExpr = node.namedChildren.find(c => c.type === 'call_expression'); + if (!callExpr) return undefined; + const callee = callExpr.firstNamedChild; + if (!callee || callee.type !== 'simple_identifier') return undefined; + const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier'); + if (!nameNode) return undefined; + return { varName: nameNode.text, calleeName: callee.text }; +}; + export const kotlinTypeConfig: LanguageTypeConfig = { declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES, extractDeclaration: extractKotlinDeclaration, extractParameter: extractKotlinParameter, extractInitializer: extractKotlinInitializer, + scanConstructorBinding: scanKotlinConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/php.ts b/gitnexus/src/core/ingestion/type-extractors/php.ts index d07ce1706..000f024ef 100644 --- a/gitnexus/src/core/ingestion/type-extractors/php.ts +++ b/gitnexus/src/core/ingestion/type-extractors/php.ts @@ -1,6 +1,6 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; -import { extractSimpleTypeName, extractVarName } from './shared.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; +import { extractSimpleTypeName, extractVarName, extractCalleeName } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ 'assignment_expression', // For constructor inference: $x = new User() @@ -110,9 +110,28 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'assignment_expression') return undefined; + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (!left || !right) return undefined; + if (left.type !== 'variable_name') return undefined; + // Skip object_creation_expression (new User()) — handled by extractInitializer + if (right.type === 'object_creation_expression') return undefined; + if (right.type !== 'function_call_expression') return undefined; + const calleeName = extractCalleeName(right); + if (!calleeName) return undefined; + // Keep the $ sigil — PHP env keys are stored with $ (e.g. "$user") by extractVarName + const varName = left.text; + if (!varName) return undefined; + return { varName, calleeName }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, extractInitializer, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/python.ts b/gitnexus/src/core/ingestion/type-extractors/python.ts index 55e399bf4..e44c3f715 100644 --- a/gitnexus/src/core/ingestion/type-extractors/python.ts +++ b/gitnexus/src/core/ingestion/type-extractors/python.ts @@ -1,5 +1,5 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; import { extractSimpleTypeName, extractVarName } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ @@ -75,9 +75,37 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map { + let left: SyntaxNode | null; + let right: SyntaxNode | null; + + if (node.type === 'named_expression') { + left = node.childForFieldName('name'); + right = node.childForFieldName('value'); + } else if (node.type === 'assignment') { + left = node.childForFieldName('left'); + right = node.childForFieldName('right'); + if (node.childForFieldName('type')) return undefined; + } else { + return undefined; + } + + if (!left || !right) return undefined; + if (left.type !== 'identifier') return undefined; + if (right.type !== 'call') return undefined; + const func = right.childForFieldName('function'); + if (!func) return undefined; + const calleeName = extractSimpleTypeName(func); + if (!calleeName) return undefined; + return { varName: left.text, calleeName }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, extractInitializer, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/ruby.ts b/gitnexus/src/core/ingestion/type-extractors/ruby.ts index 874d80667..be4a45a56 100644 --- a/gitnexus/src/core/ingestion/type-extractors/ruby.ts +++ b/gitnexus/src/core/ingestion/type-extractors/ruby.ts @@ -182,4 +182,5 @@ export const typeConfig: LanguageTypeConfig = { extractDeclaration, extractParameter, extractInitializer, + scanConstructorBinding: extractRubyConstructorAssignment, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/rust.ts b/gitnexus/src/core/ingestion/type-extractors/rust.ts index 7380037a7..3edb46495 100644 --- a/gitnexus/src/core/ingestion/type-extractors/rust.ts +++ b/gitnexus/src/core/ingestion/type-extractors/rust.ts @@ -1,6 +1,6 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; -import { extractSimpleTypeName, extractVarName } from './shared.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; +import { extractSimpleTypeName, extractVarName, hasTypeAnnotation } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ 'let_declaration', @@ -152,9 +152,38 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'let_declaration') return undefined; + if (hasTypeAnnotation(node)) return undefined; + let patternNode = node.childForFieldName('pattern'); + if (!patternNode) return undefined; + if (patternNode.type === 'mut_pattern') { + patternNode = patternNode.firstNamedChild; + if (!patternNode) return undefined; + } + if (patternNode.type !== 'identifier') return undefined; + const value = node.childForFieldName('value'); + if (!value || value.type !== 'call_expression') return undefined; + const func = value.childForFieldName('function'); + if (!func) return undefined; + if (func.type === 'scoped_identifier') { + const methodName = func.lastNamedChild; + if (methodName?.text === 'new') return undefined; + } + const calleeName = extractSimpleTypeName(func); + if (!calleeName) return undefined; + return { varName: patternNode.text, calleeName }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractInitializer, extractParameter, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/shared.ts b/gitnexus/src/core/ingestion/type-extractors/shared.ts index 398234fb7..83ba38b77 100644 --- a/gitnexus/src/core/ingestion/type-extractors/shared.ts +++ b/gitnexus/src/core/ingestion/type-extractors/shared.ts @@ -217,6 +217,38 @@ export const extractRubyConstructorAssignment = ( return { varName: left.text, calleeName }; }; +/** + * Check if an AST node has an explicit type annotation. + * Checks both named fields ('type') and child nodes ('type_annotation'). + * Used by constructor binding scanners to skip annotated declarations. + */ +export const hasTypeAnnotation = (node: SyntaxNode): boolean => { + if (node.childForFieldName('type')) return true; + for (let i = 0; i < node.childCount; i++) { + if (node.child(i)?.type === 'type_annotation') return true; + } + return false; +}; + +/** + * Unwrap an await_expression to get the inner value. + * Returns the node itself if not an await_expression, or null if input is null. + */ +export const unwrapAwait = (node: SyntaxNode | null): SyntaxNode | null => { + if (!node) return null; + return node.type === 'await_expression' ? node.firstNamedChild : node; +}; + +/** + * Extract the callee name from a call_expression node. + * Navigates to the 'function' field (or first named child) and extracts a simple type name. + */ +export const extractCalleeName = (callNode: SyntaxNode): string | undefined => { + const func = callNode.childForFieldName('function') ?? callNode.firstNamedChild; + if (!func) return undefined; + return extractSimpleTypeName(func); +}; + /** Find the first named child with the given node type */ export const findChildByType = (node: SyntaxNode, type: string): SyntaxNode | null => { for (let i = 0; i < node.namedChildCount; i++) { diff --git a/gitnexus/src/core/ingestion/type-extractors/swift.ts b/gitnexus/src/core/ingestion/type-extractors/swift.ts index 41f2f7818..1cd870681 100644 --- a/gitnexus/src/core/ingestion/type-extractors/swift.ts +++ b/gitnexus/src/core/ingestion/type-extractors/swift.ts @@ -1,6 +1,6 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; -import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; +import { extractSimpleTypeName, extractVarName, findChildByType, hasTypeAnnotation } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ 'property_declaration', @@ -77,9 +77,39 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'property_declaration') return undefined; + if (hasTypeAnnotation(node)) return undefined; + const pattern = node.childForFieldName('pattern'); + if (!pattern) return undefined; + const varName = pattern.text; + if (!varName) return undefined; + let callExpr: SyntaxNode | null = null; + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (child?.type === 'call_expression') { callExpr = child; break; } + } + if (!callExpr) return undefined; + const callee = callExpr.firstNamedChild; + if (!callee) return undefined; + if (callee.type === 'simple_identifier') { + return { varName, calleeName: callee.text }; + } + if (callee.type === 'navigation_expression') { + const receiver = callee.firstNamedChild; + const suffix = callee.lastNamedChild; + if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') { + return { varName, calleeName: receiver.text }; + } + } + return undefined; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, extractInitializer, + scanConstructorBinding, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/types.ts b/gitnexus/src/core/ingestion/type-extractors/types.ts index e874292cf..f5cd8daf9 100644 --- a/gitnexus/src/core/ingestion/type-extractors/types.ts +++ b/gitnexus/src/core/ingestion/type-extractors/types.ts @@ -13,6 +13,10 @@ export type ClassNameLookup = { has(name: string): boolean }; /** Extracts type bindings from a constructor-call initializer, with access to known class names */ export type InitializerExtractor = (node: SyntaxNode, env: Map, classNames: ClassNameLookup) => void; +/** Scans an AST node for untyped `var = callee()` patterns for return-type inference. + * Returns { varName, calleeName } if the node matches, undefined otherwise. */ +export type ConstructorBindingScanner = (node: SyntaxNode) => { varName: string; calleeName: string } | undefined; + /** Per-language type extraction configuration */ export interface LanguageTypeConfig { /** Node types that represent typed declarations for this language */ @@ -26,4 +30,8 @@ export interface LanguageTypeConfig { * Only for languages with syntactic constructor markers (new, composite_literal, ::new). * Receives classNames — the set of class/struct names visible in the current file's AST. */ extractInitializer?: InitializerExtractor; + /** Scan for untyped `var = callee()` assignments for return-type inference. + * Called on every AST node during buildTypeEnv walk; returns undefined for non-matches. + * The callee binding is unverified — the caller must confirm against the SymbolTable. */ + scanConstructorBinding?: ConstructorBindingScanner; } diff --git a/gitnexus/src/core/ingestion/type-extractors/typescript.ts b/gitnexus/src/core/ingestion/type-extractors/typescript.ts index 49e2f9723..8089d0965 100644 --- a/gitnexus/src/core/ingestion/type-extractors/typescript.ts +++ b/gitnexus/src/core/ingestion/type-extractors/typescript.ts @@ -1,6 +1,6 @@ import type { SyntaxNode } from '../utils.js'; -import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js'; -import { extractSimpleTypeName, extractVarName } from './shared.js'; +import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js'; +import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName } from './shared.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set([ 'lexical_declaration', @@ -65,9 +65,27 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map { + if (node.type !== 'variable_declarator') return undefined; + if (hasTypeAnnotation(node)) return undefined; + const nameNode = node.childForFieldName('name'); + if (!nameNode || nameNode.type !== 'identifier') return undefined; + const value = unwrapAwait(node.childForFieldName('value')); + if (!value || value.type !== 'call_expression') return undefined; + const calleeName = extractCalleeName(value); + if (!calleeName) return undefined; + return { varName: nameNode.text, calleeName }; +}; + export const typeConfig: LanguageTypeConfig = { declarationNodeTypes: DECLARATION_NODE_TYPES, extractDeclaration, extractParameter, extractInitializer, + scanConstructorBinding, };