feat: TypeEnvironment API with constructor inference, self/this/super resolution#274
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@claude try to find uncovered or suspicious places in the code that doesn't/won't work for constructor's type resolution. |
|
Claude finished @magyargergo's task in 3m 42s —— View job Constructor Type Resolution — Gap Analysis
Good work overall. Here are the uncovered patterns and bugs I found, roughly ordered by severity: 🔴 Bug: Rust
|
| 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 |
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 1929 tests passed 11 test(s) skipped — expand for detailsIntegration:
Code CoverageCombined (Unit + Integration)
Coverage breakdown by test suiteUnit Tests
Integration Tests
📋 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
|
@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 finished @magyargergo's task in 5m 14s —— View job Constructor Type Resolution — Comprehensive Gap Analysis (Round 2)
|
| Previous finding | Status | How it was fixed |
|---|---|---|
🔴 Rust: Tier 1 overwrites Tier 0 (let x: Base = Derived::new()) |
✅ Fixed | rust.ts:36 — if (node.childForFieldName('type') !== null) return; guard added |
🔴 let mut x = User::new() never inferred |
✅ Fixed | shared.ts:83-87 — mut_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.
🟠 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.
🟡 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.
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.
|
@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 finished @magyargergo's task in 4m 21s —— View job Constructor Type Resolution — Gap Analysis (Round 3)
|
| 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-388 — receiverKey(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_typePattern: 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 nodePattern: 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' → missedAfter 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.
Review ResponseFixed in this PR:
Integration tests added for each fix:
Unit tests added (4 tests): TS as-cast, TS non-null, C++ Acknowledged as known limitations — deferred to a follow-up PR:
|
…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{})
|
@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 finished @magyargergo's task in 5m 10s —— View job Constructor Type Resolution — Gap Analysis (Round 4)
|
| 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{} // ← missedThe 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.
🔴 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; // sameAfter 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;
}🟡 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 — missedThe 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.
🟡 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 pathIn 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 (if → while) 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.
Review Response (Round 4)Fixed:
Pushback:
Integration tests added:
Unit tests added (3 tests): Go |
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)
|
@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 finished @magyargergo's task in 4m 35s —— View job Constructor Type Resolution — Gap Analysis (Round 5)
|
| 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:54 — if 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.
🟠 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.
🟠 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 droppedIn 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.User → User 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.
🟡 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).
🟡 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
Round 5 ResponseAll 4 actionable findings fixed in
Tests added:
Deferred (cumulative across rounds):
|
|
@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 finished @magyargergo's task in 4m 11s —— View job Constructor Type Resolution — Gap Analysis (Round 6)
What Was Fixed Since Round 5All four actionable findings from Round 5 are addressed:
New Bugs Found🔴 Bug: Rust
|
| 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
Round 6 ResponseAll 4 actionable findings fixed in
Tests added:
Architectural Note: Chained Property Access ResolutionThe PHP property promotion fix enables What happens today:
What's needed for full chained resolution:
This affects all Deferred (cumulative across rounds):
|
…, 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
Type Resolution Gap Closure — Phase 1 + Phase 2 CompleteAfter 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
|
| 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)
… 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

Summary
buildTypeEnv()returns aTypeEnvironmentwith.lookup()method, replacing the looselookupTypeEnv(env, ...)function + raw Map. Single-pass AST walk merges type binding extraction and constructor binding scanning into one traversal.new User(),User(),User.new,User::new())self/this/$thisto the enclosing class name via AST walk for receiver-type filteringsuper/base/parentto the parent class name by extracting heritage from 8 tree-sitter grammar variants (TS/JS, Java, Python, Ruby, C#, PHP, Kotlin, C++, Swift)resolveCallTargetwidens to global candidates for super calls where same-file tier gives the wrong methodReadonlySet<string>facade with minimal{ has(name: string): boolean }interface + memoizationTypeEnvironment API
Constructor inference — languages covered
new_expressionconst x = new User()var+object_creation_expressionvar x = new User()::new()/::default()let x = User::new()object_creation_expression$x = new User()auto+new_expressionor class-verified callauto x = User()val+call_expression(SymbolTable-verified)val x = User()assignment+call(SymbolTable-verified)user = User("alice")property_declaration+call_expressionlet x = User(name: "a")assignment+callwith.newreceiveruser = User.newReceiver keyword resolution
thisself$thissuperbaseparentKey design decisions
classNames.has()to verify identifiers are known classes, not a PascalCase heuristicfuncName\0varNameto prevent collisions between same-named locals in different functionssymbols.lookupFuzzy()directly instead of undocumented empty-string convention for global resolutionTest plan
tsc --noEmit)Post-Deploy Monitoring & Validation
No additional operational monitoring required: purely in-worker type inference enrichment with no API, schema, or runtime changes.