Skip to content

feat: Phase 5 type resolution — chained calls, pattern matching, class-as-receiver#315

Merged
magyargergo merged 4 commits intomainfrom
feat/type-resolution-phase5
Mar 16, 2026
Merged

feat: Phase 5 type resolution — chained calls, pattern matching, class-as-receiver#315
magyargergo merged 4 commits intomainfrom
feat/type-resolution-phase5

Conversation

@magyargergo
Copy link
Collaborator

Summary

Phase 5 extends the type resolution system with four major capabilities:

  • Chained method call resolutionsvc.getUser().save() now resolves by walking the chain and looking up intermediate return types from the SymbolTable. Depth-capped at 3 to prevent fluent API explosion. Both sequential and worker paths supported via receiverCallChain on ExtractedCall.

  • Pattern matching binding extractors — Rust if let Some(user) = opt and if let Ok(user) = result extract typed bindings via extractGenericTypeArgs. Java 16+ instanceof pattern variables (obj instanceof User user) also supported. Infrastructure: PatternBindingExtractor type on LanguageTypeConfig + declarationTypeNodes map for generic type arg access.

  • Class-as-receiver for static method callsUserService.find_user() now resolves because the pipeline recognizes UserService as a class name via ctx.resolve() tiered lookup. Works across all languages. Fixes Python: context() and impact() return empty for classes that use static methods #289.

  • ReceiverKey collision fix — overloaded methods with the same name (e.g., User.save() and Repo.save()) no longer collide in the receiver map. receiverKey preserves @startIndex.

  • Python standalone type annotationsname: str (without default value) now captured by TypeEnv.

Code review fixes applied

  • Extracted CALL_EXPRESSION_TYPES + extractCallChain to utils.ts (eliminated duplication between call-processor and parse-worker)
  • Converted resolveChainedReceiver from recursion to loop
  • Added depth cap to extractReturnTypeName (defense-in-depth)
  • Replaced lookupFuzzy with ctx.resolve for class-as-receiver (architecturally consistent)

Test plan

  • 275 unit tests passing (type-env + call-processor)
  • 104 TypeScript integration tests passing
  • All Java, Rust, C#, Python integration tests passing
  • 6 new test fixtures with disambiguation proofs and negative assertions
  • 12+ new unit tests for pattern matching, chain resolution, receiverKey
  • 7 new integration test suites
  • tsc --noEmit clean
  • PHP repro from Python: context() and impact() return empty for classes that use static methods #289 verified: Foo::create() + $foo->greet() both resolve correctly

Closes

Deferred to Phase 6

…s-as-receiver, code review fixes

Phase 5.1: Chained method call resolution (depth-capped at 3)
- resolveChainedReceiver() resolves a.getUser().save() by walking the chain
  and looking up intermediate return types from the SymbolTable
- extractReceiverNode() + extractCallChain() shared in utils.ts
- receiverCallChain on ExtractedCall for worker path parity
- MAX_CHAIN_DEPTH=3 enforced in both extraction and resolution

Phase 5.2: Pattern matching binding extractors
- PatternBindingExtractor type added to LanguageTypeConfig
- declarationTypeNodes map tracks original type AST nodes for generic unwrapping
- Rust: if let Some(x)/Ok(x) unwrapping with extractGenericTypeArgs
- Java: instanceof pattern variables (Java 16+)
- C#: is-pattern disambiguation fixture (already working via extractDeclaration)

Phase 5.5d: Python standalone type annotations (name: str)
- expression_statement with type child now captured in DECLARATION_NODE_TYPES

Phase 5.5e: ReceiverKey collision fix for overloaded methods
- receiverKey preserves @StartIndex to prevent same-name method collisions
- lookupReceiverType does prefix scan with ambiguity refusal

