mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
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:
parent
dbf37cd572
commit
4a99c7fe9a
16 changed files with 312 additions and 912 deletions
|
|
@ -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`
|
||||
|
|
@ -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`
|
||||
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -182,4 +182,5 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding: extractRubyConstructorAssignment,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue