refactor: merge CONSTRUCTOR_BINDING_SCANNERS into per-language LanguageTypeConfig

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.
This commit is contained in:
Gergo Magyar 2026-03-15 09:44:51 +00:00
parent dbf37cd572
commit 4a99c7fe9a
16 changed files with 312 additions and 912 deletions

View file

@ -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/<lang>-<pattern>/`
- Integration test: `test/integration/resolvers/<lang>.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> → 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<User>` → `List` (base only)
- **New**: `List<User>` → `{ 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/<lang>-<pattern>/
- 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/<lang>.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`

View file

@ -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<User>]`
- 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`

View file

@ -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<string>();
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<Record<SupportedLanguages, (node: SyntaxNode) => { 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 };
},
};

View file

@ -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<string> = new Set([
@ -126,9 +126,44 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** C/C++: auto x = User() where function is an identifier (not type_identifier) */
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
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,
};

View file

@ -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<string> = new Set([
@ -103,8 +103,43 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** C#: var x = SomeFactory(...) → bind x to SomeFactory (constructor-like call) */
const scanConstructorBinding: ConstructorBindingScanner = (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: 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,
};

View file

@ -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<string> = new Set([
@ -141,8 +141,29 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** Go: user := NewUser(...) — infer type from single-assignment call expression */
const scanConstructorBinding: ConstructorBindingScanner = (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 };
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
scanConstructorBinding,
};

View file

@ -33,7 +33,7 @@ export const typeConfigs = {
[SupportedLanguages.Ruby]: rubyConfig,
} satisfies Record<SupportedLanguages, LanguageTypeConfig>;
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor } from './types.js';
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor, ConstructorBindingScanner } from './types.js';
export {
TYPED_PARAMETER_TYPES,
extractSimpleTypeName,

View file

@ -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<str
if (varName && typeName) env.set(varName, typeName);
};
/** Java: var x = SomeFactory.create() — constructor binding for `var` with method_invocation */
const scanJavaConstructorBinding: ConstructorBindingScanner = (node) => {
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,
};

View file

@ -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<string> = new Set([
'assignment_expression', // For constructor inference: $x = new User()
@ -110,9 +110,28 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** PHP: $x = SomeFactory() — bind variable to factory/static call return type */
const scanConstructorBinding: ConstructorBindingScanner = (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 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,
};

View file

@ -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<string> = new Set([
@ -75,9 +75,37 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
}
};
/** Python: user = User("alice") — scan assignment/walrus for constructor-like calls.
* Returns {varName, calleeName} without checking classNames (caller validates). */
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
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,
};

View file

@ -182,4 +182,5 @@ export const typeConfig: LanguageTypeConfig = {
extractDeclaration,
extractParameter,
extractInitializer,
scanConstructorBinding: extractRubyConstructorAssignment,
};

View file

@ -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<string> = new Set([
'let_declaration',
@ -152,9 +152,38 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** 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.
*/
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
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,
};

View file

@ -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++) {

View file

@ -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<string> = new Set([
'property_declaration',
@ -77,9 +77,39 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
}
};
/** Swift: let user = User(name: "alice") — scan property_declaration for constructor binding */
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
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,
};

View file

@ -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<string, string>, 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;
}

View file

@ -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<string> = new Set([
'lexical_declaration',
@ -65,9 +65,27 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
}
};
/**
* 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 scanConstructorBinding: ConstructorBindingScanner = (node) => {
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,
};