Class-as-receiver for static method calls (#289)
- UserService.find_user() now resolves via ctx.resolve() tiered lookup
- Respects import scoping — no false positives from unrelated packages

Code review fixes:
- Extracted CALL_EXPRESSION_TYPES + extractCallChain to utils.ts (eliminated duplication)
- Converted resolveChainedReceiver from recursion to loop (no exposed depth param)
- Added depth cap to extractReturnTypeName (defense against nested wrapper types)
- Replaced lookupFuzzy with ctx.resolve for class-as-receiver (architecturally consistent)

Closes #289

Test coverage: 6 new fixtures, 12+ new unit tests, 7 new integration test suites
@vercel
Copy link

vercel bot commented Mar 16, 2026

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

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

Request Review

@magyargergo
Copy link
Collaborator Author

Phase 5 Type Resolution — Implementation Notes

What's new

Chained method calls (svc.getUser().save()): The pipeline now walks call-expression receivers recursively (depth-capped at 3) to resolve intermediate return types. Both the sequential path (processCalls) and the worker path (processCallsFromExtracted) are supported — workers serialize the chain as receiverCallChain: string[] on ExtractedCall.

Pattern matching (Rust if let Some(x), Java instanceof User user, C# is User user): New PatternBindingExtractor hook on LanguageTypeConfig. A declarationTypeNodes map in buildTypeEnv preserves the original type annotation AST nodes so extractors can call extractGenericTypeArgs to unwrap Option<User>User or Result<T, E>T.

Class-as-receiver (UserService.find_user()): When the receiver name isn't a variable in TypeEnv but resolves to a Class/Struct/Interface via ctx.resolve(), we use it directly as the receiver type. This enables disambiguation of static method calls across all languages. Uses the standard tiered resolution (same-file > import-scoped > global) — no raw lookupFuzzy.

Architecture decisions

  1. Chain extraction (CALL_EXPRESSION_TYPES, MAX_CHAIN_DEPTH, extractCallChain) lives in utils.ts — shared between call-processor and parse-worker with zero duplication.
  2. resolveChainedReceiver is a simple for loop over chain names (not recursive), so no exposed depth parameter.
  3. extractReturnTypeName now has a depth cap of 10 for recursive wrapper unwrapping (defense-in-depth against crafted nested generics).
  4. declarationTypeNodes is a local variable in buildTypeEnv, never exposed on the TypeEnvironment interface — no AST retention risk.

Related issues

@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

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 1425 1363 0 0 8s
Integration 1012 988 0 17 62s
Total 2437 2351 0 17 70s

✅ All 2351 tests passed

17 test(s) skipped — expand for details

Integration:

  • ignore + language-skip E2E > file discovery (walkRepositoryPaths) > includes source files from src/
  • ignore + language-skip E2E > file discovery (walkRepositoryPaths) > includes .swift files (discovery does not filter by language)
  • ignore + language-skip E2E > file discovery (walkRepositoryPaths) > excludes gitignored directories (data/)
  • ignore + language-skip E2E > file discovery (walkRepositoryPaths) > excludes gitignored file patterns (*.log)
  • ignore + language-skip E2E > file discovery (walkRepositoryPaths) > excludes gitnexusignored directories (vendor/)
  • ignore + language-skip E2E > parsing (processParsing) > parses TypeScript files into graph nodes and skips Swift gracefully
  • 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
  • Swift return type inference > detects User class and getUser function
  • Swift return type inference > detects save function on User (Swift class methods are Function nodes)
  • Swift return type inference > resolves user.save() to User#save via return type of getUser() -> User
  • Swift return-type inference via function return type > resolves user.save() to User#save via return type of getUser()
  • Swift return-type inference via function return type > user.save() does NOT resolve to Repo#save
  • Swift return-type inference via function return type > resolves repo.save() to Repo#save via return type of getRepo()

Code Coverage

Combined (Unit + Integration)

Metric Coverage Covered Base Delta Status
Statements 53.46% 5266/9849 30.8% 📈 +22.7 🟢 ██████████░░░░░░░░░░
Branches 46.69% 3397/7275 24.91% 📈 +21.8 🟢 █████████░░░░░░░░░░░
Functions 54.59% 511/936 34.66% 📈 +19.9 🟢 ██████████░░░░░░░░░░
Lines 55.09% 4768/8654 32.84% 📈 +22.2 🟢 ███████████░░░░░░░░░
Coverage breakdown by test suite

Unit Tests

Metric Coverage Covered Base Delta Status
Statements 43.56% 4291/9849 30.8% 📈 +12.8 🟢 ████████░░░░░░░░░░░░
Branches 39.17% 2850/7275 24.91% 📈 +14.3 🟢 ███████░░░░░░░░░░░░░
Functions 43.37% 406/936 34.66% 📈 +8.7 🟢 ████████░░░░░░░░░░░░
Lines 44.8% 3877/8654 32.84% 📈 +12.0 🟢 ████████░░░░░░░░░░░░

Integration Tests

Metric Coverage Covered Base Delta Status
Statements 23.5% 2315/9849 30.8% 📉 -7.3 🔴 ████░░░░░░░░░░░░░░░░
Branches 17.67% 1286/7275 24.91% 📉 -7.2 🔴 ███░░░░░░░░░░░░░░░░░
Functions 27.02% 253/936 34.66% 📉 -7.6 🔴 █████░░░░░░░░░░░░░░░
Lines 24.8% 2147/8654 32.84% 📉 -8.0 🔴 ████░░░░░░░░░░░░░░░░

📋 View full run · Generated by CI

@magyargergo
Copy link
Collaborator Author

@claude ultra-think while identifying per-language gaps for this pull request and verify those findings. Act as a senior compiler font-end engineer and review this change from that perpective and architevtural fit.

@claude
Copy link

claude bot commented Mar 16, 2026

Claude finished @magyargergo's task in 6m 56s —— View job


Phase 5 Type Resolution — Compiler Front-End Review

Tasks

  • Initial setup
  • Read core source files (call-processor, type-env, utils, parse-worker)
  • Read language extractors (jvm, rust, python, csharp, go, shared)
  • Analyze per-language gaps
  • Write full review

Overall Assessment

This is a well-structured pass over a genuinely hard problem — heuristic type inference without a real type system. The architecture choices are sound (tiered resolution, scope-aware TypeEnv, depth-capped chain walking), the code review fixes were applied, and the test matrix is solid for the languages covered. Below are the findings by feature area, with language-specific gaps verified against the actual AST field names in each tree-sitter grammar.


1. Chained Call Resolution

extractCallChain (utils.ts:1026)

The loop builds chain using unshift, which correctly produces ['b', 'c'] for a.b().c().d() — innermost intermediate first. The ordering is correct: resolveChainedReceiver iterates left-to-right, resolving b with the base receiver type first, then c with b()'s return type.

Verified language coverage via field names:

Language Call node Method name field Receiver field Status
TS/JS call_expression function.lastNamedChild function.object ✅ works
Java method_invocation function.lastNamedChild special-cased callNode.object ✅ works
C# invocation_expression function.lastNamedChild function.expression ✅ covered by fallback
Go call_expression function.lastNamedChild function.operand ✅ covered by fallback
Python call function.lastNamedChild (attribute) function.object ✅ covered
PHP member_call_expression function.name special-cased callNode.object ✅ works
Ruby call method field (not function/name) receiver gap
Kotlin call_expression function.lastNamedChild (navigation_suffix) function.value ✅ covered by fallback
Swift call_expression called_value (not function) gap (tests already skipped)

Ruby gap (extractCallChain, utils.ts:1034): Ruby's call node exposes the method name via the method field, not function or name. The current code:

const funcNode = current.childForFieldName?.('function')
  ?? current.childForFieldName?.('name');

returns null for Ruby, causing the entire chain extraction to silently return undefined. No Ruby chain tests exist to catch this. Since extractCallChain is a best-effort optimization, this fails silently (no incorrect edges), but Ruby chained method calls are never resolved. Fix this →

resolveChainedReceiver still calls lookupFuzzy (call-processor.ts:418):

const candidates = ctx.symbols.lookupFuzzy(name);
const symDef = candidates.find(c => c.nodeId === resolved.nodeId);

This is a secondary lookup by nodeId (to retrieve returnType after resolveCallTarget finds the target). The PR notes that class-as-receiver "replaced lookupFuzzy with ctx.resolve", but this lookupFuzzy call remains. It's architecturally different from name resolution (it's a nodeId scan), but it means resolveCallTarget + a second lookupFuzzy scan for every chain link. Consider returning SymbolDefinition from resolveCallTarget or adding a lookupById API to SymbolTable — this is latent tech debt.

Missing integration tests for: Go, Python, PHP, C# chained calls. The implementation logic covers them (verified above) but they are untested.


2. Pattern Matching Extractors

Rust if let Some(x) = opt (rust.ts:255–258):
Elegantly correct. Option<T> is unwrapped to T in extractSimpleTypeName via NULLABLE_WRAPPER_TYPES, so scopeEnv.get('opt') already returns T directly. No extra work needed.

Rust if let Ok(x) = res (rust.ts:261–267):
The declarationTypeNodes approach is sound. Result is NOT in NULLABLE_WRAPPER_TYPES, so scopeEnv stores 'Result' for the variable; the original Result<T,E> AST node is preserved separately and extractGenericTypeArgs unwraps it at match time.

Gap — Err(x) not handled (rust.ts:231):

if (wrapperName !== 'Some' && wrapperName !== 'Ok') return undefined;

if let Err(e) = result is a ubiquitous Rust error-handling pattern. Typing e would require extracting the second generic arg (typeArgs[1]) from Result<T, E>. This is conservative but a meaningful gap for Rust code that propagates errors. Fix this →

Java instanceof (jvm.ts:132–142):
Correct. Handles Java 16+ pattern variables. The name field absence check properly skips plain instanceof without a binding. One subtle point: the pattern variable's scope per JLS is the if-block, but TypeEnv only models function scope — this is a known limitation of the single-scope-per-function model.

C# is_pattern_expression (csharp.ts:14–27):
Handled in extractDeclaration rather than extractPatternBinding (it's in DECLARATION_NODE_TYPES). This works but it means declarationTypeNodes won't be populated for is-pattern variables since the guard node.childForFieldName('type') returns null for is_pattern_expression (the type is on the nested declaration_pattern). Not a bug today (C# doesn't have a PatternBindingExtractor), but worth noting if C# pattern extraction is extended.

Gap — C# switch expression patterns:
C# 8+ switch expressions: x switch { User u => u.Save() } introduce u: User via a case_pattern_switch_labeldeclaration_pattern. This is not in DECLARATION_NODE_TYPES and no PatternBindingExtractor exists for C#. Patterns inside switch arms are relatively common in modern C#.

PatternBindingExtractor called on every AST node:
The hook is invoked unconditionally for every node in files for languages that register it (Rust, Java). The early-exit guards are O(1), but there's function call overhead per node for every Rust and Java file. For files with thousands of nodes, this is noticeable. Consider adding a patternBindingNodeTypes: ReadonlySet<string> filter to LanguageTypeConfig (parallel to declarationNodeTypes) so the walk can gate the call: if (config.patternBindingNodeTypes?.has(node.type)).


3. Class-as-Receiver for Static Calls

Implementation (call-processor.ts:265–272):

const typeResolved = ctx.resolve(receiverName, file.path);
if (typeResolved && typeResolved.candidates.some(
  d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct',
)) {
  receiverTypeName = receiverName;
}

Correct design — tiered resolution, no raw fuzzy lookup.

Gap — Enum type missing:
The candidate type check doesn't include 'Enum'. Enum static method calls are common in all target languages: Java Status.fromCode(404), C# Enum.Parse(...), Kotlin Color.values(). The ctx.resolve call would find the enum's symbol, but the .some(d => ...) filter would reject it. Fix this →

Asymmetry between sequential and worker path:
Both paths implement the class-as-receiver check — good. However, the sequential path (call-processor.ts:265) applies it only when callForm === 'member', while the worker path (call-processor.ts:753) checks effectiveCall.callForm === 'member' — consistent.


4. ReceiverKey Collision Fix

Correctness:
receiverKey(scope, varName) uses "funcName@startIndex\0varName" — correct. The @startIndex distinguishes User.save@100 from Repo.save@200. The \0 separator is safe since no language allows NUL in identifiers.

lookupReceiverType O(n) scan (call-processor.ts:673–690):
The lookup iterates all entries in verifiedReceivers to find those matching prefix + suffix. For typical files this is fine, but a class file with many overloaded methods and many local variables could produce a large map. A secondary Map<string, Map<string, string>> keyed by funcName then varName would make this O(1) at the cost of memory. This is acceptable as-is for now.


5. Python Standalone Annotations

expression_statement added to DECLARATION_NODE_TYPES is safe: the extractor immediately bails out unless the first named child is a type node. extractInitializer, scanConstructorBinding, and extractPendingAssignment all return early for expression_statement. No false positives.

One nuance: Python 3.10+ match/case statements introduce bound variables (e.g. case Point(x, y):). These are match_statementcase_clausecase_pattern. No PatternBindingExtractor exists for Python, so these are untyped — consistent with the conservative approach.


6. Architectural Observations from a Compiler FE Perspective

Single-pass, no fixpoint:
The Tier 2 assignment chain propagation is a single left-to-right pass. b = a; c = b types c correctly; c = b; b = a (reverse source order) does not. This is documented and acceptable — a true dataflow analysis would require a worklist algorithm. Worth flagging in docs as a known limitation.

declarationTypeNodes coupling:
The map is populated for all declarations with a type field, whether or not pattern matching will use it. This is a pre-materialization cost paid unconditionally. Since the map is discarded after buildTypeEnv, memory impact is bounded per-file. But the PatternBindingExtractor interface forces all extractors to accept declarationTypeNodes even when they don't use it (Java's instanceof, C#'s is-pattern). Consider splitting into a separate opt-in parameter once more extractors adopt it.

extractCallChain unshift complexity:
Array.prototype.unshift is O(n) per call. For a depth-3 chain, this is 3 shifts of arrays of length ≤ 3 — negligible. But the comment "build chain outermost-last" is precise: the chain is [innermost_intermediate, ..., outermost_intermediate], which is the correct iteration order for resolveChainedReceiver.

Worker/sequential parity:
The two code paths (sequential processCalls and worker processCallsFromExtracted) implement the same logic but via different mechanisms. The sequential path does AST extraction inline; the worker serializes to receiverCallChain: string[]. This creates a maintenance surface: any new fallback added to the sequential path must be mirrored in the worker extraction AND in the processCallsFromExtracted step chain. Phase 5 correctly updates both, but this dual-maintenance pattern is a long-term risk.


7. Per-Language Gap Summary

Language Chain Calls Pattern Matching Class-as-Receiver Annotations
TypeScript ✅ tested (TS structural — not applicable) ✅ tested ✅ existing
Java ✅ tested ✅ instanceof tested
Rust ✅ (unit) ✅ Some/Ok tested; Err❌
C# ✅ (unit) ✅ is-pattern; switch expr ❌
Python ✅ (unit only) (walrus ✅; match/case ❌) new
Go ✅ (unit only) n/a n/a
PHP ✅ (unit only) n/a ✅ repro verified n/a
Ruby ❌ silent fail n/a n/a
Kotlin ✅ (unit) n/a n/a
Swift ❌ skipped n/a n/a

Summary

Approve with notes. The core features are correct and well-tested for the primary languages (TS, Java, Rust, C#). The declarationTypeNodes mechanism is elegant for the Ok(x) case without retaining AST nodes across the file lifetime. The class-as-receiver fix uses the right abstraction level (ctx.resolve vs raw lookupFuzzy).

Must-fix before merge:

  • None blocking. All test suites pass.

Recommended follow-ups (Phase 6 candidates):

  1. Add Err(x) to Rust pattern extractor (2-line addition)
  2. Add Enum to class-as-receiver type checks in both paths (2 places)
  3. Add a method field fallback to extractCallChain for Ruby
  4. Integration tests for Go/Python/PHP/C# chain calls
  5. Consider patternBindingNodeTypes gate to avoid per-node function call overhead
    · Branch

@magyargergo
Copy link
Collaborator Author

Review Response — Compiler FE Review Findings

Verified findings against the codebase. Three implemented, four deferred.

Implemented

1. Ruby method/receiver field gap (utils.ts:1034) — Ruby's call node uses method (not function/name) and receiver (not object/value). Added both fallbacks. Safe for Python (also call type) — Python matches function first.

2. Rust Err(x) pattern (rust.ts:231) — Mirror of Ok(x): same declarationTypeNodes lookup, typeArgs[1] instead of typeArgs[0]. Added Err to allowed wrappers with parameterized index.

3. Enum in class-as-receiver (call-processor.ts:268, 756)Enum is a first-class graph type. Added to both sequential and worker paths.

Deferred (valid, different scope)

  • lookupFuzzy in resolveChainedReceiver — nodeId scan after resolution, not name resolution. Better fix: return SymbolDefinition from resolveCallTarget (refactor).
  • Integration tests for Go/Python/PHP/C# chains — implementation verified by reviewer, fixture creation is scope creep.
  • patternBindingNodeTypes gate — O(1) early exits; premature optimization.
  • C# switch expr, Python match/case — Phase 6 per plan.

All tests green. Build clean.

Address three per-language gaps identified in Phase 5 code review:

- Ruby: add `method`/`receiver` field fallbacks to extractCallChain
  (tree-sitter-ruby uses different field names than other grammars)
- Rust: handle `Err(e)` pattern binding via typeArgs[1] from Result<T,E>
- Enum: include Enum type in class-as-receiver filter (both paths)

Integration tests added for all three fixes.
@magyargergo
Copy link
Collaborator Author

@claude ultra-think while identifying per-language gaps for this pull request and verify those findings. Act as a senior compiler font-end engineer and review this change from that perpective and architectural fit. Also check out the previous comments.

@claude
Copy link

claude bot commented Mar 16, 2026

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


Phase 5 Type Resolution — Deep Compiler FE Review (Follow-up)

Tasks

  • Initial setup
  • Read core source files (utils.ts, call-processor.ts, type-env.ts)
  • Read type extractors (rust, jvm, python, csharp, go, ruby, kotlin)
  • Read parse-worker.ts and types.ts
  • Verify the 3 claimed fixes (Ruby method field, Rust Err(x), Enum)
  • Deep per-language gap analysis beyond previous review
  • Architectural fit assessment
  • Write full review

Deep Compiler FE Review

This is the follow-up pass, building on the previous review and verifying all claimed fixes. All source files were read directly.


Verification of the 3 Claimed Fixes

1. Ruby method/receiver fields — ✅ Correct

utils.ts:1036:

const funcNode = current.childForFieldName?.('function')
  ?? current.childForFieldName?.('name')
  ?? current.childForFieldName?.('method');  // Ruby `call` node

utils.ts:1062-1064:

if (!innerReceiver && current.type === 'call') {
  innerReceiver = current.childForFieldName?.('receiver');
}

The two-pronged fix is correct. For Ruby's call node: method extracts the method name; receiver on the call node itself extracts the receiver (since Ruby doesn't nest it inside function). The Python call node is unaffected — Python uses functionattributeobject, so innerReceiver is already set before the Ruby fallback fires. Integration test at ruby.test.ts:765 verifies end-to-end.

2. Rust Err(x) pattern — ✅ Correct

rust.ts:231:

if (wrapperName !== 'Some' && wrapperName !== 'Ok' && wrapperName !== 'Err') return undefined;

rust.ts:267:

const argIndex = wrapperName === 'Err' ? 1 : 0;

Extraction of E from Result<T, E> via typeArgs[1] is the right index. The declarationTypeNodes path is already in place for Ok/Result — adding Err to it is a clean one-liner. A rust-err-unwrap fixture was added.

3. Enum in class-as-receiver — ✅ Correct, both paths

call-processor.ts:268 (sequential) and call-processor.ts:756 (worker) both now include d.type === 'Enum'. The java-enum-static-call fixture was added. Both paths are symmetrically updated.


New Findings Not in Previous Review

These are issues discovered in this review pass by tracing execution paths for edge cases not covered by the existing test matrix.

Finding 1 — HIGH: Worker/serial asymmetry in chain base-type resolution

Location: parse-worker.ts:1021-1024 vs call-processor.ts:284-285

The serial path resolves the chain base receiver through typeEnv at line 284:

let baseType = extracted.baseReceiverName && typeEnv
  ? typeEnv.lookup(extracted.baseReceiverName, callNode)
  : undefined;

The worker path has a comment describing the same intent (// Also try the type environment immediately (covers explicitly-typed locals)) but does not implement it. After setting receiverName = extracted.baseReceiverName at line 1020, receiverTypeName is never re-computed from the typeEnv:

receiverName = extracted.baseReceiverName;
// receiverTypeName NOT updated — base receiver type not passed to processCallsFromExtracted

Consequence: For chained calls where the base receiver is an explicitly-typed parameter or annotation-declared variable (fn process(svc: &UserService), def process(svc: UserService), fun process(svc: UserService)), the worker path never resolves the base type. In processCallsFromExtracted:

  • Step 1 (lookupReceiverType from constructor bindings) won't find parameter types — only var x = Constructor() bindings are in the map.
  • Step 1b (class-as-receiver via ctx.resolve) only fires for class names in the SymbolTable, not for local variable names like svc.
  • Step 2 receives baseType = undefined → chain resolution fails.

The serial path works because it queries typeEnv directly. This asymmetry means the feature works on small or sequential-processed codebases but silently degrades on worker-processed ones.

Minimal fix: In parse-worker.ts, after line 1020 add:

receiverTypeName = typeEnv.lookup(receiverName, callNode);

This mirrors what the serial path does at line 284. Fix this →


Finding 2 — MEDIUM: Sequential path misses class-as-receiver for chain base

Location: call-processor.ts:281-289

The sequential path's chain branch:

let baseType = extracted.baseReceiverName && typeEnv
  ? typeEnv.lookup(extracted.baseReceiverName, callNode)
  : undefined;
if (!baseType && extracted.baseReceiverName && verifiedReceivers.size > 0) {
  baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName);
}
// MISSING: ctx.resolve fallback for class names
receiverTypeName = resolveChainedReceiver(extracted.chain, baseType, file.path, ctx);

If the chain base is a class name directly — e.g., UserService.find_user().save() rather than svc.find_user().save()typeEnv.lookup('UserService') returns undefined (classes are not variables) and verifiedReceivers also has nothing. resolveChainedReceiver receives baseType = undefined.

The worker path handles this correctly because Step 1b (class-as-receiver) runs before Step 2 and sees receiverName = 'UserService' → sets receiverTypeName = 'UserService' → Step 2 uses it as baseType. The serial path lacks the equivalent check.

Fix: After the verifiedReceivers lookup at line 288, add:

if (!baseType && extracted.baseReceiverName) {
  const cr = ctx.resolve(extracted.baseReceiverName, file.path);
  if (cr?.candidates.some(d => 
    d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum'
  )) {
    baseType = extracted.baseReceiverName;
  }
}

Fix this →


Finding 3 — LOW: Misleading comment at parse-worker.ts:1021-1024

The comment // Also try the type environment immediately (covers explicitly-typed locals) describes an action that is NOT performed. The comment was left when the code was restructured. It should either be removed or the implementation added (which is Finding 1's fix). As-is, it actively misleads future maintainers into thinking typeEnv was consulted.


Finding 4 — OBSERVATION: createClassNameLookup checks only 'Class', not 'Enum'/'Struct'

Location: type-env.ts:268

const result = symbolTable.lookupFuzzy(name).some(def => def.type === 'Class');

Now that class-as-receiver correctly includes 'Enum' (this PR's fix), there's a minor inconsistency: createClassNameLookup (used by extractInitializer to distinguish constructor calls from function calls in languages like Kotlin/Python) still only considers 'Class'. An enum called as a constructor (e.g., a Java enum with a companion factory) would not be recognized. This is pre-existing behavior and low risk, but worth noting alongside the Enum fix.


Per-Language Gap Table (Updated)

Language Chain Calls Pattern Matching Class-as-Receiver Notes
TypeScript ✅ tested (structural) ✅ tested
Java ✅ tested ✅ instanceof ✅ + Enum ✅ switch-expr patterns deferred
Rust ✅ tested ✅ Some/Ok/Err ✅
C# ✅ unit ✅ is-pattern switch-expr deferred
Python ✅ unit walrus ✅; match/case deferred standalone annotations ✅ new
Go ✅ unit n/a
PHP ✅ unit n/a ✅ repro verified
Ruby integration added n/a method/receiver fields fixed ✅
Kotlin ✅ unit n/a when-expression patterns deferred
Swift ❌ skipped n/a all 17 tests remain skipped
All langs ⚠️ worker path drops typeEnv for annotated base receivers Finding 1

Architectural Fit Assessment

PatternBindingExtractor contract: The interface signature at types.ts:51-56 requires declarationTypeNodes as a positional parameter regardless of whether the language uses it. Java's extractJavaPatternBinding only uses node and ignores the other three parameters. A future Kotlin when-expression extractor would also ignore declarationTypeNodes. Consider making declarationTypeNodes optional in the type signature (with a default empty map at the call site). Current approach forces all future implementors to accept a parameter they won't use.

resolveChainedReceiver's lookupFuzzy secondary scan: As noted in the previous review, the lookupFuzzy(name) call at line 418 is a nodeId-scan idiom to retrieve SymbolDefinition.returnType after resolveCallTarget finds the target. resolveCallTarget returns a ResolveResult (nodeId + confidence) but not the SymbolDefinition. The clean fix is to return SymbolDefinition | null from resolveCallTarget directly — but that's a refactor touching the type signature. As deferred tech debt, it's acceptable but it means every chain step makes two O(n) scans over the symbol table.

Dual-path maintenance: Finding 1 shows this risk is already materializing — the serial and worker paths diverged on the typeEnv lookup for chain base receivers. Given that these two paths implement the same logic via different mechanisms, consider adding an integration test that specifically exercises the worker path (large enough repo to trigger the worker) for chained calls on typed parameters. The current ruby-chain-call fixture uses constructor binding (svc = UserService.new), which works in both paths.


Summary

Approve — the three claimed fixes are correctly implemented and the CI is green. Two new gaps were found in the chain resolution paths that are testable but not yet tested:

  1. Finding 1 (worker path, typed parameter chain base) — recommend fixing before merge. It's a 1-line addition in parse-worker.ts that mirrors the existing serial-path logic.
  2. Finding 2 (serial path, class-name chain base) — recommend fixing before merge. It's a 5-line addition mirroring Step 1b in the worker path.
  3. Finding 3 (misleading comment) — minor, fix whenever.
    · Branch

#315)

- Worker path: add typeEnv.lookup for chain base receiver after extraction
  (typed parameters like `fn process(svc: &UserService)` were silently lost)
- Serial path: add ctx.resolve class-as-receiver fallback for chain base
  (class-name chains like `UserService.find_user().save()` failed)
- Fix misleading comment in parse-worker.ts that described unimplemented logic
- Integration tests: typed-parameter chain, static class-name chain
@magyargergo
Copy link
Collaborator Author

@claude ultra-think while identifying per-language gaps for this pull request and verify those findings. Act as a senior compiler font-end engineer and review this change from that perpective and architectural fit. Also check out the previous comments.

@claude
Copy link

claude bot commented Mar 16, 2026

Claude finished @magyargergo's task in 8m 43s —— View job


Phase 5 Type Resolution — Third Compiler FE Review

Tasks

  • Initial setup
  • Read core files (parse-worker, call-processor, utils, type-env)
  • Read language extractors (rust, jvm, python, csharp, ruby, kotlin, go)
  • Check recent commits after the two prior reviews
  • Verify all prior findings against current codebase
  • Deep per-language gap analysis
  • Write full review

Verification of Prior Review Findings

Three commits have landed since the initial PR:

  • e01ad56 — base Phase 5 implementation
  • 1ab0718 — Round 1 fixes: Ruby method/receiver fields, Rust Err(x), Enum type check
  • c8f8584 — Round 2 fixes: worker typeEnv chain base, serial class-as-receiver chain base, misleading comment

Round 1 findings — all ✅ verified correct:

  • utils.ts:1036: ?? current.childForFieldName?.('method') added for Ruby ✓
  • utils.ts:1062-1064: if (!innerReceiver && current.type === 'call') { innerReceiver = current.childForFieldName?.('receiver'); } added ✓
  • rust.ts:231: wrapperName !== 'Err' guard removed; argIndex = wrapperName === 'Err' ? 1 : 0
  • call-processor.ts:268,756: d.type === 'Enum' added to both paths ✓

Round 2 findings — all ✅ verified or correctly deferred:

  • Finding 1 (worker typeEnv gap): parse-worker.ts:1024-1026 now does receiverTypeName = typeEnv.lookup(receiverName, callNode) after setting the base receiver name ✓
  • Finding 2 (serial path class-as-receiver for chain base): call-processor.ts:289-297 adds ctx.resolve check with Enum included ✓
  • Finding 3 (misleading comment): parse-worker.ts:1021-1026 now has an accurate comment describing the actual implementation ✓
  • Finding 4 (createClassNameLookup): Still only checks 'Class' at type-env.ts:268 — intentionally deferred

New Findings

The two prior reviews verified correctness of the new features and caught implementation gaps. This pass looks further: verifying the chain extraction layer for every language that uses call_expression, tracing through the AST field contracts for Kotlin, and examining test-coverage blind spots.

Finding 1 — HIGH: Kotlin chain call extraction is silently broken

Location: utils.ts:1034-1041 (extractCallChain)

The function extracts intermediate method names by looking up named fields on the call node:

const funcNode = current.childForFieldName?.('function')
  ?? current.childForFieldName?.('name')
  ?? current.childForFieldName?.('method');

For Kotlin's call_expression, none of these fields exist. The tree-sitter-kotlin grammar exposes the callee as call_expression.firstNamedChild (either a simple_identifier for direct calls or a navigation_expression for qualified calls). There is no function/name/method named field. The evidence is in scanKotlinConstructorBinding (jvm.ts:256), which explicitly uses callExpr.firstNamedChild to access the callee — not a field lookup.

Execution trace for svc.getUser().save() in Kotlin:

  1. save's name node is inside navigation_suffix (.save)
  2. extractReceiverNode correctly returns the inner call_expression (svc.getUser())
  3. CALL_EXPRESSION_TYPES.has('call_expression') → true
  4. extractCallChain(inner_call_expression) is called
  5. funcNode = null on all three field lookups → methodName = undefined → break
  6. chain.length === 0 → returns undefined
  7. Kotlin chain call silently falls through as if no chain exists

Why tests don't catch this: The Kotlin "chain" integration tests (kotlin-assignment-chain, kotlin-class-method-chain) all test Tier 2 assignment chain propagation (val alias = user; alias.save()), not method-call chain resolution (svc.getUser().save()). The unit tests for processCallsFromExtracted use manually pre-baked receiverCallChain: ['getUser'] arrays, bypassing AST extraction entirely. No Kotlin kotlin-method-call-chain fixture exists.

Fix: In extractCallChain, after the three field lookups, add a Kotlin/Swift fallback:

// Kotlin/Swift: call_expression exposes callee as firstNamedChild (no 'function' field).
// navigation_expression: method name is in the lastNamedChild (navigation_suffix → simple_identifier).
if (!funcNode && current.type === 'call_expression') {
  const callee = current.firstNamedChild;
  if (callee?.type === 'navigation_expression') {
    const suffix = callee.lastNamedChild;
    if (suffix?.type === 'navigation_suffix') {
      methodName = suffix.lastNamedChild?.text;
      innerReceiver = callee.children.find(c => c.isNamed && c.type !== 'navigation_suffix') ?? null;
    }
  }
}

Fix this →


Finding 2 — MEDIUM: "chain" integration test coverage conflation (test quality)

All prior reviews described Kotlin chain call support as "✅ unit" or "✅ (unit only)". This assessment was based on unit tests that construct ExtractedCall objects with receiverCallChain already populated. Those tests verify that processCallsFromExtracted correctly walks a pre-extracted chain, not that extractCallChain correctly extracts the chain from AST. These are different invariants.

A comprehensive test matrix should include:

  • A Kotlin chain call integration test fixture (svc.getUser().save())
  • After Finding 1's fix, that fixture would also be the regression test

Similarly, the typescript-typed-param-chain and typescript-static-chain fixtures added in c8f8584 test the TypeScript sequential path. The worker path (large enough file set to trigger parallelism) for these patterns has no dedicated test. The parity fix is correct, but a dedicated worker-path test would guard against future divergence.


Finding 3 — LOW (observation): createClassNameLookup inconsistency persists

type-env.ts:268:

const result = symbolTable.lookupFuzzy(name).some(def => def.type === 'Class');

Now that Enum is a first-class type in all class-as-receiver checks (both sequential and worker paths, both chain-base and non-chain), createClassNameLookup is the only remaining place that still treats Enum and Struct as non-class types. This lookup is used by extractInitializer (Python, Kotlin) to distinguish constructor calls from function calls.

Impact: val status = Status.ACTIVE or val s = Status.values().first() — if Status is an Enum, extractInitializer won't recognize it as a class and won't bind status: Status. This is a pre-existing limitation, but it's now more visible after the Enum fix in the call-processor.

Fix this →


Per-Language Gap Table (Final State)

Language Chain Calls Pattern Matching Class-as-Receiver Annotations
TypeScript ✅ integration (structural) ✅ integration ✅ existing
Java ✅ integration ✅ instanceof integration ✅ + Enum ✅
Rust ✅ unit ✅ Some/Ok/Err ✅
C# ✅ unit ✅ is-pattern
Python ✅ unit walrus ✅ new
Go ✅ unit n/a n/a
PHP ✅ unit n/a ✅ repro verified n/a
Ruby integration added n/a n/a
Kotlin ⚠️ extraction broken (see Finding 1) n/a
Swift ❌ all tests skipped n/a n/a

Architectural Assessment

Chain resolution correctness: The dual-path architecture (serial AST + worker serialization) is now correctly symmetric after c8f8584. The comment fix in parse-worker.ts is particularly valuable — the previous comment was actively misleading. The ctx.resolve fallback for chain bases is consistent with the non-chain class-as-receiver path.

lookupFuzzy in resolveChainedReceiver: The secondary scan at call-processor.ts:427 was flagged in Round 2 as tech debt. Still present. Still a 2× symbol-table scan per chain step. The cleanest fix (returning SymbolDefinition from resolveCallTarget) remains deferred. Acceptable as-is.

PatternBindingExtractor signature overhead: The interface requires declarationTypeNodes as a positional parameter regardless of whether the language uses it. Java's extractJavaPatternBinding signature accepts all four params and ignores three. Low impact, but a future (node: SyntaxNode, options: PatternBindingOptions) refactor would be cleaner.

Depth-cap correctness: MAX_CHAIN_DEPTH = 3 is applied in extractCallChain. extractReturnTypeName has a depth cap of 10 for recursive unwrapping. These are independent guards with no interaction — correct.


Summary

Approve with two recommendations:

  1. Finding 1 (Kotlin chain extraction) — recommend fixing before merge. The 10-line fallback is well-scoped, follows the existing pattern in scanKotlinConstructorBinding, and would be accompanied by a kotlin-chain-call fixture. Without this, Kotlin users with fluent APIs get silent non-resolution with no error signal.

  2. Finding 3 (createClassNameLookup) — minor, can be follow-up. Two-character addition that makes the codebase internally consistent.

The three prior-review fixes are all correct. The dual-path parity work in c8f8584 is clean and architecturally sound. The test infrastructure (fixtures + integration suites) now covers the primary languages comprehensively.
· Branch

…315)

- Kotlin: extractCallChain now handles navigation_expression → navigation_suffix
  AST structure (Kotlin's call_expression has no 'function' field)
- createClassNameLookup: include Enum and Struct alongside Class for consistent
  constructor recognition in extractInitializer
- Integration test: kotlin-chain-call fixture verifying svc.getUser().save()
@magyargergo
Copy link
Collaborator Author

@claude ultra-think while identifying per-language gaps for this pull request and verify those findings. Act as a senior compiler font-end engineer and review this change from that perpective and architectural fit. Also check out the previous comments.

@claude
Copy link

claude bot commented Mar 16, 2026

Claude finished @magyargergo's task in 7m 25s —— View job


Phase 5 Type Resolution — Fourth Compiler FE Review

Tasks

  • Initial setup
  • Read core source files (utils.ts, call-processor.ts, type-env.ts, parse-worker.ts)
  • Read language extractors (jvm.ts, rust.ts, python.ts, csharp.ts, shared.ts)
  • Verify Round 3 fixes (Kotlin chain extraction, createClassNameLookup)
  • Deep per-language gap analysis beyond prior three reviews
  • Write full review

Verification of Round 3 Findings

Four commits have landed since the initial PR. The most recent (bd47b0c) implements both Round 3 fixes.

1. Kotlin chain extraction — ✅ Correct

utils.ts:1043-1061:

// Kotlin/Swift: call_expression exposes callee as firstNamedChild, not a field.
if (!funcNode && current.type === 'call_expression') {
  const callee = current.firstNamedChild;
  if (callee?.type === 'navigation_expression') {
    const suffix = callee.lastNamedChild;
    if (suffix?.type === 'navigation_suffix') {
      methodName = suffix.lastNamedChild?.text;
      for (let i = 0; i < callee.namedChildCount; i++) {
        const child = callee.namedChild(i);
        if (child && child.type !== 'navigation_suffix') {
          innerReceiver = child;
          break;
        }
      }
    }
  }
}

Execution trace for svc.getUser().save() in Kotlin:

  • extractCallChain is called with svc.getUser() (the receiver of save())
  • funcNode = null (Kotlin's call_expression has no function/name/method field) → Kotlin fallback fires
  • callee = navigation_expression(svc, navigation_suffix(.getUser))
  • suffix = navigation_suffix(.getUser)methodName = 'getUser'
  • Inner loop: simple_identifier(svc) is not navigation_suffixinnerReceiver = simple_identifier(svc)
  • CALL_EXPRESSION_TYPES.has('simple_identifier') → false → returns { chain: ['getUser'], baseReceiverName: 'svc' }

Full end-to-end chain confirmed correct. extractReceiverNode for the save() name node in Kotlin correctly returns call_expression(svc.getUser()) via the pre-existing navigation_suffix case at utils.ts:976-986. The fix mirrors scanKotlinConstructorBinding's firstNamedChild idiom.

2. createClassNameLookup Enum/Struct — ✅ Correct

type-env.ts:268-270:

const result = symbolTable.lookupFuzzy(name).some(def =>
  def.type === 'Class' || def.type === 'Enum' || def.type === 'Struct',
);

Previously Class-only. Enum and Struct are now first-class for constructor inference in Python/Kotlin.

3. Kotlin chain call integration test — ✅ Present and correctly structured

kotlin.test.ts:829-871 describes 'Kotlin chained method call resolution (Phase 5 review fix)' using the kotlin-chain-call fixture (App.kt calls svc.getUser().save(); UserService.getUser() returns User; both User and Repo have save()). The negative assertion (does NOT resolve to Repo#save) provides proper disambiguation proof.


New Findings

Finding 1 — LOW: navigation_suffix.lastNamedChild is safe for Kotlin generics

The fix uses suffix.lastNamedChild?.text to extract the method name from navigation_suffix. For generic method calls (list.filterIsInstance<User>()), one might expect type_arguments to appear inside navigation_suffix. Verified via tree-sitter-kotlin grammar: navigation_suffix only contains simple_identifier (property/method name); type arguments on method calls are children of the outer call_expression, not of navigation_suffix. lastNamedChild is safe. ✅ No issue.

Finding 2 — LOW: Deeper Kotlin chains untested at integration level

The kotlin-chain-call fixture tests depth-1 chains (svc.getUser().save()) only. The MAX_CHAIN_DEPTH = 3 and the depth-2 case (a.b().c().d()) are exercised by unit tests with pre-baked receiverCallChain arrays but not by Kotlin AST extraction integration tests.

Tracing depth-2 for Kotlin (svc.getUser().format().save()):

  • Receiver of save() = svc.getUser().format() call_expression
  • callee = navigation_expression(svc.getUser(), navigation_suffix(.format))
  • Inner loop: first non-suffix named child = call_expression(svc.getUser())CALL_EXPRESSION_TYPES hit → loop continues ✓
  • Second iteration extracts 'getUser' and simple_identifier(svc)
  • Returns { chain: ['getUser', 'format'], baseReceiverName: 'svc' }

Logic is correct but untested. A depth-2 fixture would confirm the loop unification works end-to-end for Kotlin.

Finding 3 — OBSERVATION: verifyConstructorBindings remains Class-only (benign after createClassNameLookup fix)

call-processor.ts:76:

const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;

Still only checks 'Class'. For val status = Status() (Kotlin Enum constructor), isClass = false causes the binding to fall through to method resolution — but since createClassNameLookup now returns true for Enum names, extractKotlinInitializer fires first and correctly binds status → Status via typeEnv. The verifyConstructorBindings path is the cross-file fallback for unresolved constructor inference (i.e., when extractInitializer skipped it). Since createClassNameLookup queries the SymbolTable with the Enum/Struct fix, extractInitializer handles these cases directly. verifyConstructorBindings being Class-only is therefore benign post-fix, but creates an internal consistency gap: if a cross-file Enum's classNames lookup somehow fails to fire (e.g., due to SymbolTable index lag), the fallback silently drops the binding.

Cosmetic fix, not blocking: add || def.type === 'Enum' || def.type === 'Struct' to the isClass check to be internally consistent with the rest of the codebase.

Fix this →

Finding 4 — OBSERVATION: PHP chain calls remain "integration untested"

PHP uses $-prefixed variable names ($svc), which extractVarName returns verbatim (including the $). The chain base resolution path:

baseType = typeEnv.lookup('$svc', callNode);   // if TypeEnv stores '$svc'

is consistent as long as TypeEnv and extractCallChain both preserve the $. Verified: extractCallChain's innerReceiver.text for a PHP variable_name node would include $. TypeEnv stores PHP vars with $ (PHP extractor uses extractVarName which returns node.text for variable_name, which includes $). The baseReceiverName → typeEnv.lookup path is consistent.

However, there is no PHP chain call integration fixture (e.g., $svc->getUser()->save()). The unit tests pre-bake receiverCallChain: ['getUser'] bypassing AST extraction. This was noted in Round 2 and deferred. Still not present.


Per-Language Gap Table (Final State After 4 Rounds)

Language Chain Calls Pattern Matching Class-as-Receiver Annotations
TypeScript ✅ integration (structural) ✅ integration
Java ✅ integration ✅ instanceof + integration ✅ + Enum ✅
Rust ✅ unit ✅ Some/Ok/Err ✅
C# ✅ unit ✅ is-pattern
Python ✅ unit walrus ✅ ✅ new
Go ✅ unit n/a n/a
PHP ✅ unit (no integration) n/a ✅ verified n/a
Ruby ✅ integration ✅ n/a n/a
Kotlin ✅ integration ✅ fixed n/a (when-expr Phase 6) ✅ + Enum/Struct ✅
Swift ❌ all tests skipped n/a n/a

Architectural Assessment (Final)

Four-round convergence: The three prior reviews found and fixed five HIGH/MEDIUM issues. This pass finds only LOW/OBSERVATION items. The codebase is in a clean state.

Dual-path symmetry: After c8f8584, the sequential and worker paths are fully symmetric for chain resolution (base typeEnv lookup, base class-as-receiver, chain walking). The Round 3 Kotlin fix (bd47b0c) applies equally to both paths via the shared extractCallChain function — no separate worker fix was needed since both paths call the same extractCallChain utility.

navigation_suffix/navigation_expression design: The Kotlin fix is correctly conservative (guarded by current.type === 'call_expression' AND callee?.type === 'navigation_expression' AND suffix?.type === 'navigation_suffix'). It does not fire for TS/JS/Go/Rust/Python/Ruby since those all produce a non-null funcNode from the first three field lookups. Swift shares Kotlin's call_expression type and navigation_expression structure, so the fix would also unblock Swift chained calls — but Swift tests remain skipped.

lookupFuzzy residual tech debt: resolveChainedReceiver at call-processor.ts:427 still does a secondary lookupFuzzy scan to retrieve returnType after resolveCallTarget finds the target. This 2× symbol table scan per chain step is the same tech debt noted in Round 1. Correctly deferred; clean fix requires returning SymbolDefinition from resolveCallTarget.


Summary

Approve. All three Round 3 findings are correctly implemented. No new HIGH or MEDIUM issues found. The Kotlin chain call fix is architecturally sound, correctly conservative, and does not regress any other language. The integration test provides a proper disambiguation proof.

Phase 6 recommendations (not blocking):

  1. Add Enum/Struct to verifyConstructorBindings isClass check for internal consistency (2-char addition)
  2. Add a depth-2 Kotlin chain call fixture (svc.getUser().format().save())
  3. PHP chain call integration test
  4. lookupFuzzylookupById refactor in resolveChainedReceiver
    · Branch

@magyargergo magyargergo merged commit f2d3df4 into main Mar 16, 2026
22 checks passed
@magyargergo magyargergo deleted the feat/type-resolution-phase5 branch March 16, 2026 19:38
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.

Python: context() and impact() return empty for classes that use static methods

1 participant