Skip to content

feat: TypeEnvironment API with constructor inference, self/this/super resolution#274

Merged
magyargergo merged 18 commits into
mainfrom
feat/type-resolution-constructor-inference
Mar 14, 2026
Merged

feat: TypeEnvironment API with constructor inference, self/this/super resolution#274
magyargergo merged 18 commits into
mainfrom
feat/type-resolution-constructor-inference

Conversation

@magyargergo

@magyargergo magyargergo commented Mar 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • TypeEnvironment API: buildTypeEnv() returns a TypeEnvironment with .lookup() method, replacing the loose lookupTypeEnv(env, ...) function + raw Map. Single-pass AST walk merges type binding extraction and constructor binding scanning into one traversal.
  • Constructor-call type inference (Tier 1 fallback): when a declaration has no explicit type annotation, infer the type from constructor-call syntax (new User(), User(), User.new, User::new())
  • Cross-file constructor verification via SymbolTable: languages like Kotlin, Python, Swift, C++ where constructors look like function calls verify against the project-wide symbol table
  • Self/this/$this resolution: resolves self/this/$this to the enclosing class name via AST walk for receiver-type filtering
  • Super/base/parent resolution: resolves super/base/parent to the parent class name by extracting heritage from 8 tree-sitter grammar variants (TS/JS, Java, Python, Ruby, C#, PHP, Kotlin, C++, Swift)
  • D-phase widening: resolveCallTarget widens to global candidates for super calls where same-file tier gives the wrong method
  • ResolutionContext refactor: unified tiered name resolution replacing duplicated logic in symbol-resolver.ts and call-processor.ts
  • ClassNameLookup type: replaces over-broad ReadonlySet<string> facade with minimal { has(name: string): boolean } interface + memoization

TypeEnvironment API

// Before (clunky)
const { env, constructorBindings } = buildTypeEnv(tree, lang);
const type = lookupTypeEnv(env, receiverName, callNode);

// After (clean)
const typeEnv = buildTypeEnv(tree, language);
const type = typeEnv.lookup(receiverName, callNode);
typeEnv.constructorBindings; // cross-file bindings

Constructor inference — languages covered

Language Constructor Pattern Example
TypeScript/JS new_expression const x = new User()
Java 10+ var + object_creation_expression var x = new User()
Rust ::new() / ::default() let x = User::new()
PHP object_creation_expression $x = new User()
C++ auto + new_expression or class-verified call auto x = User()
Kotlin val + call_expression (SymbolTable-verified) val x = User()
Python assignment + call (SymbolTable-verified) user = User("alice")
Swift property_declaration + call_expression let x = User(name: "a")
Ruby assignment + call with .new receiver user = User.new

Receiver keyword resolution

Keyword Languages Resolves to
this TS, JS, Java, C#, C++, Kotlin Enclosing class name
self Python, Rust, Ruby, Swift Enclosing class name
$this PHP Enclosing class name
super TS, JS, Java, Kotlin, Ruby Parent class name
base C# Parent class name
parent PHP Parent class name

Key design decisions

  • Wrong edge worse than no edge: C++ uses classNames.has() to verify identifiers are known classes, not a PascalCase heuristic
  • Tier 0 > Tier 1: explicit annotations always take precedence over constructor inference
  • Cross-file verification: unresolved constructor bindings from parse-worker are verified against the SymbolTable in call-processor
  • Scope-aware constructor bindings: keyed by funcName\0varName to prevent collisions between same-named locals in different functions
  • D-phase uses symbols.lookupFuzzy() directly instead of undocumented empty-string convention for global resolution

Test plan

  • 1193 unit tests passing (69 type-env, 21 call-processor, 1103 others)
  • Integration tests for self/this resolution across 12 languages
  • Integration tests for parent class (EXTENDS/IMPLEMENTS) resolution across 12 languages
  • Integration tests for super/base/parent keyword resolution across 10 languages
  • Integration tests for constructor-inferred type resolution across 9 languages
  • ~60 per-language fixture directories for all resolution scenarios
  • TypeScript build clean (tsc --noEmit)

Post-Deploy Monitoring & Validation

No additional operational monitoring required: purely in-worker type inference enrichment with no API, schema, or runtime changes.

Add extractInitializer as a Tier 1 fallback in buildTypeEnv: when a
declaration node has no explicit type annotation, infer the type from
constructor-call patterns (new X(), X::new(), X::default(), $x = new X()).

Languages covered: TypeScript/JS, Java (var), Rust, PHP, C++ (auto).
Python/Kotlin/Swift deferred — need symbol-table access to distinguish
class constructors from function calls.

Adds 20 new unit tests covering constructor inference, annotation
precedence, and known limitations across all supported languages.
- Add collectClassNames pre-scan: walks AST to build Set<string> of
  class/struct names defined in the file
- C++ extractInitializer uses classNames.has() to verify identifier is
  a known class before inferring (auto x = User() resolves, auto x =
  getUser() does not — no false positives)
- Add InitializerExtractor type that receives classNames parameter
- Fix env.size gating: always call extractInitializer when available,
  so mixed declarators like const a: A = x, b = new B() resolve both
- Add env.has() guard in Java extractInitializer to skip already-bound vars
- Document Rust new/default whitelist rationale
- Pin all test assertions, add mixed multi-declarator test case
@vercel

vercel Bot commented Mar 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Mar 14, 2026 7:03pm

Request Review

@magyargergo

Copy link
Copy Markdown
Collaborator Author

@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution.

@claude

claude Bot commented Mar 13, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 3m 42s —— View job


Constructor Type Resolution — Gap Analysis

  • Gather context and read changed files
  • Analyze type-env.ts for edge cases
  • Analyze individual language extractors
  • Analyze types.ts interface
  • Compile findings and post review

Good work overall. Here are the uncovered patterns and bugs I found, roughly ordered by severity:


🔴 Bug: Rust extractInitializer overwrites Tier 0 annotation

File: gitnexus/src/core/ingestion/type-extractors/rust.ts:20-37

extractInitializer for Rust has no guard against overwriting a binding already placed by extractDeclaration. For let user: BaseUser = User::new():

  1. extractDeclarationuser → BaseUser
  2. extractInitializer runs, sees ::new(), writes user → Useroverwrites Tier 0

The test prefers explicit annotation over constructor inference passes only because it uses the same class name for both annotation and constructor (let user: User = User::new()), masking the bug.

Java correctly guards with if (!varName || env.has(varName)) continue;. TypeScript guards by checking declarator.childForFieldName('type') !== null. Rust has neither.

The fix: add if (node.childForFieldName('type') !== null) return; as the first line of extractInitializer in rust.ts.

Fix this →


🔴 Bug: Rust let mut x = User::new() is never inferred

File: gitnexus/src/core/ingestion/type-extractors/rust.ts:35 and gitnexus/src/core/ingestion/type-extractors/shared.ts:73-83

In tree-sitter-rust, let mut x = ... produces a let_declaration where the pattern field is a mut_pattern node (not a plain identifier). extractVarName in shared.ts only handles identifier, simple_identifier, variable_name, name, and variable_declarator — so mut_pattern returns undefined.

Result: all let mut bindings are silently dropped from Rust inference, even for explicit annotations (let mut x: User = ...). Since let mut is the majority of mutable Rust variables, this is a very common miss.

A fix would be to add mut_pattern handling to extractVarName (delegating to its inner identifier child), or handle it in the Rust-specific extractor.

Fix this →


🟠 Gap: C++ direct construction from external/forward-declared classes

File: gitnexus/src/core/ingestion/type-extractors/c-cpp.ts:82-92

collectClassNames (in type-env.ts:66) only scans the current file's AST. Forward-declared or #include-d classes are invisible. This means:

// declared in another header, only forward-declared here:
class HttpClient;

void run() {
  auto client = HttpClient(); // ← HttpClient not in classNames → no inference
  auto client2 = new HttpClient(); // ← works, uses new_expression path
}

The new path works correctly because it reads the type from the new_expression node directly. The direct-construction path (HttpClient()) requires the name to be in classNames. So auto + direct construction from cross-file types is silently dropped. This is an architectural constraint, but worth documenting explicitly as a known limitation.


🟠 Gap: C++ brace-initialization not inferred

File: gitnexus/src/core/ingestion/type-extractors/c-cpp.ts:36-93

auto user = User{};
auto user = User{field1, field2};

These are parsed as initializer_list or aggregate initialization — neither new_expression nor call_expression. The extractor covers only new_expression (heap) and call_expression (stack via function-call syntax), so brace-init is completely missed. Brace-init is idiomatic modern C++ and used heavily in codebases that follow the C++11/14/17 style.


🟠 Gap: TypeScript new_expression wrapped in as or !

File: gitnexus/src/core/ingestion/type-extractors/typescript.ts:53

const user = new User() as IUser;   // value is as_expression, not new_expression
const user = new User()!;            // value is non_null_expression, not new_expression

Both wrappings make valueNode.type !== 'new_expression' true, so inference is skipped. The as case is interesting because the correct answer would arguably be IUser (the cast target), not User. Non-null assertions are common in initialized class members.

Fix this →


🟡 Gap: PHP new self() / new static() / new parent()

File: gitnexus/src/core/ingestion/type-extractors/php.ts:23-27

$x = new self();
$x = new static();
$x = new parent();

These will be inferred as the literal type names 'self', 'static', 'parent'. These are PHP late-static-binding keywords — the actual type is the enclosing class, which isn't available in the per-file extractor. The env will contain a spurious mapping that will never match a real class definition, possibly causing false-positive method edges.

A simple guard: skip inference if ctorType.text matches self, static, or parent.

Fix this →


🟡 Gap: C++ multiple auto declarators in one statement

File: gitnexus/src/core/ingestion/type-extractors/c-cpp.ts:48

extractDeclaration and extractInitializer both use node.childForFieldName('declarator') which returns only the first declarator. A statement like:

auto x = new User(), y = new Admin(); // C++ does not allow auto here

is actually not valid C++ (you can't mix auto with multiple declarators of different types), but int* x = new int, y = new int with the same type is valid C++. In that case, extractDeclaration already handles it via the first declarator only. However, if the grammar emits multiple declarator fields (some grammars use a list), the second would be silently dropped.


🟡 Gap: Rust Box::new(User::new()) — nested constructors

File: gitnexus/src/core/ingestion/type-extractors/rust.ts:20-37

let user = Box::new(User::new());

The outer ::new() call is Box::new(...), so pathField.text resolves to Box, not User. The inferred type would be Box, which is almost certainly not what's useful for method resolution (you'd want User). This is a fundamental limitation of single-level AST inspection without type-checking, but it's a very common Rust pattern (Box, Arc, Rc, Mutex).


🟡 Gap: Java var with generic constructor new HashMap<>()

File: gitnexus/src/core/ingestion/type-extractors/jvm.ts:32-48

var map = new HashMap<String, User>();

extractSimpleTypeName on a generic_type node strips to the base (HashMap). That's correct for Java. However:

var users = new ArrayList<>(existingList); // copy constructor

The type would be inferred as ArrayList. Acceptable, but callers should know this strips generics.


Comment mismatch in typescript.ts

File: gitnexus/src/core/ingestion/type-extractors/typescript.ts:44

The comment /** TypeScript: const x = new User(), let x = new ns.Service() */ implies new ns.Service() is handled, but the test at line 431–437 explicitly documents it is not handled (returns undefined). The comment should be updated to avoid misleading future readers.


Summary table

Issue Severity Language Pattern Effect
Rust: Tier 1 overwrites Tier 0 🔴 Bug Rust let x: Base = Derived::new() Wrong type inferred
let mut not inferred 🔴 Bug Rust let mut x = User::new() Missed inference
new X() as T / new X()! 🟠 Gap TypeScript/JS wrapped new_expression Missed inference
Brace-init User{} 🟠 Gap C++ auto x = User{} Missed inference
Cross-file direct construction 🟠 Gap C++ auto x = ExternalClass() Missed inference
new self() / new static() 🟡 Gap PHP $x = new self() Spurious type edge
Box::new(User::new()) 🟡 Gap Rust nested constructors Wrong type (Box instead of User)
Comment mismatch 🟡 Docs TypeScript ns.Service() comment Misleading

@github-actions

github-actions Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Unit Tests success 3 platforms
✅ Integration success 3 OS x 4 groups = 12 jobs

Test Results

Suite Tests Passed Failed Skipped Duration
Unit 1255 1232 0 0 6s
Integration 708 697 0 11 48s
Total 1963 1929 0 11 54s

✅ All 1929 tests passed

11 test(s) skipped — expand for details

Integration:

  • Swift constructor-inferred type resolution > detects User and Repo classes, both with save methods
  • Swift constructor-inferred type resolution > resolves user.save() to Models/User.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > resolves repo.save() to Models/Repo.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > emits exactly 2 save() CALLS edges (one per receiver type)
  • Swift self resolution > detects User and Repo classes, each with a save function
  • Swift self resolution > resolves self.save() inside User.process to User.save, not Repo.save
  • Swift parent resolution > detects BaseModel and User classes plus Serializable protocol
  • Swift parent resolution > emits EXTENDS edge: User → BaseModel
  • Swift parent resolution > emits IMPLEMENTS edge: User → Serializable (protocol conformance)
  • Swift cross-file User.init() inference > resolves user.save() via User.init(name:) inference
  • Swift cross-file User.init() inference > resolves user.greet() via User.init(name:) inference

Code Coverage

Combined (Unit + Integration)

Metric Coverage Covered Base Delta Status
Statements 50.22% 4289/8540 37.29% 📈 +12.9 🟢 ██████████░░░░░░░░░░
Branches 42.85% 2598/6063 32.1% 📈 +10.8 🟢 ████████░░░░░░░░░░░░
Functions 52.71% 456/865 38.74% 📈 +14.0 🟢 ██████████░░░░░░░░░░
Lines 51.8% 3939/7603 38.64% 📈 +13.2 🟢 ██████████░░░░░░░░░░
Coverage breakdown by test suite

Unit Tests

Metric Coverage Covered Base Delta Status
Statements 40.17% 3431/8540 37.29% 📈 +2.9 🟢 ████████░░░░░░░░░░░░
Branches 35.13% 2130/6063 32.1% 📈 +3.0 🟢 ███████░░░░░░░░░░░░░
Functions 41.5% 359/865 38.74% 📈 +2.8 🟢 ████████░░░░░░░░░░░░
Lines 41.53% 3158/7603 38.64% 📈 +2.9 🟢 ████████░░░░░░░░░░░░

Integration Tests

Metric Coverage Covered Base Delta Status
Statements 23.29% 1989/8540 37.29% 📉 -14.0 🔴 ████░░░░░░░░░░░░░░░░
Branches 17.86% 1083/6063 32.1% 📉 -14.2 🔴 ███░░░░░░░░░░░░░░░░░
Functions 26.24% 227/865 38.74% 📉 -12.5 🔴 █████░░░░░░░░░░░░░░░
Lines 24.2% 1840/7603 38.64% 📉 -14.4 🔴 ████░░░░░░░░░░░░░░░░

📋 View full run · Generated by CI

- Rust: Self::new()/Self::default() resolves to enclosing impl type
- PHP: new self()/static() resolves to enclosing class, parent() to superclass
- Rust: Tier 0 annotation guard prevents overwrite by constructor inference
- Rust: mut_pattern handling in extractVarName for let mut bindings
- TS: fix misleading comment in extractInitializer
- 58 tests passing (3 new Self/self resolution tests)
Refactors buildTypeEnv to use closures instead of passing mutable state
as parameters. classNames, env, and config are captured by the inner
walk and extractTypeBinding functions — no parameter mutation.

- Eliminates separate collectClassNames pre-scan (O(2n) → O(n))
- config looked up once per file instead of per-node
- 29 fewer lines
Add cross-file constructor type inference to the ingestion pipeline,
enabling receiver-type disambiguation for member calls like
`user.save()` when the variable is assigned from a constructor without
explicit type annotations.

Pipeline changes:
- Add extractInitializer to Python and Swift type extractors
- Add CONSTRUCTOR_BINDING_SCANNERS for Python, Swift, C/C++ in type-env
- Wire constructorBindings through parse-worker → parsing-processor →
  pipeline → processCallsFromExtracted
- Rewrite resolveCallTarget receiver-type filtering (step D) to use
  tiered import resolution (same-file → import-scoped → global) before
  falling back to fuzzy ownerId matching
- Use collectTieredCandidates for constructor binding verification
  instead of raw lookupFuzzy

Bug fixes:
- Fix C++ inline method query: @definition.method was captured on
  field_declaration_list instead of function_definition, causing wrong
  parameterCount for all inline class methods
- Fix parse-worker accumulated/flush results missing constructorBindings

CI changes:
- Add swift.test.ts to ci-integration pipeline group and coverage job
- Update ci-report to fetch base branch (main) coverage for delta
  reporting instead of showing config thresholds
- Add per-suite timing breakdown table (unit/integration/total)
- Add expandable skipped test details section

Tests: 288 passed, 4 skipped (swift — macOS only) across 10 languages
- 36 new constructor-inferred integration tests (4 per language)
- 10 fixture directories with cross-file constructor patterns
- TypeScript, JavaScript, Java, Kotlin, Python, PHP, Rust, Go, C++, Swift
…ructor-inference

# Conflicts:
#	gitnexus/src/core/ingestion/call-processor.ts
Resolve conflicts:
- CI: include both swift and ruby resolver tests
- call-processor: keep FileConstructorBindings import + callRouters dispatch
- buildTypeEnv: keep symbolTable param + callRouter lookup
…pping

Add Ruby User.new constructor binding scanner to type-env, enabling
receiver-type disambiguation for member calls like user.save vs repo.save.
Add self/this → enclosing class resolution in lookupTypeEnv so self.method()
calls resolve to the correct class even when the method name is ambiguous.
…p passing

Introduce createResolutionContext() as the single resolution API for all
processors. Eliminates duplicated tier-selection logic, fixes heritage
namedImportMap bug, and adds per-file resolution caching.

- NEW resolution-context.ts: closure-factory with resolve(), per-file cache,
  TIER_CONFIDENCE constant, and shared ResolutionTier type
- DELETE symbol-resolver.ts: zero production importers, logic now in
  resolution-context.ts
- call-processor: all functions take ctx instead of 6 separate maps,
  collectTieredCandidates removed (ctx.resolve replaces it),
  D4 redundant re-resolve eliminated
- heritage-processor: takes ctx, resolveHeritageId helper extracts
  repeated 14-line fallback pattern, namedImportMap now included
- import-processor: takes ctx, dead createImportMap/createPackageMap/
  createNamedImportMap factories removed
- pipeline: creates single ctx, wires onProgress to all processors,
  logs cache hit rate in dev mode
- Tier renamed: unique-global → global (honest about returning all candidates)
- Tests migrated: 1178 unit + 84 integration passing
@magyargergo

Copy link
Copy Markdown
Collaborator Author

@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution. Also review your previous comment and see review what's changed and how it improved the resolution and what's the next steps are.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 5m 14s —— View job


Constructor Type Resolution — Comprehensive Gap Analysis (Round 2)

  • Read current state of all type extractor files
  • Compare with previous findings to see what was fixed
  • Analyze type-env.ts and lookupTypeEnv for self/this resolution gaps
  • Analyze call-processor.ts for cross-file verification gaps
  • Find new uncovered patterns and bugs
  • Compile findings and post review

What Was Fixed Since Last Review

The three highest-severity items from the previous analysis were addressed:

Previous finding Status How it was fixed
🔴 Rust: Tier 1 overwrites Tier 0 (let x: Base = Derived::new()) ✅ Fixed rust.ts:36if (node.childForFieldName('type') !== null) return; guard added
🔴 let mut x = User::new() never inferred ✅ Fixed shared.ts:83-87mut_pattern case added to extractVarName, recursing into the inner identifier
🟡 PHP new self() / new static() → spurious type ✅ Fixed + improved Rather than just skipping, php.ts:24-44 now resolves self/static → enclosing class name via AST walk, and parent → base class from base_clause

The ns.Service() comment mismatch is now documented explicitly as a "known limitation" in the unit test (type-env.test.ts:431-437). The remaining 4 items from last review (TS as/! wrapping, C++ brace-init, cross-file C++ direct construction, Box::new(User::new())) are still unaddressed architectural limitations.


New Bugs Found

🔴 Bug: PHP $this->method() never gets receiver-type disambiguation

File: gitnexus/src/core/ingestion/type-env.ts:45-47

lookupTypeEnv handles self and this specially:

if (varName === 'self' || varName === 'this') {
  return findEnclosingClassName(callNode);
}

But PHP's $this is parsed by tree-sitter-php as a variable_name node with text "$this" (the dollar sign is part of the text). extractReceiverName in utils.ts:786 accepts variable_name nodes and returns their full text including the $. So lookupTypeEnv(env, '$this', callNode) is called, which does not match the self/this check, falls through to a regular TypeEnv lookup for "$this", and finds nothing — because PHP's extractDeclaration is a no-op and no $this → ClassName entry is ever written.

Result: $this->method() in PHP will never be disambiguated, even when the enclosing class is unambiguous.

The fix: extend the guard to || varName === '$this' in lookupTypeEnv.

Fix this →


🟠 Bug: Scope collision in processCallsFromExtracted constructor bindings

File: gitnexus/src/core/ingestion/call-processor.ts:358-368

ConstructorBinding carries a scope field ("funcName@startIndex") that is silently dropped when building fileReceiverTypes:

for (const { filePath, bindings } of constructorBindings) {
  for (const { varName, calleeName } of bindings) {   // ← scope ignored
    // ...
    fileReceiverTypes.get(filePath)!.set(varName, calleeName);  // flat per-file map
  }
}

If two functions in the same file both declare a local user variable via cross-file constructor inference but pointing to different types:

fun processUser() {
  val user = User()       // → User  (scope: "processUser@12")
  user.save()
}
fun processRepo() {
  val user = Repo()       // → Repo  (scope: "processRepo@89")
  user.save()
}

The flat Map<varName, typeName> will contain user → Repo (or user → User, whichever was processed last). Both user.save() calls will then be resolved against the same type, producing a wrong CALLS edge for one of them.

The scope field exists on ConstructorBinding (from scanConstructorBindings) but is never used on the consumer side. The root cause is that fileReceiverTypes is Map<filePath, Map<varName, typeName>> — it needs a third dimension for scope. However, ExtractedCall.sourceId uses generateId('Function', 'filepath:name') format while ConstructorBinding.scope uses "funcName@startIndex" format — aligning these would require additional plumbing.

Fix this →


🟡 Gap: Kotlin object_declaration / companion_object missing from CLASS_CONTAINER_TYPES

File: gitnexus/src/core/ingestion/utils.ts:263-274, gitnexus/src/core/ingestion/type-env.ts:70-80

CLASS_CONTAINER_TYPES does not include object_declaration or companion_object. Both are indexed as Class symbols in the knowledge graph (they appear in tree-sitter-queries.ts:561-567) but are invisible to findEnclosingClassName:

object AppConfig {
  fun setup() {
    this.init()  // ← findEnclosingClassName won't find AppConfig; returns undefined
  }
}

class Repo {
  companion object {
    fun create(): Repo {
      this.build()  // ← same issue; companion_object not in CLASS_CONTAINER_TYPES
    }
  }
}

this inside Kotlin singleton objects and companion objects will not be resolved to the enclosing type, so this.method() calls from these containers will lose receiver-type disambiguation.

Fix this →


Remaining from Previous Review (Not Yet Fixed)

Issue Severity Pattern Effect
new X() as T / new X()! 🟠 Gap TypeScript Missed inference — value node is as_expression or non_null_expression, not new_expression
C++ brace-init User{} 🟠 Gap C++ Missed inference — parsed as initializer_list, neither new_expression nor call_expression
C++ cross-file direct construction 🟠 Gap C++ auto x = ExternalClass() where ExternalClass is in another file — classNames doesn't have it
Box::new(User::new()) 🟡 Gap Rust Outer ::new() resolves to Box, not User — common pattern for Arc, Mutex, Rc wrappers

Summary Table

Issue Severity Language Status
Rust: Tier 1 overwrites Tier 0 🔴 Bug Rust ✅ Fixed
let mut x not inferred 🔴 Bug Rust ✅ Fixed
PHP $this->method() never disambiguated 🔴 Bug PHP 🆕 New finding
Scope collision in cross-file constructor bindings 🟠 Bug Kotlin/Python/Swift/Ruby 🆕 New finding
new X() as T / new X()! 🟠 Gap TypeScript Carried over
C++ brace-init / cross-file direct construction 🟠 Gap C++ Carried over
PHP new self() resolved to spurious type 🟡 Gap PHP ✅ Fixed (enhanced to resolve)
Kotlin object_declaration missing from CLASS_CONTAINER_TYPES 🟡 Gap Kotlin 🆕 New finding
Box::new(User::new()) 🟡 Gap Rust Carried over

Next Steps

The highest-priority actionable item is the $this fix in lookupTypeEnv — it's a one-line guard and unblocks all PHP $this->method() disambiguation. The scope collision in processCallsFromExtracted is architecturally more complex but could be addressed by keying the receiver map on scope+varName and using the call's enclosing function scope for lookup. The Kotlin CLASS_CONTAINER_TYPES gap is a safe, low-risk addition.

…review fixes

Add cross-language receiver keyword resolution:
- self/this/$this → enclosing class name via AST walk
- super/base/parent → parent class name via heritage AST extraction
  (8 grammar variants: TS/JS, Java, Python, Ruby, C#, PHP, Kotlin, C++, Swift)
- D-phase widening in resolveCallTarget for super→parent method dispatch

Introduce TypeEnvironment API replacing loose TypeEnvResult + lookupTypeEnv:
- buildTypeEnv() returns TypeEnvironment with .lookup() method
- Single-pass AST walk merges constructor binding scan (was separate traversal)
- ClassNameLookup type replaces over-broad ReadonlySet<string> facade
- Memoized class name lookups to avoid redundant SymbolTable scans

Code review fixes (6 agents, 11 findings):
- Replace ctx.resolve(name, '') hack with direct symbols.lookupFuzzy()
- Extract scope key helpers (extractFuncNameFromScope, receiverKey)
- Simplify D-phase from 5 steps to 4 with deduped typeNodeIds
- Remove C from CONSTRUCTOR_BINDING_SCANNERS (YAGNI — C has no constructors)
- Cache Map reuse in ResolutionContext to reduce GC pressure
- Remove unused TieredCandidates import

Integration tests for self/this, parent, and super resolution across all
12 supported languages with per-language fixture directories.
@magyargergo magyargergo changed the title feat: constructor-call type inference for TypeEnv (Phase 1) feat: TypeEnvironment API with constructor inference, self/this/super resolution Mar 14, 2026
@magyargergo

Copy link
Copy Markdown
Collaborator Author

@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution. Also review your previous comment and see review what's changed and how it improved the resolution and what's the next steps are.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 4m 21s —— View job


Constructor Type Resolution — Gap Analysis (Round 3)

  • Review previous findings and gather context
  • Read current state of type-env.ts
  • Read current state of call-processor.ts and utils.ts
  • Read all type extractor files
  • Compile findings

What Was Fixed Since Round 2

All three high-severity items from the previous analysis were addressed:

Previous finding Status How it was fixed
🔴 PHP $this->method() never disambiguated ✅ Fixed type-env.ts:55 — `
🟠 Scope collision in processCallsFromExtracted ✅ Fixed call-processor.ts:363-388receiverKey(funcName, varName) composite key replaces flat map; lookup falls back to file-scope receiverKey('', varName)
🟡 Kotlin object_declaration/companion_object missing ✅ Fixed utils.ts:274-276, 294-295 — both types added to CLASS_CONTAINER_TYPES and CONTAINER_TYPE_TO_LABEL

New Bugs Found

🔴 Bug: super resolution broken for generic parent classes

File: gitnexus/src/core/ingestion/type-env.ts:128-165

extractParentClassFromNode uses .text directly on AST nodes that can be generic types. In every language where a parent class can be generic, this returns the full string including type parameters — which then fails to resolve against the symbol table:

TypeScript/JS (class_heritage case, line 158):

const typeNode = clause.firstNamedChild;  // could be `generic_type` node
if (typeNode) return typeNode.text;       // returns "Base<T>" instead of "Base"

Pattern: class User extends Base<T> { super.save(); }receiverTypeName = "Base<T>" → no match → CALLS edge dropped.

Java (superclass field, line 133-136):

const inner = superclassNode.childForFieldName('type')
  ?? superclassNode.firstNamedChild ?? superclassNode;
return inner.text;  // could return "Base<T>" if inner is generic_type

Pattern: class User extends Base<T> → same failure mode. Java generics like ArrayList<E>, HashMap<K,V> make this a common hit.

Python (superclasses field, line 142-143):

const first = superclassesNode.firstNamedChild;
if (first) return first.text;  // returns "models.Model" for attribute node

Pattern: class User(models.Model)first.text = "models.Model" → lookup for "models.Model" fails to match the Model class. Django codebases hit this constantly.

The fix for all three cases is to use extractSimpleTypeName(node) (already imported in the shared extractors but not imported in type-env.ts) with .text as a fallback. Requires adding an import from ./type-extractors/shared.js.

Fix this →%20class_heritage%20case%20(line%20158)%3A%20change%20%60return%20typeNode.text%60%20to%20%60return%20extractSimpleTypeName(typeNode)%20%3F%3F%20typeNode.text%60%20for%20TypeScript%2FJS%20generic%20extends%20like%20%60class%20User%20extends%20Base%3CT%3E%60.%202)%20superclass%20field%20case%20(line%20136)%3A%20change%20%60return%20inner.text%60%20to%20%60return%20extractSimpleTypeName(inner)%20%3F%3F%20inner.text%60%20for%20Java%20generic%20extends.%203)%20superclasses%20field%20case%20(line%20143)%3A%20change%20%60return%20first.text%60%20to%20%60return%20extractSimpleTypeName(first)%20%3F%3F%20first.text%60%20for%20Python%20qualified%20base%20class%20like%20%60models.Model%60.%20extractSimpleTypeName%20is%20in%20.%2Ftype-extractors%2Fshared.js%20and%20needs%20to%20be%20imported%20in%20type-env.ts.%20Branch%3A%20feat%2Ftype-resolution-constructor-inference&repo=abhigyanpatwari/GitNexus)


🟠 Gap: Overloaded method scope collision still possible

File: gitnexus/src/core/ingestion/call-processor.ts:353-354

The round 2 fix correctly introduced scope-keyed receiver maps to prevent cross-function collisions. But extractFuncNameFromScope strips the positional disambiguator:

const extractFuncNameFromScope = (scope: string): string =>
  scope.slice(0, scope.indexOf('@'));  // "processUser@12" → "processUser"

Two overloaded methods in the same class that share the same name but have different signatures will produce the same extracted function name:

fun processUser(type: String) {
  val user = User()       // scope: "processUser@12" → key: "processUser\0user"
  user.save()
}
fun processUser(id: Int) {
  val user = Repo()       // scope: "processUser@89" → key: "processUser\0user" ← collision
  user.commit()  // resolved as User.commit, not Repo.commit
}

This is partial — the fix is a major improvement over the previous flat map (prevents cross-function collisions for distinct function names), but same-name overloads still collide. A complete fix would require aligning scope's @startIndex with the source position in ExtractedCall.sourceId format, which would require additional plumbing in parse-worker.ts. Worth noting as a known limitation.


🟡 Gap: C++ namespaced constructor calls not inferred

File: gitnexus/src/core/ingestion/type-extractors/c-cpp.ts:82-92 and gitnexus/src/core/ingestion/type-env.ts:388-408

Both the extractInitializer and the extractCppConstructorBinding scanner handle identifier and type_identifier function nodes, but not scoped_identifier:

auto client = ns::HttpClient();   // func.type === 'scoped_identifier' — missed
auto client2 = new ns::HttpClient();  // new_expression — works (uses ctorType field)

auto x = new ns::HttpClient() works because new_expression.childForFieldName('type') returns the scoped_type_identifier node, which extractSimpleTypeName then strips to HttpClient. But the stack-allocation form auto x = ns::HttpClient() falls through both code paths silently. Namespaced construction is common in C++ projects with explicit namespacing conventions.


🟡 Gap: Ruby chained constructor calls missed

File: gitnexus/src/core/ingestion/type-env.ts:412-424

extractRubyConstructorBinding requires right.childForFieldName('method').text === 'new'. A chained call breaks this:

user = User.new.tap { |u| u.name = "alice" }   # method = 'tap', not 'new' → missed
user = User.new.freeze                           # method = 'freeze', not 'new' → missed

After calling User.new, common Ruby idioms chain .tap, .freeze, .then, or .itself. The outer call node's method is the chained method, not new. This is a structural limitation — would require recognizing that the receiver of the outer call is itself a User.new call.


Remaining from Previous Reviews

Issue Severity Language Status
new X() as T / new X()! 🟠 Gap TypeScript Carried over — valueNode.type !== 'new_expression'
C++ brace-init User{} 🟠 Gap C++ Carried over — initializer_list not handled
C++ cross-file direct construction 🟠 Gap C++ Carried over — classNames only has local AST names
Box::new(User::new()) 🟡 Gap Rust Carried over — outer ::new() resolves to Box

Summary Table

Issue Severity Language Status
PHP $this->method() never disambiguated 🔴 Bug PHP ✅ Fixed
Scope collision in cross-file constructor bindings 🟠 Bug All ✅ Fixed (partial — overloads still collide)
Kotlin object_declaration missing from CLASS_CONTAINER_TYPES 🟡 Gap Kotlin ✅ Fixed
Generic parent class breaks super resolution 🔴 Bug TS/Java/Python 🆕 New finding
Overloaded method scope collision 🟠 Gap All 🆕 Remaining gap after partial fix
C++ ns::User() namespaced constructor 🟡 Gap C++ 🆕 New finding
Ruby User.new.tap {} chained constructor 🟡 Gap Ruby 🆕 New finding
new X() as T / new X()! 🟠 Gap TypeScript Carried over
C++ brace-init / cross-file 🟠 Gap C++ Carried over
Box::new(User::new()) 🟡 Gap Rust Carried over

Next Steps

The highest-priority actionable item is the generic parent class bug in extractParentClassFromNode — it's a one-import + three-line change that fixes super resolution for TypeScript, Java, and Python generics/qualified names in one shot. The overloaded method scope collision is a known architectural limitation that would need sourceId format alignment to fully close.

@magyargergo

Copy link
Copy Markdown
Collaborator Author

Review Response

Fixed in this PR:

Finding Status Commit
🔴 Generic parent class breaks super resolution (TS/Java/Python/C#) Fixed extractSimpleTypeName strips generic params and qualified names
🟠 new X() as T / new X()! missed inference Fixed Unwrap as_expression/non_null_expression before new_expression check
🟠 C++ brace-init User{} missed inference Fixed Handle compound_literal_expression with type_identifier
🟠 C++ cross-file direct construction Already fixed Constructor binding pipeline verifies against SymbolTable

Integration tests added for each fix:

  • typescript-cast-constructor-inference/new User() as any, new Repo()!
  • typescript-generic-parent-resolution/extends BaseModel<string>
  • java-generic-parent-resolution/extends BaseModel<String>
  • csharp-generic-parent-resolution/: BaseModel<string>
  • kotlin-generic-parent-resolution/: BaseModel<String>()
  • cpp-brace-init-inference/auto x = User{}

Unit tests added (4 tests): TS as-cast, TS non-null, C++ User{}, C++ User{1,2}


Acknowledged as known limitations — deferred to a follow-up PR:

Gap Reason for deferral
🟠 Overloaded method scope collision (processUser@12 vs processUser@89 → same key) Requires plumbing @startIndex through ExtractedCall.sourceId format — architectural change across parse-worker ↔ call-processor boundary. Near-zero real-world impact (two overloads assigning to same-named local of different constructor types).
🟡 Rust Box::new(User::new()) Resolving nested ::new() arguments requires inspecting call arguments recursively — different mechanism than current single-level scoped_identifier matching. Common pattern (Arc, Mutex, Rc wrappers) but would need a dedicated "wrapper type unwrapping" strategy.
🟡 Ruby User.new.tap {} chained constructor Outer call's method is tap/freeze/then, not new. Would need recursive receiver unwrapping to find the inner User.new call.
🟡 C++ ns::User{} namespaced brace-init Brace-init fix handles type_identifier but not scoped_type_identifier. Rare pattern — most namespaced C++ uses () not {}.

…e-init

Fix generic parent class breaking super resolution:
- extractParentClassFromNode now uses extractSimpleTypeName to strip
  generic params (Base<T> → Base) and qualified names (models.Model → Model)
- Affects TS, Java, Python, C# heritage extraction

Fix TypeScript new X() as T / new X()! missed inference:
- Unwrap as_expression and non_null_expression before checking for
  new_expression in extractInitializer

Fix C++ brace-init User{} missed inference:
- Handle compound_literal_expression with type_identifier child
  in extractInitializer

Clean up deprecated lookupTypeEnv:
- Remove standalone lookupTypeEnv export, migrate all callers to
  TypeEnvironment.lookup() method
- Update all 80+ test assertions to use the new API

Integration test fixtures added:
- typescript-cast-constructor-inference (new X() as T, new X()!)
- typescript/java/csharp/kotlin-generic-parent-resolution
- cpp-brace-init-inference (auto x = User{})
@magyargergo

Copy link
Copy Markdown
Collaborator Author

@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution. Also review previous comments and review what's changed and how it improved the type resolution and what's the next steps are.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 5m 10s —— View job


Constructor Type Resolution — Gap Analysis (Round 4)

  • Review previous findings and what was fixed
  • Read current state of type-env.ts
  • Read current state of all type extractor files
  • Read call-processor.ts and resolution-context.ts
  • Find new uncovered patterns and bugs
  • Compile findings and post review

What Was Fixed Since Round 3

All four items flagged in Round 3's new-findings section are addressed:

Previous finding Status Evidence
🔴 Generic parent class breaks super resolution (TS/Java/Python) ✅ Fixed type-env.ts:135,142,158 all use extractSimpleTypeName(x) ?? x.text
🟠 new X() as T / new X()! missed inference ✅ Fixed typescript.ts:54-56 unwraps as_expression/non_null_expression before checking new_expression
🟠 C++ brace-init User{} ✅ Fixed c-cpp.ts:97-102 handles compound_literal_expression with type_identifier
🟠 C++ cross-file direct construction ✅ Already present constructor binding pipeline verified against SymbolTable

New Bugs Found

🔴 Bug: Go user := &User{} pointer-to-struct literal never inferred

File: gitnexus/src/core/ingestion/type-extractors/go.ts:63

extractGoShortVarDeclaration only matches when the RHS is a composite_literal:

if (valueNode.type !== 'composite_literal') continue;

But in Go, struct construction almost universally uses the address-of form:

user := &User{Name: "alice"}     // ← unary_expression wrapping composite_literal
user := User{Name: "alice"}      // ← works ✓
repo := &Repo{}                  // ← missed

The RHS of &User{} is a unary_expression whose first named child is the composite_literal. Both the type and the pattern (*User) are dropped. Since virtually every Go codebase creates structs this way (heap allocation via &), this is the highest-frequency miss across all languages.

The fix: before the composite_literal check, unwrap unary_expression nodes that are address-of operations.

Fix this →


🔴 Bug: TypeScript double-cast new X() as unknown as T — only one level unwrapped

File: gitnexus/src/core/ingestion/type-extractors/typescript.ts:54-56

The fix from Round 3 unwraps exactly one as_expression:

if (valueNode?.type === 'as_expression' || valueNode?.type === 'non_null_expression') {
  valueNode = valueNode.firstNamedChild;
}
// ← if valueNode is still as_expression here, we miss it
if (valueNode?.type !== 'new_expression') continue;

Double-casting as unknown as T is idiomatic TypeScript for escape-hatch type coercions and is common in test setup code and adapter layers:

const user = new User() as unknown as Admin;   // valueNode = as_expression after one unwrap → missed
const mock = new MockUser() as unknown as User; // same

After one unwrap of the outer as_expression, valueNode is the inner as_expression. The new_expression check fails. A single loop would fix this:

while (valueNode?.type === 'as_expression' || valueNode?.type === 'non_null_expression') {
  valueNode = valueNode.firstNamedChild;
}

Fix this →


🟡 Gap: Swift User.init(name:) explicit init call missed

File: gitnexus/src/core/ingestion/type-extractors/swift.ts:57-58 and gitnexus/src/core/ingestion/type-env.ts:489-490

Both the local extractor and the cross-file scanner check callee.type !== 'simple_identifier':

const callee = callExpr.firstNamedChild;
if (!callee || callee.type !== 'simple_identifier') return;

Swift allows explicitly calling .init:

let user = User.init(name: "alice")   // callee = navigation_expression, not simple_identifier
let repo = Repo.init()                // same — missed

The callee for User.init(...) is a navigation_expression node (or dot_expression), not simple_identifier. Both in-file inference and cross-file constructor binding miss this pattern. The fix is to unwrap navigation_expression to check if the receiver is a simple_identifier (the class name) and the field name is init.

Fix this →


🟡 Gap: C# var x = new Foo() — dead first branch in inference path

File: gitnexus/src/core/ingestion/type-extractors/csharp.ts:49-55

const initializer = findChildByType(declarators[0], 'object_creation_expression')  // ← always null
  ?? findChildByType(declarators[0], 'equals_value_clause')?.firstNamedChild;       // ← actual path

In tree-sitter-c-sharp, object_creation_expression is never a direct named child of variable_declarator — it's always wrapped inside equals_value_clause. So the first findChildByType call always returns null and the second path is always used. The dead code doesn't cause a bug (the fallback works), but it misleads future readers and carries unnecessary overhead on every var declaration processed.


Remaining from Previous Reviews (Acknowledged Deferred)

Issue Severity Language Status
Overloaded method scope collision (processUser@12 vs processUser@89) 🟠 Gap All Deferred — requires plumbing @startIndex through ExtractedCall.sourceId
Box::new(User::new()) nested constructor 🟡 Gap Rust Deferred — needs recursive wrapper-type unwrapping strategy
User.new.tap {} chained constructor 🟡 Gap Ruby Deferred — outer call method is tap/freeze, not new
ns::User{} namespaced brace-init 🟡 Gap C++ Deferred — brace-init fix handles type_identifier but not scoped_type_identifier

Summary Table

Issue Severity Language Status
Generic parent class breaks super resolution 🔴 Bug TS/Java/Python/C# ✅ Fixed
new X() as T / new X()! missed inference 🟠 Gap TypeScript ✅ Fixed
C++ brace-init User{} 🟠 Gap C++ ✅ Fixed
Go &User{} pointer-to-struct not inferred 🔴 Bug Go 🆕 New finding
TypeScript double-cast as unknown as T 🔴 Bug TypeScript 🆕 New finding
Swift User.init(name:) missed 🟡 Gap Swift 🆕 New finding
C# dead-code first branch in var inference 🟡 Nit C# 🆕 New finding

Next Steps

The Go &User{} fix is the highest-leverage change: it affects the most real-world code (virtually every Go struct construction) and requires a minimal one-line unwrap before the existing composite_literal check. The TypeScript double-cast fix (ifwhile) is a one-line change. Both are safe and isolated. The Swift .init gap requires slightly more structural work to handle navigation_expression callee nodes.

@magyargergo

Copy link
Copy Markdown
Collaborator Author

Review Response (Round 4)

Fixed:

Finding Status
🔴 Go &User{} pointer-to-struct not inferred Fixed — unwrap unary_expression (address-of) before composite_literal check
🔴 TS double-cast as unknown as T only one level unwrapped Fixed — changed if to while loop for nested as_expression/non_null_expression
🟡 Swift User.init(name:) missed Fixed — handle navigation_expression callee with .init suffix

Pushback:

Finding Verdict
🟡 C# dead-code first branch in var inference Reviewer was wrong. Verified against actual tree-sitter-c-sharp AST: object_creation_expression IS a direct child of variable_declarator (not wrapped in equals_value_clause). The first findChildByType branch is live code. Removing it broke the existing var x = new User() test. Reverted with updated comment.

Integration tests added:

  • go-pointer-constructor-inference/user := &User{}, repo := &Repo{}
  • typescript-double-cast-inference/new User() as unknown as any, new Repo() as unknown as object

Unit tests added (3 tests): Go &User{}, Go multi-assign &User{}, &Repo{}, TS new X() as unknown as T

Fix Go pointer-to-struct literal not inferred:
- Unwrap unary_expression (address-of &) before composite_literal check
- user := &User{} now correctly infers type User

Fix TypeScript double-cast only unwrapping one level:
- Change if to while loop for nested as_expression/non_null_expression
- new User() as unknown as Admin now correctly infers type User

Fix Swift User.init(name:) explicit init call missed:
- Handle navigation_expression callee with .init suffix in extractInitializer

Integration test fixtures:
- go-pointer-constructor-inference (&User{}, &Repo{})
- typescript-double-cast-inference (as unknown as T)
@magyargergo

Copy link
Copy Markdown
Collaborator Author

@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution. Also review previous comments and review what's changed and how it improved the type resolution and what's the next steps are.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 4m 35s —— View job


Constructor Type Resolution — Gap Analysis (Round 5)

  • Review Round 4 fixes and gather context
  • Read current state of all type extractor files
  • Read type-env.ts, call-processor.ts, utils.ts
  • Find new uncovered patterns and bugs
  • Compile findings and post review

What Was Fixed Since Round 4

All three actionable items from Round 4 are addressed:

Previous finding Status How it was fixed
🔴 Go &User{} pointer-to-struct not inferred ✅ Fixed go.ts:64-66 — unwraps unary_expression (address-of) before composite_literal check
🔴 TS double-cast new X() as unknown as T only one level unwrapped ✅ Fixed typescript.ts:54if changed to while loop for nested as_expression/non_null_expression
🟡 Swift User.init(name:) missed ✅ Fixed swift.ts:68-77 — handles navigation_expression callee with .init suffix

The C# dead-code branch (Round 4 finding) was correctly rejected after AST verification.


New Bugs Found

🔴 Bug: Rust struct literal User { name: "alice" } never inferred

File: gitnexus/src/core/ingestion/type-extractors/rust.ts:40

extractInitializer only handles call_expression (for ::new()/::default() paths). But Rust struct initialization syntax — the most common way to construct a struct — produces a struct_expression node:

let user = User { name: "alice", age: 30 };   // struct_expression — missed
let user = User::new("alice");                 // call_expression — works ✓

extractDeclaration also won't catch this because let user = User { ... } has no explicit type annotation. As a result, user.save() cannot be resolved to User.save via receiver typing.

Notably, struct_expression is already registered in utils.ts:716 under CONSTRUCTOR_CALL_NODE_TYPES, so the parse-worker does emit the struct construction call — but the TypeEnv side has no corresponding binding. The binding extraction and the call extraction are mismatched.

The fix: add a struct_expression branch to extractInitializer in rust.ts — read node.childForFieldName('name') to get the type name and extractVarName(pattern) to get the variable.

Fix this →


🟠 Gap: Swift cross-file User.init() still missed by scanner

File: gitnexus/src/core/ingestion/type-env.ts:489-490

Round 4 correctly fixed the local extractInitializer in swift.ts:68-77 to handle navigation_expression callee (.init suffix). But the cross-file CONSTRUCTOR_BINDING_SCANNERS[Swift] at line 490 still only matches simple_identifier:

const callee = callExpr.firstNamedChild;
if (!callee || callee.type !== 'simple_identifier') return undefined;  // ← misses User.init()

When User is defined in another file and not in the local classNames, the local extractInitializer guard classNames.has(calleeName) would fail. The scanner is the fallback for cross-file verification — but it too drops navigation_expression callee nodes. So let x = User.init(name:) where User is from another file produces no ConstructorBinding, and the call is never verified against the SymbolTable.

The fix: extend the Swift scanner's callee handling to also match navigation_expression where lastNamedChild.text === 'init', mirroring what the local extractor now does.

Fix this →


🟠 Gap: Python qualified constructor models.User("alice") never inferred

File: gitnexus/src/core/ingestion/type-extractors/python.ts:54 and gitnexus/src/core/ingestion/type-env.ts:465-467

Both the local extractor and the cross-file scanner filter to func.type !== 'identifier':

const func = right.childForFieldName('function');
if (!func || func.type !== 'identifier') return;  // ← attribute node dropped

In tree-sitter-python, models.User("alice") parses the function field as an attribute node (not identifier). extractSimpleTypeName already handles attribute nodes by extracting the last segment — so models.UserUser is structurally supported, but the identifier type guard prevents it from being reached.

This is a high-frequency miss for any Python codebase using Django (models.Model), SQLAlchemy (db.Model), or other module-prefixed class constructors.

A fix: change func.type !== 'identifier' to extractSimpleTypeName(func) === undefined and extract the callee name via extractSimpleTypeName rather than func.text. The same change applies to both locations.

Fix this →


🟡 Gap: Go built-in new(User) not inferred

File: gitnexus/src/core/ingestion/type-extractors/go.ts:67

extractGoShortVarDeclaration now handles both User{} and &User{}. But Go's built-in new() function is another common zero-value pointer construction:

user := new(User)   // call_expression, not composite_literal — missed
repo := &Repo{}     // works ✓ (unary + composite_literal)

new(User) parses as a call_expression whose first named child is the identifier new and whose argument is a type_identifier. Since Go's extractGoShortVarDeclaration only processes composite_literal (after unwrapping unary_expression), the call_expression path for new() is entirely unhandled. The inferred type should be User (not *User, since method resolution matches against the base class name).

Fix this →


🟡 Gap: C++ structured bindings auto [x, y] = SomeClass() not handled

File: gitnexus/src/core/ingestion/type-extractors/c-cpp.ts:48-52 and gitnexus/src/core/ingestion/type-env.ts:394

C++17 structured bindings produce a structured_binding_declarator instead of an init_declarator:

auto [user, repo] = getObjects();    // structured_binding_declarator — declarator.type !== 'init_declarator' → dropped
auto x = new User();                 // init_declarator — works ✓

Both extractInitializer (line 52: if (declarator.type !== 'init_declarator') return;) and extractCppConstructorBinding (line 394: same guard) require init_declarator. Structured bindings are a common C++17 pattern (tuples, pairs, structured data unpacking). The type can't be inferred without knowing the function return type, so this is more of a structural limitation than a fixable bug — but worth documenting as a known gap alongside the other deferred C++ gaps.


Remaining from Previous Reviews (Acknowledged Deferred)

Issue Severity Language Status
Overloaded method scope collision (processUser@12 vs processUser@89) 🟠 Gap All Deferred
Box::new(User::new()) nested wrapper type 🟡 Gap Rust Deferred
User.new.tap {} chained Ruby constructor 🟡 Gap Ruby Deferred
ns::User{} namespaced C++ brace-init 🟡 Gap C++ Deferred

Summary Table

Issue Severity Language Status
Go &User{} pointer-to-struct not inferred 🔴 Bug Go ✅ Fixed
TS double-cast as unknown as T 🔴 Bug TypeScript ✅ Fixed
Swift User.init(name:) local inference 🟡 Gap Swift ✅ Fixed
Rust struct literal User { name: "alice" } 🔴 Bug Rust 🆕 New finding
Swift cross-file User.init() scanner gap 🟠 Gap Swift 🆕 New finding
Python models.User("alice") missed 🟠 Gap Python 🆕 New finding
Go new(User) built-in not inferred 🟡 Gap Go 🆕 New finding
C++ structured binding auto [x, y] 🟡 Gap C++ 🆕 Known limitation

Next Steps

The Rust struct literal bug is the highest-leverage fix: User { ... } is the most common Rust construction pattern, and struct_expression is already recognized in CONSTRUCTOR_CALL_NODE_TYPES in utils.ts — so the call-extraction side is covered, but the TypeEnv binding side is missing. Adding the struct_expression branch to extractInitializer in rust.ts closes this gap with a small, targeted change.

The Python qualified constructor fix (models.User()) has broad real-world impact for Django/SQLAlchemy codebases and is a two-location change. The Swift cross-file .init scanner gap is a one-line extension mirroring the already-implemented local fix.

…it scanner

- Rust: handle struct_expression in extractInitializer (User { name: "alice" })
- Python: support attribute nodes in extractInitializer (models.User("alice"))
  and the cross-file scanner — extractSimpleTypeName handles qualified names
- Go: handle new(User) built-in in extractGoShortVarDeclaration
- Swift: extend CONSTRUCTOR_BINDING_SCANNERS to handle navigation_expression
  callee for User.init(name:) cross-file resolution

Unit tests: 87 → 96 (Rust struct literal, Go new(), Python qualified ctor,
Python scanner qualified, plus edge cases)
Integration tests: 4 new describe blocks with fixtures
@magyargergo

Copy link
Copy Markdown
Collaborator Author

Round 5 Response

All 4 actionable findings fixed in 514b8f5. C++ structured bindings acknowledged as known limitation.

Finding Status Fix
🔴 Rust struct literal User { name: "alice" } ✅ Fixed rust.ts:40-48struct_expression branch in extractInitializer, reads type from value.childForFieldName('name')
🟠 Swift cross-file User.init() scanner ✅ Fixed type-env.ts:490-498 — scanner now handles navigation_expression callee with .init suffix, mirroring the local extractor
🟠 Python models.User("alice") qualified ctor ✅ Fixed python.ts:54 + type-env.ts:466 — replaced func.type !== 'identifier' guard with extractSimpleTypeName(func) which handles attribute nodes
🟡 Go new(User) built-in ✅ Fixed go.ts:68-78call_expression branch checks for new callee and extracts type from first argument
🟡 C++ structured bindings auto [x, y] ⏭️ Deferred Requires return type inference — structural limitation

Tests added:

  • Unit: 87 → 96 (+9 tests: Rust struct literal ×3, Go new() ×2, Python qualified ctor ×3, Python scanner qualified ×1)
  • Integration: 4 new describe blocks with dedicated fixtures (rust-struct-literal-inference, go-new-builtin, python-qualified-constructor, swift-init-cross-file)

Deferred (cumulative across rounds):

  • Overloaded method scope collision (processUser@12 vs processUser@89) — all languages
  • Box::new(User::new()) nested wrapper — Rust
  • User.new.tap {} chained constructor — Ruby
  • ns::User{} namespaced brace-init — C++
  • auto [x, y] = SomeClass() structured bindings — C++

@magyargergo

Copy link
Copy Markdown
Collaborator Author

@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution. Also review previous comments and review what's changed and how it improved the type resolution and what's the next steps are.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 4m 11s —— View job


Constructor Type Resolution — Gap Analysis (Round 6)

  • Review previous findings and gather context
  • Read current state of all type extractor files
  • Read type-env.ts, call-processor.ts, utils.ts
  • Find new uncovered patterns and bugs
  • Compile findings and post review

What Was Fixed Since Round 5

All four actionable findings from Round 5 are addressed:

Previous finding Status How it was fixed
🔴 Rust struct literal User { name: "alice" } not inferred ✅ Fixed rust.ts:43-50struct_expression branch reads value.childForFieldName('name') for the type
🟠 Swift cross-file User.init() scanner gap ✅ Fixed type-env.ts:499-506 — scanner now handles navigation_expression callee with .init suffix
🟠 Python models.User("alice") missed ✅ Fixed python.ts:57 + type-env.ts:468extractSimpleTypeName(func) replaces func.type !== 'identifier' guard, handling attribute nodes
🟡 Go new(User) built-in ✅ Fixed go.ts:68-78call_expression branch checks for new callee and extracts first argument

New Bugs Found

🔴 Bug: Rust Self {} struct literal resolves to the string "Self" instead of the enclosing type

File: gitnexus/src/core/ingestion/type-extractors/rust.ts:43-50

The Round 5 fix correctly added a struct_expression branch, but it doesn't apply the same Self → enclosing impl type resolution that the call_expression branch already has:

// call_expression branch (line 65) — handles Self::new() correctly:
const typeName = rawType === 'Self' ? findEnclosingImplType(node) : rawType;

// struct_expression branch (lines 46-48) — no Self resolution:
const typeName = extractSimpleTypeName(typeNode);  // returns "Self" verbatim
if (varName && typeName) env.set(varName, typeName);

Pattern that fails:

impl User {
  fn reset(&self) -> Self {
    let fresh = Self { name: String::new() };  // ← stored as "Self", not "User"
    fresh.validate()  // receiver type "Self" never matches "User" in call resolution
  }
}

Using Self { ... } in impl blocks to create new instances is standard Rust — it's the idiomatic way to write constructors that return Self. The fix is a one-liner: add the same rawType === 'Self' ? findEnclosingImplType(node) : rawType guard in the struct_expression branch.

Fix this →


🟠 Gap: C++ auto x = ns::HttpClient{} brace-init with scoped type — still no-ops

File: gitnexus/src/core/ingestion/type-extractors/c-cpp.ts:97-102

The compound_literal_expression handler uses a hard type_identifier type guard:

if (value.type === 'compound_literal_expression') {
  const typeId = value.firstNamedChild;
  if (typeId?.type === 'type_identifier') {   // ← drops scoped_type_identifier
    env.set(varName, typeId.text);
  }
}

For auto x = ns::HttpClient{}, tree-sitter-cpp parses the type as scoped_type_identifier, not type_identifier. extractSimpleTypeName already handles scoped_type_identifier (it takes the last segment). Replacing the hardcoded guard with extractSimpleTypeName handles both forms with no added complexity:

if (value.type === 'compound_literal_expression') {
  const typeId = value.firstNamedChild;
  const typeName = typeId ? extractSimpleTypeName(typeId) : undefined;
  if (typeName) env.set(varName, typeName);
}

This was previously deferred under "ns::User{} namespaced brace-init" but is now a one-line change that fixes it. Same fix should be applied to the cross-file scanner's compound_literal_expression handling — though the scanner currently has no brace-init handling at all (see below).

Fix this →


New Gaps Found

🟡 Gap: PHP 8.0+ constructor property promotion not in TYPED_PARAMETER_TYPES

File: gitnexus/src/core/ingestion/type-extractors/shared.ts:92-99

PHP 8.0 introduced constructor property promotion — parameters prefixed with visibility modifiers are simultaneously declared as properties:

class User {
  public function __construct(
    private readonly string $name,
    private UserRepo $repo   // ← property_promotion_parameter, not simple_parameter
  ) {
    $repo->connect();  // ← $repo type should resolve to UserRepo
  }
}

TYPED_PARAMETER_TYPES includes 'simple_parameter' (regular PHP function params) but not 'property_promotion_parameter'. The buildTypeEnv walker dispatches to config.extractParameter only if TYPED_PARAMETER_TYPES.has(node.type). Since property_promotion_parameter is not in the set, these nodes are silently skipped, and $repo->method() inside the constructor body won't have its receiver type resolved.

The fix: add 'property_promotion_parameter' to TYPED_PARAMETER_TYPES in shared.ts. The extractParameter generic fallback in php.ts will then handle it correctly (it reads name and type fields, which the promoted parameter node also has).

Fix this →


🟡 Gap: Ruby constant constructor assignment missed by scanner

File: gitnexus/src/core/ingestion/type-env.ts:429

extractRubyConstructorBinding requires left.type === 'identifier', but Ruby constants use a different node type ('constant'):

UserService = UserService.new      # left.type === 'constant' — missed
REPO = Repo.new                    # same
MyApp::USERS = User.new            # constant in module — left is scope_resolution

These class-level constant assignments act as singleton-like accessors. While not as common as local variables, they do appear in service objects and configuration patterns. The fix is a two-character change: left.type !== 'identifier'left.type !== 'identifier' && left.type !== 'constant'.

Fix this →


Remaining from Previous Reviews (Acknowledged Deferred)

Issue Severity Language Status
Overloaded method scope collision (processUser@12 vs processUser@89) 🟠 Gap All Deferred
Box::new(User::new()) nested wrapper 🟡 Gap Rust Deferred
User.new.tap {} chained Ruby constructor 🟡 Gap Ruby Deferred

Summary Table

Issue Severity Language Status
Rust struct literal User { name: "alice" } 🔴 Bug Rust ✅ Fixed
Swift cross-file User.init() scanner 🟠 Gap Swift ✅ Fixed
Python models.User("alice") qualified ctor 🟠 Gap Python ✅ Fixed
Go new(User) built-in 🟡 Gap Go ✅ Fixed
Rust Self {} struct literal resolves to "Self" 🔴 Bug Rust 🆕 New finding
C++ ns::User{} brace-init with scoped type 🟠 Gap C++ 🆕 Easy fix (was deferred)
PHP 8.0+ constructor property promotion 🟡 Gap PHP 🆕 New finding
Ruby constant constructor (USER = User.new) 🟡 Gap Ruby 🆕 New finding

Next Steps

The Rust Self {} fix is the highest-priority item — a one-liner that makes Self { ... } struct literals consistent with Self::new(), which already resolves Self correctly. It also matches a very common Rust idiom (factory methods returning Self via struct literal). The C++ scoped brace-init fix is a one-line change (type_identifier check → extractSimpleTypeName) that finally closes the deferred namespaced brace-init gap. The PHP property promotion fix is a one-word addition to TYPED_PARAMETER_TYPES that unblocks PHP 8.0+ constructor parameter resolution.

…ams, Ruby constants

- Rust: resolve Self {} struct literal to enclosing impl type (was stored as "Self")
- C++: replace type_identifier guard with extractSimpleTypeName for compound_literal_expression,
  enabling ns::User{} scoped brace-init (closes previously deferred gap)
- PHP: add property_promotion_parameter to TYPED_PARAMETER_TYPES for PHP 8.0+
  constructor property promotion (__construct(private Foo $x))
- Ruby: extend extractRubyConstructorBinding to accept constant left-hand side
  (REPO = Repo.new)

Unit tests: 96 → 101 (+5: Rust Self{} ×2, C++ ns::User{} ×1, PHP promotion ×1,
Ruby constant ×1)
Integration tests: 4 new describe blocks with fixtures
@magyargergo

Copy link
Copy Markdown
Collaborator Author

Round 6 Response

All 4 actionable findings fixed in 0073c42. C++ structured bindings acknowledged as known limitation.

Finding Status Fix
🔴 Rust Self {} struct literal resolves to "Self" ✅ Fixed rust.ts:46 — added Self → findEnclosingImplType(node) resolution, matching the call_expression branch
🟠 C++ ns::User{} scoped brace-init ✅ Fixed c-cpp.ts:98 — replaced type_identifier guard with extractSimpleTypeName(typeId) which handles scoped_type_identifier
🟡 PHP 8.0+ constructor property promotion ✅ Fixed shared.ts:99 — added property_promotion_parameter to TYPED_PARAMETER_TYPES
🟡 Ruby constant constructor (REPO = Repo.new) ✅ Fixed type-env.ts:429 — extended extractRubyConstructorBinding to accept constant LHS alongside identifier

Tests added:

  • Unit: 96 → 101 (+5: Rust Self{} ×2, C++ ns::User{} ×1, PHP promotion ×1, Ruby constant ×1)
  • Integration: 4 new describe blocks with fixtures (rust-self-struct-literal, cpp-scoped-brace-init, php-property-promotion, ruby-constant-constructor)

Architectural Note: Chained Property Access Resolution

The PHP property promotion fix enables $repo->method() inside the constructor body (the promoted parameter is a local variable). However, $this->repo->method() in other methods does NOT resolve, because this requires multi-step receiver resolution — a cross-language gap, not PHP-specific.

What happens today:

  1. $this->repo->save()extractReceiverName sees the receiver of save() is $this->repo (a member_access_expression)
  2. extractReceiverName only captures simple identifiers (SIMPLE_RECEIVER_TYPES) — complex expressions like $this->repo return undefined
  3. Without a receiver type, the call falls through to arity-based or unique-global resolution

What's needed for full chained resolution:

  1. Property-type tracking — store class property types in the TypeEnv (e.g., from promoted params, typed properties, or constructor assignments)
  2. Multi-step receiver unwinding — when receiver is obj.prop, resolve obj to its class, look up prop's type on that class, then use that type for method resolution

This affects all obj.prop.method() patterns across all languages (TypeScript this.service.call(), Java this.repo.find(), Python self.db.query(), etc.). It's a separate feature that warrants its own PR, not a fix within constructor inference scope.

Deferred (cumulative across rounds):

  • Overloaded method scope collision (processUser@12 vs processUser@89) — all languages
  • Box::new(User::new()) nested wrapper — Rust
  • User.new.tap {} chained Ruby constructor — Ruby
  • auto [x, y] = SomeClass() structured bindings — C++
  • Chained property access resolution ($this->prop->method(), self.attr.call()) — all languages

…, Go make/assert

Phase 1 quick wins from the type resolution gap analysis:

1. Python walrus operator := (named_expression) — extractInitializer + scanner
2. PHP 7.4+ typed class properties — property_declaration in extractDeclaration
3. Nullable union unwrapping — User | null → User in extractSimpleTypeName
4. Go make() builtin — slice/map element type extraction
5. Go type assertions — iface.(User) type extraction

Also: PHP primitive_type handling in extractSimpleTypeName (string, int, etc.)

Unit tests: 101 → 114 (+13)
Integration tests: 8 new describe blocks with fixtures
…attern matching, Python class annotations

Phase 2 medium-effort improvements:

1. C++ range-for with explicit type — for (User& u : vec) binds u: User
2. Rust if-let/while-let captured_pattern — user @ User { .. } binds user: User
3. C# is-pattern matching — if (obj is User user) binds user: User
4. Python class-level annotations — confirmed already working, added tests

Unit tests: 114 → 127 (+13)
Integration tests: 11 new test cases with fixtures
@magyargergo

Copy link
Copy Markdown
Collaborator Author

Type Resolution Gap Closure — Phase 1 + Phase 2 Complete

After the 6 rounds of constructor inference review, a comprehensive 4-agent gap analysis identified 14 categories of missing type resolution patterns across all 12 languages. Phase 1 and Phase 2 are now implemented and pushed.


Phase 1: Quick Wins (commit a5649ab)

Task Language Pattern Tests
Python walrus operator Python `if (user := User("alice")):` → `user: User` +3 unit, +2 integration
PHP typed class properties PHP `private UserRepo $repo;` → `$repo: UserRepo` +2 unit, +4 integration
Nullable union unwrapping TypeScript `User | null` → `User` in extractSimpleTypeName +4 unit, +5 integration
Go make() builtin Go `make([]User, 0)` → element type `User` +2 unit, +2 integration
Go type assertions Go `iface.(User)` → `user: User` +2 unit, +2 integration

Phase 2: Medium Effort (commit 41d59f6)

Task Language Pattern Tests
C++ range-for C++ `for (User& u : vec)` → `u: User` +2 unit, +3 integration
Rust if-let/while-let Rust `if let user @ User { .. } = expr` → `user: User` +7 unit, +3 integration
C# pattern matching C# `if (obj is User user)` → `user: User` +1 unit, +4 integration
Python class annotations Python Already worked — confirmed with tests +3 unit, +4 integration

Test Count Growth

  • Unit tests: 101 → 127 (+26)
  • Integration tests: 24+ new test cases across 8 fixtures

Phase 3 (Deferred — Architecture Changes)

These require TypeEnv redesign and are scoped for a follow-up PR:

  • Return type inference (wire existing extractMethodSignature data into TypeEnv)
  • Chained property access (`this.repo.save()` multi-step resolution)
  • For-loop variable typing (depends on generic extraction)
  • Generic type parameter extraction (`List` → element type)
  • Block-level type narrowing (if/match scoping)
  • Ruby dedicated type extractor (YARD annotations)

@magyargergo magyargergo merged commit 62242d5 into main Mar 14, 2026
19 checks passed
@magyargergo magyargergo deleted the feat/type-resolution-constructor-inference branch March 14, 2026 19:05
motolese pushed a commit to motolese/datamoto-gitnexus that referenced this pull request Apr 23, 2026
… resolution (abhigyanpatwari#274)

* feat(type-env): constructor-call type inference for TypeEnv (Phase 1)

Add extractInitializer as a Tier 1 fallback in buildTypeEnv: when a
declaration node has no explicit type annotation, infer the type from
constructor-call patterns (new X(), X::new(), X::default(), $x = new X()).

Languages covered: TypeScript/JS, Java (var), Rust, PHP, C++ (auto).
Python/Kotlin/Swift deferred — need symbol-table access to distinguish
class constructors from function calls.

Adds 20 new unit tests covering constructor inference, annotation
precedence, and known limitations across all supported languages.

* fix(type-env): class-aware constructor resolution, multi-declarator fix

- Add collectClassNames pre-scan: walks AST to build Set<string> of
  class/struct names defined in the file
- C++ extractInitializer uses classNames.has() to verify identifier is
  a known class before inferring (auto x = User() resolves, auto x =
  getUser() does not — no false positives)
- Add InitializerExtractor type that receives classNames parameter
- Fix env.size gating: always call extractInitializer when available,
  so mixed declarators like const a: A = x, b = new B() resolve both
- Add env.has() guard in Java extractInitializer to skip already-bound vars
- Document Rust new/default whitelist rationale
- Pin all test assertions, add mixed multi-declarator test case

* fix(type-env): resolve Self/self/static/parent to actual type names

- Rust: Self::new()/Self::default() resolves to enclosing impl type
- PHP: new self()/static() resolves to enclosing class, parent() to superclass
- Rust: Tier 0 annotation guard prevents overwrite by constructor inference
- Rust: mut_pattern handling in extractVarName for let mut bindings
- TS: fix misleading comment in extractInitializer
- 58 tests passing (3 new Self/self resolution tests)

* perf(type-env): single-pass AST walk with closure-scoped state

Refactors buildTypeEnv to use closures instead of passing mutable state
as parameters. classNames, env, and config are captured by the inner
walk and extractTypeBinding functions — no parameter mutation.

- Eliminates separate collectClassNames pre-scan (O(2n) → O(n))
- config looked up once per file instead of per-node
- 29 fewer lines

* feat(type-env): constructor-inferred type resolution for all languages

Add cross-file constructor type inference to the ingestion pipeline,
enabling receiver-type disambiguation for member calls like
`user.save()` when the variable is assigned from a constructor without
explicit type annotations.

Pipeline changes:
- Add extractInitializer to Python and Swift type extractors
- Add CONSTRUCTOR_BINDING_SCANNERS for Python, Swift, C/C++ in type-env
- Wire constructorBindings through parse-worker → parsing-processor →
  pipeline → processCallsFromExtracted
- Rewrite resolveCallTarget receiver-type filtering (step D) to use
  tiered import resolution (same-file → import-scoped → global) before
  falling back to fuzzy ownerId matching
- Use collectTieredCandidates for constructor binding verification
  instead of raw lookupFuzzy

Bug fixes:
- Fix C++ inline method query: @definition.method was captured on
  field_declaration_list instead of function_definition, causing wrong
  parameterCount for all inline class methods
- Fix parse-worker accumulated/flush results missing constructorBindings

CI changes:
- Add swift.test.ts to ci-integration pipeline group and coverage job
- Update ci-report to fetch base branch (main) coverage for delta
  reporting instead of showing config thresholds
- Add per-suite timing breakdown table (unit/integration/total)
- Add expandable skipped test details section

Tests: 288 passed, 4 skipped (swift — macOS only) across 10 languages
- 36 new constructor-inferred integration tests (4 per language)
- 10 fixture directories with cross-file constructor patterns
- TypeScript, JavaScript, Java, Kotlin, Python, PHP, Rust, Go, C++, Swift

* fix(type-extractors): add type assertion for LanguageTypeConfig

* feat(ruby): constructor-inferred type resolution and self-receiver mapping

Add Ruby User.new constructor binding scanner to type-env, enabling
receiver-type disambiguation for member calls like user.save vs repo.save.
Add self/this → enclosing class resolution in lookupTypeEnv so self.method()
calls resolve to the correct class even when the method name is ambiguous.

* docs: update README with constructor inference and self/this resolution details

* refactor(ingestion): unified ResolutionContext replaces fragmented map passing

Introduce createResolutionContext() as the single resolution API for all
processors. Eliminates duplicated tier-selection logic, fixes heritage
namedImportMap bug, and adds per-file resolution caching.

- NEW resolution-context.ts: closure-factory with resolve(), per-file cache,
  TIER_CONFIDENCE constant, and shared ResolutionTier type
- DELETE symbol-resolver.ts: zero production importers, logic now in
  resolution-context.ts
- call-processor: all functions take ctx instead of 6 separate maps,
  collectTieredCandidates removed (ctx.resolve replaces it),
  D4 redundant re-resolve eliminated
- heritage-processor: takes ctx, resolveHeritageId helper extracts
  repeated 14-line fallback pattern, namedImportMap now included
- import-processor: takes ctx, dead createImportMap/createPackageMap/
  createNamedImportMap factories removed
- pipeline: creates single ctx, wires onProgress to all processors,
  logs cache hit rate in dev mode
- Tier renamed: unique-global → global (honest about returning all candidates)
- Tests migrated: 1178 unit + 84 integration passing

* feat(type-env): self/this/super resolution, TypeEnvironment API, and review fixes

Add cross-language receiver keyword resolution:
- self/this/$this → enclosing class name via AST walk
- super/base/parent → parent class name via heritage AST extraction
  (8 grammar variants: TS/JS, Java, Python, Ruby, C#, PHP, Kotlin, C++, Swift)
- D-phase widening in resolveCallTarget for super→parent method dispatch

Introduce TypeEnvironment API replacing loose TypeEnvResult + lookupTypeEnv:
- buildTypeEnv() returns TypeEnvironment with .lookup() method
- Single-pass AST walk merges constructor binding scan (was separate traversal)
- ClassNameLookup type replaces over-broad ReadonlySet<string> facade
- Memoized class name lookups to avoid redundant SymbolTable scans

Code review fixes (6 agents, 11 findings):
- Replace ctx.resolve(name, '') hack with direct symbols.lookupFuzzy()
- Extract scope key helpers (extractFuncNameFromScope, receiverKey)
- Simplify D-phase from 5 steps to 4 with deduped typeNodeIds
- Remove C from CONSTRUCTOR_BINDING_SCANNERS (YAGNI — C has no constructors)
- Cache Map reuse in ResolutionContext to reduce GC pressure
- Remove unused TieredCandidates import

Integration tests for self/this, parent, and super resolution across all
12 supported languages with per-language fixture directories.

* fix(type-env): generic parent resolution, TS cast inference, C++ brace-init

Fix generic parent class breaking super resolution:
- extractParentClassFromNode now uses extractSimpleTypeName to strip
  generic params (Base<T> → Base) and qualified names (models.Model → Model)
- Affects TS, Java, Python, C# heritage extraction

Fix TypeScript new X() as T / new X()! missed inference:
- Unwrap as_expression and non_null_expression before checking for
  new_expression in extractInitializer

Fix C++ brace-init User{} missed inference:
- Handle compound_literal_expression with type_identifier child
  in extractInitializer

Clean up deprecated lookupTypeEnv:
- Remove standalone lookupTypeEnv export, migrate all callers to
  TypeEnvironment.lookup() method
- Update all 80+ test assertions to use the new API

Integration test fixtures added:
- typescript-cast-constructor-inference (new X() as T, new X()!)
- typescript/java/csharp/kotlin-generic-parent-resolution
- cpp-brace-init-inference (auto x = User{})

* fix(type-extractors): Go &User{}, TS double-cast, Swift .init inference

Fix Go pointer-to-struct literal not inferred:
- Unwrap unary_expression (address-of &) before composite_literal check
- user := &User{} now correctly infers type User

Fix TypeScript double-cast only unwrapping one level:
- Change if to while loop for nested as_expression/non_null_expression
- new User() as unknown as Admin now correctly infers type User

Fix Swift User.init(name:) explicit init call missed:
- Handle navigation_expression callee with .init suffix in extractInitializer

Integration test fixtures:
- go-pointer-constructor-inference (&User{}, &Repo{})
- typescript-double-cast-inference (as unknown as T)

* feat: Rust struct literal, Python qualified ctor, Go new(), Swift .init scanner

- Rust: handle struct_expression in extractInitializer (User { name: "alice" })
- Python: support attribute nodes in extractInitializer (models.User("alice"))
  and the cross-file scanner — extractSimpleTypeName handles qualified names
- Go: handle new(User) built-in in extractGoShortVarDeclaration
- Swift: extend CONSTRUCTOR_BINDING_SCANNERS to handle navigation_expression
  callee for User.init(name:) cross-file resolution

Unit tests: 87 → 96 (Rust struct literal, Go new(), Python qualified ctor,
Python scanner qualified, plus edge cases)
Integration tests: 4 new describe blocks with fixtures

* fix: Rust Self{} resolution, C++ scoped brace-init, PHP promotion params, Ruby constants

- Rust: resolve Self {} struct literal to enclosing impl type (was stored as "Self")
- C++: replace type_identifier guard with extractSimpleTypeName for compound_literal_expression,
  enabling ns::User{} scoped brace-init (closes previously deferred gap)
- PHP: add property_promotion_parameter to TYPED_PARAMETER_TYPES for PHP 8.0+
  constructor property promotion (__construct(private Foo $x))
- Ruby: extend extractRubyConstructorBinding to accept constant left-hand side
  (REPO = Repo.new)

Unit tests: 96 → 101 (+5: Rust Self{} ×2, C++ ns::User{} ×1, PHP promotion ×1,
Ruby constant ×1)
Integration tests: 4 new describe blocks with fixtures

* feat: Phase 1 type resolution gaps — walrus, PHP properties, nullable, Go make/assert

Phase 1 quick wins from the type resolution gap analysis:

1. Python walrus operator := (named_expression) — extractInitializer + scanner
2. PHP 7.4+ typed class properties — property_declaration in extractDeclaration
3. Nullable union unwrapping — User | null → User in extractSimpleTypeName
4. Go make() builtin — slice/map element type extraction
5. Go type assertions — iface.(User) type extraction

Also: PHP primitive_type handling in extractSimpleTypeName (string, int, etc.)

Unit tests: 101 → 114 (+13)
Integration tests: 8 new describe blocks with fixtures

* feat: Phase 2 type resolution gaps — C++ range-for, Rust if-let, C# pattern matching, Python class annotations

Phase 2 medium-effort improvements:

1. C++ range-for with explicit type — for (User& u : vec) binds u: User
2. Rust if-let/while-let captured_pattern — user @ User { .. } binds user: User
3. C# is-pattern matching — if (obj is User user) binds user: User
4. Python class-level annotations — confirmed already working, added tests

Unit tests: 114 → 127 (+13)
Integration tests: 11 new test cases with fixtures
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant