feat: return type inference, doc-comment parsing, and per-language type extractors#284
Conversation
…YARD type extractor Three architectural improvements to the type resolution system: 1. Return type inference — wire extractMethodSignature returnType through SymbolDefinition into call-processor. When var = callee() and callee has a known return type, bind var to that type. Handles Promise<T> unwrapping, nullable stripping, pointer/reference removal. 2. Generic type argument extraction — new extractGenericTypeArgs() utility that extracts type parameters from List<User> → ['User']. Handles TS/Java/Kotlin/C#/Rust generic syntax. Building block for for-loop variable typing. 3. Ruby dedicated type extractor — replaces the stub with YARD annotation parsing (@param name [Type]), handling qualified types, nullable types, and singleton methods. Ruby now has real type resolution. Unit tests: 127 → 192+ (type-env) + 65 (symbol-table, call-processor) + 18 (generics) Integration tests: 8+ new test cases with fixtures across TS/Python/Go/Java/Ruby
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 2158 tests passed 17 test(s) skipped — expand for detailsIntegration:
Code CoverageCombined (Unit + Integration)
Coverage breakdown by test suiteUnit Tests
Integration Tests
📋 View full run · Generated by CI |
|
@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 52s —— View job Code Review: Constructor Type Resolution AnalysisTasks:
What This PR ImprovedBefore Phase 3: Type resolution depended entirely on explicit type annotations (e.g. After Phase 3:
Issues Found🔴 Critical: TypeScript/JavaScript has no scanner —
|
| Pattern | TS | Python | Java | Go | Ruby |
|---|---|---|---|---|---|
var: Type = ... (explicit annotation) |
✅ | ✅ | ✅ | ✅ | ❌ (no annotations) |
var = new Class() |
✅ (callForm) | ✅ | ✅ | N/A | ✅ (via User.new) |
var = fn() return type inference |
❌ no scanner | ✅ | ❌ no scanner | ❌ no scanner | ❌ no scanner + no @return |
@param name [Type] (YARD) |
N/A | N/A | N/A | N/A | ✅ new |
Namespaced constructor (Ns::Class.new) |
N/A | N/A | N/A | N/A | ❌ |
Next Steps (from PR description + analysis)
Immediate follow-ups from this PR:
- Add integration tests for the 4 existing fixtures (
ts-return-type-inference,python-return-type-inference,go-return-type-inference,java-return-type-inference) — the fixtures were created but never exercised. - Add TypeScript/Java/Go scanners that capture untyped
var = fn()assignments so return type inference can fire for those languages. - Fix Ruby namespaced constructor: Handle
scope_resolutionreceiver type in bothextractRubyConstructorBindingandextractInitializer. - Add Ruby
@returnYARD parsing inruby.tsto populatereturnTypein SymbolDefinition — needed for return type inference from Ruby functions to work at all.
Planned Phase 3 remaining (from PR):
- 3.2: Chained property access (
this.repo.save()) — TypeEnv redesign needed - 3.3: For-loop variable typing — now unblocked by 3.1 + 3.4 being in this PR
- 3.5: Block-level type narrowing — scope model changes needed
| Branch
…namespaced constructors - Remove collection types (List, Array, Vec, Set) from WRAPPER_GENERICS to prevent false CALLS edges (e.g. List<User> no longer unwraps to User) - Add :: qualifier handling in extractReturnTypeName for Ruby/C++/Rust namespaced types - Add Ruby `constant` and `scope_resolution` node types to shared extractors - Extract shared extractRubyConstructorAssignment helper (dedup type-env.ts + ruby.ts) - Add integration tests for return type inference: Python, TypeScript, Go, Java, Ruby - Add Ruby namespaced constructor fixture (Models::UserService.new) - Add unit tests for collection reclassification and :: qualifiers
…rn type inference tests Add CONSTRUCTOR_BINDING_SCANNERS for 6 missing languages, completing return type inference coverage across all 11 supported languages: - TypeScript/JS: variable_declarator with call_expression, unwraps await - Go: short_var_declaration single-assignment (skips multi-return, new/make) - Java: local_variable_declaration with `var` type + method_invocation - C#: variable_declaration with implicit_type (var) + invocation_expression - Rust: let_declaration without type annotation, handles mut_pattern - PHP: assignment_expression with function_call_expression Also adds property_identifier to extractSimpleTypeName for qualified member calls (repo.getUser → getUser), fixing namespaced constructor inference that was previously a known limitation. Integration tests added for all 11 languages with correct label assertions (Function vs Method per language's tree-sitter queries).
…geTypeConfig Eliminates the parallel dispatch map in type-env.ts by moving all 11 constructor binding scanners into their respective type-extractors/*.ts files as `scanConstructorBinding` on LanguageTypeConfig. - Add ConstructorBindingScanner type to types.ts - Add shared helpers: hasTypeAnnotation, unwrapAwait, extractCalleeName - Move scanners to typescript.ts, jvm.ts, python.ts, php.ts, go.ts, rust.ts, swift.ts, c-cpp.ts, csharp.ts, ruby.ts - Fix `any` types in C# scanner → SyntaxNode | null - Delete ~300 lines from type-env.ts (CONSTRUCTOR_BINDING_SCANNERS map) - Update buildTypeEnv to use config.scanConstructorBinding All 143 type-env unit tests and all 10 language integration suites pass.
… comment - Remove unused extractCalleeName import from jvm.ts - Fix (c: any) → (c: SyntaxNode) in Java scanner - Update stale CONSTRUCTOR_BINDING_SCANNERS reference in ruby.ts comment
|
@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 6m 10s —— View job Code Review: Constructor Type Resolution — Deep AnalysisTasks:
Corrections to the Previous Review CommentThe previous Claude comment contained several inaccuracies — corrected here:
What This PR Actually ImprovedPhase 3 brought three concrete advances:
Real Issues Found🔴 Critical: C# and PHP integration tests are false positivesC# —
const func = value.firstNamedChild; // member_access_expression for svc.GetUser()
const calleeName = extractSimpleTypeName(func); // → undefined!
The integration test at PHP —
if (right.type !== 'function_call_expression') return undefined;In tree-sitter-php, Again the test at 🟡 Gap: Go multi-return pattern — most idiomatic Go is uncovered
if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined;The dominant Go idiom for constructor-like calls is 🟡 Gap: Ruby
|
| Pattern | TS | JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ | ✅* | ✅ | ✅** | ✅ | ❌ no @return |
❌ skipped | ||
var = obj.method() return inference |
✅ | ✅ | n/a | ✅ | n/a | n/a | n/a | ❌ bug | ❌ bug | ❌ | ❌ |
YARD @param |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ new | n/a |
* Java var-typed declarations only
** Go single-return only; user, err := fn() not captured
Recommended Next Steps (Priority Order)
-
Fix C#
member_access_expression: Add'member_access_expression'toextractSimpleTypeName's qualified-names block, then add a disambiguating test (two classes with same method name) to prove it works. -
Fix PHP
member_call_expression: ExtendscanConstructorBindingto also handlemember_call_expression(method calls on objects). Update the fixture to avoid the false-positive ambiguity. -
Investigate Swift skips: Debug why
extractInitializerdoesn't bind Swift constructor assignments in cross-file scenarios. Likely aclassNames.has()lookup failure. -
Add Ruby
@returnextraction: Wire YARD@return [Type]throughextractMethodSignature→SymbolDefinition.returnTypeto enable return type inference for Ruby. -
Go multi-return: Consider extracting the first variable from
user, err := fn()patterns (common Go idiom). Requires carefully avoiding false bindings fromerr-like variable names. -
Add
ruby-return-typeintegration test: The fixture exists but has no test suite.
| Branch
…re extraction, and cross-file resolution Addresses code review findings on PR #284: C# scanner (csharp.ts): - Fix type node lookup: iterate children instead of childForFieldName('type') which returns undefined in tree-sitter-c-sharp - Fix initializer lookup: handle direct invocation_expression children (no equals_value_clause wrapper in tree-sitter-c-sharp) C# return type extraction (utils.ts): - Add 'returns' field check to extractMethodSignature — tree-sitter-c-sharp uses 'returns', not 'type', for method return types C# cross-file resolution (call-processor.ts + fixture): - Add constructor binding verification to sequential processCalls path (was only in the worker processCallsFromExtracted path) - Add ReturnType.csproj to csharp-return-type fixture - Update fixture namespaces to use ReturnType.Models/ReturnType.Services prefix (matches real C# project conventions) PHP scanner (php.ts): - Extend scanConstructorBinding to handle member_call_expression ($this->getUser() patterns), not just function_call_expression Shared (shared.ts): - Add member_access_expression to extractSimpleTypeName qualified-names block (C# method calls like svc.GetUser()) Tests: - Add Repo.cs/Repo.php disambiguation fixtures (two Save methods) - Strengthen C# and PHP return type tests with hard disambiguation assertions - Add C# scanner unit tests and return type extraction test
|
@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 42s —— View job Code Review: Constructor Type Resolution — Third Pass AnalysisTasks:
Corrections to the Second Review CommentThe second comment identified real bugs in C# and PHP, but both were already fixed in the code on this branch:
Both of those "critical" findings describe code that was already fixed before the second review was written. The existing integration tests for C# and PHP likely pass for the right reason, not just fuzzy matching. What This PR Actually Improved (Accurate Summary)
Real Issues Found (Not in Previous Comments)🔴 Critical: Ruby
|
| Pattern | TS/JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|
| Explicit annotation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() / Class.new |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return-type inference |
✅ | ✅ | ✅ (var only) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ double gap | |
var = pkg.fn() package-qualified |
✅ | ✅ (attribute) | ✅ | n/a | ❌ selector_expression | ✅ | ✅ | ✅ | ❌ | |
YARD @return → return type |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ❌ never set | n/a |
Go user, err := fn() multi-return |
n/a | n/a | n/a | n/a | ❌ intentional | n/a | n/a | n/a | n/a | n/a |
Recommended Next Steps (Priority Order)
-
Harden the
ruby-return-typefixture — Add a second class with asavemethod to the fixture so the test actually requires type disambiguation (exposing the false positive). Then fix the two underlying gaps. -
Fix
selector_expressioninextractSimpleTypeName— A one-line addition toshared.ts:22unlocks package-qualified Go factory calls. Also addfield_identifierto the child-type allowlist at line 24-26. Low risk, high impact for Go codebases. -
Add Ruby
@returnYARD parsing — ExtendcollectYardParams(or add a sibling) to parse@return [Type]and thread throughextractMethodSignature→SymbolDefinition.returnType. Required before Ruby return-type inference can work at all. -
Broaden Ruby
scanConstructorBinding— After readme #3, changescanConstructorBindinginruby.tsfromextractRubyConstructorAssignmentto a function that also captures plain call assignmentsuser = get_user()(matching Python's approach). -
Go multi-return consideration —
user, err := fn()is idiomatic Go. Extracting only the first LHS for error-idiom patterns would be safe (err,_are well-known discard names). Worth a separate small PR.
| Branch
…or PHP, JS, Ruby Add ReturnTypeExtractor to LanguageTypeConfig interface with implementations for Ruby (YARD @return), PHP (PHPDoc @return), and JS/TS (JSDoc @returns). The fallback is wired in both parsing-processor and parse-worker paths, activating only when extractMethodSignature finds no AST-based return type. Also add doc-comment @param type extraction for PHP and JS/TS, following Ruby's existing collectYardParams pattern. This enables parameter.method() resolution in loosely-typed codebases using PHPDoc @param or JSDoc @param. Additional fixes from PR #284 code review: - Go: add selector_expression + field_identifier to extractSimpleTypeName (enables package-qualified factory calls like models.NewUser()) - Ruby: broaden scanConstructorBinding to capture plain call assignments (user = get_user()) in addition to Class.new patterns - Ruby: harden return-type fixture with disambiguation (two save methods) Test coverage: +14 new integration tests across Go, Ruby, PHP, JS/TS
Review Response: Third-Pass Analysis (claude[bot])All findings verified and addressed in commit Fixes Applied
Additional Work (beyond review scope)Extended the
Test Results
Corrections ConfirmedThe second review's "critical" findings were indeed already fixed on this branch:
|
|
@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 9m 42s —— View job Code Review: Constructor Type Resolution — Fourth Pass AnalysisTasks:
Verification of Third Review Fixes (Commit
|
| Claimed Fix | Verified |
|---|---|
Ruby ruby-return-type false positive — added repo.rb (two save methods) |
✅ Confirmed: models.rb has User#save, repo.rb has Repo#save, test checks for ≥2 save methods and verifies correct target file |
Go selector_expression + field_identifier added to extractSimpleTypeName |
✅ Confirmed: shared.ts:22-28 has both additions; go-return-type-inference fixture tests models.NewUser("bob") cross-package call |
Ruby @return YARD parsing added via extractReturnType |
✅ Confirmed: ruby.ts:189-211, wired in parse-worker.ts:1052-1057 |
Ruby scanConstructorBinding broadened to capture plain call assignments |
✅ Confirmed: ruby.ts:219-236 tries .new first, then falls through to plain call capture |
What Genuinely Works After Phase 3 + 36ca3a3
| Pattern | TS | JS | Python | Java | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ | ✅ (var only) | ✅ | ✅ | ✅ | ✅ | ✅ | |
var = pkg.fn() qualified |
✅ | ✅ | ✅ | ✅ | ✅ fixed | ✅ | ✅ | ✅ | n/a | |
| Doc-comment return annotation | n/a | ✅ JSDoc | n/a | n/a | n/a | n/a | ✅ PHPDoc | ✅ new YARD | n/a | |
@param doc annotation |
✅ JSDoc | ✅ JSDoc | n/a | n/a | n/a | n/a | ✅ PHPDoc | ✅ new YARD | n/a |
New Issues Found (Not in Previous Reviews)
🟡 Bug: normalizeJsDocType strips Promise<User> → Promise before storage — async JS functions produce wrong bindings
Location: gitnexus/src/core/ingestion/type-extractors/typescript.ts:29 + typescript.ts:169
normalizeJsDocType strips generic wrappers to their base type:
// Strip generic wrapper: Promise<User> → Promise (base type, not inner)
const genericMatch = type.match(/^(\w+)\s*</);
if (genericMatch) type = genericMatch[1];This is correct for @param {Array<User>} (binding the param to Array). But for @returns {Promise<User>}, normalizeJsDocType('Promise<User>') returns 'Promise'. This is then stored as SymbolDefinition.returnType = 'Promise'.
In call-processor.ts:508, extractReturnTypeName('Promise') is then called. Since 'Promise' starts with uppercase and is not in PRIMITIVE_TYPES, it returns 'Promise'. The variable is bound to type 'Promise' — completely wrong.
extractReturnTypeName already handles this correctly when it receives the full string: extractReturnTypeName('Promise<User>') → WRAPPER_GENERICS.has('Promise') → unwraps → 'User'. But normalizeJsDocType strips the generic before it ever reaches that function.
Affected path: JavaScript files with async functions annotated @returns {Promise<User>} (common in real-world JS codebases). TypeScript is unaffected because fetchUserAsync(): Promise<User> has the AST return type Promise<User> captured directly by extractMethodSignature without passing through normalizeJsDocType.
The existing js-jsdoc-return-type fixture avoids this because @returns {User} (no wrapper) — the tests pass but don't exercise the async path.
Fix: In extractReturnType (typescript.ts:164-174), return match[1] directly (optionally stripping just the JSDoc ?/! prefix), rather than calling normalizeJsDocType. Let extractReturnTypeName in call-processor.ts handle the wrapper unwrapping. @param annotation handling is a separate path (collectJsDocParams) and is unaffected.
🟡 Gap: PHP 8+ #[Attribute] nodes between PHPDoc and method break extractReturnType
Location: gitnexus/src/core/ingestion/type-extractors/php.ts:211-218
The comment walker stops at the first named non-comment sibling:
while (sibling) {
if (sibling.type === 'comment') {
const match = PHPDOC_RETURN_RE.exec(sibling.text);
if (match) return normalizePhpType(match[1]);
} else if (sibling.isNamed) break; // ← stops at attribute_list
sibling = sibling.previousSibling;
}For PHP 8+ code with Symfony/Laravel attributes:
/** @return User */
#[Route('/user')] // ← attribute_list node (isNamed = true) → BREAKS the walker
public function getUser() { ... } // ← no explicit return typeThe attribute_list is a named sibling between the PHPDoc block and the method. The walker reaches attribute_list, which isNamed → breaks, never finding the @return annotation. The same pattern affects the Ruby and JS/TS extractReturnType walkers for the analogous decorator patterns (Python @decorator, JS @decorator).
Note: this only affects untyped PHP methods. If getUser(): User has an explicit return type, extractMethodSignature captures it and extractReturnType is never called. The gap is for legacy untyped PHP code with attributes, which is a realistic PHP 8 migration scenario.
Fix: Allow skipping past known attribute/decorator node types (attribute_list, attribute) in the walker loop rather than breaking on them.
🟡 Gap: PHP member_call_expression scanner discards receiver — multiple classes with same method name causes no binding
Location: gitnexus/src/core/ingestion/type-extractors/php.ts:195-199
For $user = $this->getUser("alice"), the scanner captures only calleeName = 'getUser' (method name without receiver):
if (right.type === 'member_call_expression') {
const methodName = right.childForFieldName('name');
if (!methodName) return undefined;
return { varName: left.text, calleeName: methodName.text }; // ← only method name
}In call-processor.ts:507, callableDefs.length === 1 is required. If two classes in a project both have getUser() methods, the SymbolTable lookup returns 2 candidates → no binding. The existing php-return-type fixture passes only because UserService is the sole class with getUser(). Any PHP project with e.g. both UserService::getUser() and AdminService::getUser() would silently fail receiver type inference for the $this->getUser() pattern.
This is also present in Java's scanJavaConstructorBinding (captures only methodName.text from method_invocation), but is more impactful in PHP since PHP method calls commonly go through $this->.
There's no clean fix without receiver-type context (the receiver is $this, which requires knowing the enclosing class, which is available via findEnclosingClass). A targeted improvement: include the enclosing class name as a qualifier in the callee lookup when the receiver is $this/self/static.
Previously Identified Gaps That Remain
These were documented in earlier reviews and are still present (not introduced by this PR, and not fixed yet):
| Gap | Status |
|---|---|
Go user, err := fn() multi-return — only first LHS captured for single-LHS |
Intentional/deferred |
Java scanJavaConstructorBinding limited to var declarations |
Intentional design |
| Swift — all 11 tests skipped (tree-sitter-swift build failure on Node 22) | Infrastructure, not code bug |
Kotlin scanner can't handle val user = factory.getUser() qualified calls |
Acceptable simplification |
Previous Review Accuracy Summary
| Review | Finding | Accuracy |
|---|---|---|
| 1st review | "TS/JS has no scanner" | ❌ Wrong — typescript.ts has scanConstructorBinding |
| 1st review | "Ruby constant not in extractVarName" |
❌ Wrong — shared.ts:104 includes it |
| 1st review | "4 fixture dirs have no integration tests" | ❌ Wrong — all have tests |
| 2nd review | "C# member_access_expression not in extractSimpleTypeName" |
❌ Wrong — shared.ts:20 has it |
| 2nd review | "PHP member_call_expression skipped" |
❌ Wrong — php.ts:195 handles it |
| 3rd review | "Ruby ruby-return-type is a false positive" |
✅ Correct — fixed in 36ca3a3 |
| 3rd review | "Go selector_expression missing" |
✅ Correct — fixed in 36ca3a3 |
| 3rd review | "Ruby @return YARD never parsed" |
✅ Correct — fixed in 36ca3a3 |
| 3rd review | "Ruby scanner narrower than Python" | ✅ Correct — fixed in 36ca3a3 |
The third review was accurate. Commit 36ca3a3 addressed all real issues found.
Recommended Next Steps (Priority Order)
-
Fix
normalizeJsDocTypefor@returnspath —extractReturnTypeintypescript.ts:169should not runnormalizeJsDocType(which strips wrapper generics). Instead, pass the raw type string toextractReturnTypeNamein call-processor.ts, which already hasWRAPPER_GENERICSlogic. Add a JS fixture with@returns {Promise<User>}to verify. -
Allow attribute/decorator nodes in
extractReturnTypewalkers — Extend the skip set in the comment-walker loop inphp.ts(and the same pattern intypescript.ts,ruby.ts) to skipattribute_list/attributenodes instead of breaking on them. This is a one-liner fix per extractor. -
PHP
$this->method()receiver disambiguation — UsefindEnclosingClass(already inphp.ts) to qualifycalleeNamewith the enclosing class name when the receiver is$this/self/static, reducing false disambiguation failures.
| Branch
…er disambiguation Three fixes from fourth-pass code review on PR #284: 1. JSDoc `@returns {Promise<User>}` no longer stripped to `Promise` — extractReturnType now uses sanitizeReturnType (preserves generics) instead of normalizeJsDocType (which stripped them before extractReturnTypeName could unwrap WRAPPER_GENERICS). 2. PHP 8+ `#[Attribute]` and JS `@decorator` nodes no longer break doc-comment walkers. Both extractReturnType and collect*Params functions now skip attribute_list/decorator nodes instead of breaking on them as named siblings. 3. PHP `$this->method()` now provides receiverClassName for disambiguation. When two classes define the same method, the enclosing class narrows candidates via ownerId matching in call-processor, preventing false no-binding results.
|
@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 8m 20s —— View job Code Review: Constructor Type Resolution — Fifth Pass Analysis
|
| Fourth Review Claim | Status |
|---|---|
"normalizeJsDocType strips Promise<User> before extractReturnTypeName can unwrap it" |
✅ Fixed — sanitizeReturnType replaces normalizeJsDocType for the @returns path; it preserves generic wrappers |
"PHP attribute_list between PHPDoc and method breaks comment walker" |
✅ Fixed — SKIP_NODE_TYPES = { 'attribute_list', 'attribute' } at php.ts:70-71 |
"PHP $this->method() scanner discards receiver — disambiguation fails when two classes share a method name" |
✅ Fixed — scanConstructorBinding in php.ts:199-211 now passes receiverClassName (enclosing class) for $this/self/static receivers, and call-processor.ts:509-515 narrows candidates by owner class |
"Ruby false positive: ruby-return-type fixture has only one save method — test doesn't prove disambiguation" |
✅ Fixed — repo.rb added with Repo#save, broadened scanner, YARD @return parsed |
The parsing-processor.ts also has the extractReturnType fallback (line 220-225), mirroring parse-worker.ts:1052-1057 — both code paths are covered.
What This PR Has Accomplished (Accurate Fifth-Pass Summary)
After commit fda19a9, the resolution matrix is:
| Pattern | TS | JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ | ✅ (var) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ new | |
var = pkg.fn() qualified |
✅ | ✅ | ✅ | ✅ | n/a | ✅ fixed | ✅ | ✅ | ✅ | n/a | |
await fn() async inference |
✅ | ✅ new | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a |
$this->fn() receiver hint |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ new | n/a | n/a |
| Doc-comment return type | JSDoc | JSDoc | n/a | n/a | n/a | n/a | n/a | PHPDoc new | PHPDoc new | YARD new | n/a |
New Issues Found (Not in Previous Reviews)
🔴 Bug: sanitizeReturnType corrupts @returns {Promise<models.User>} — dot inside angle brackets
Location: gitnexus/src/core/ingestion/type-extractors/typescript.ts:172-173
const dotIdx = type.lastIndexOf('.');
if (dotIdx >= 0) type = type.slice(dotIdx + 1); // ← finds dot INSIDE angle bracketsFor @returns {Promise<models.User>}, lastIndexOf('.') finds the dot in models.User at position 14 inside the generic brackets. type becomes 'User>' (with trailing >). extractReturnTypeName('User>') fails the ^[A-Z_]\w*$ regex and returns undefined — silent type inference failure.
The existing test at typescript.test.ts:968 uses @returns {Promise<User>} (no dot inside generic), which has dotIdx = -1 and passes correctly. The bug is masked by the test fixture design.
Why the dot-stripping is unnecessary: extractReturnTypeName in call-processor.ts:444-447 already handles qualified names:
if (text.includes('::') || text.includes('.')) {
text = text.split(/::|\./).pop()!;
}So sanitizeReturnType can safely return 'models.User' and extractReturnTypeName will reduce it to 'User'. The dot-stripping in sanitizeReturnType is both redundant and harmful for the generic-wrapper case.
Safe fix: Remove lines 172-173 from sanitizeReturnType. With this change, 'Promise<models.User>' is passed intact to extractReturnTypeName, which correctly unwraps to 'User'.
🟡 Gap: JS/TS async return type tests lack negative assertions
Location: gitnexus/test/integration/resolvers/typescript.test.ts:968-982
The models.js fixture has both User#save and Repo#save. The test only checks:
// Positive: some save call from processUser targets models.js
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('models.js'),
);
expect(saveCall).toBeDefined();Since both methods live in models.js, this assertion passes whether type inference correctly returns User#save or falls back to fuzzy-matching both User#save and Repo#save. A broken type inference path (e.g., if the Promise unwrapping regressed) would still pass the test.
Compare to the PHP test at php.test.ts:736-741 which correctly adds:
// Negative: must NOT resolve to wrong class
const repoSave = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('Repo.php'),
);
expect(repoSave).toBeUndefined();Fix: Add a negative assertion to the JS async test: expect(calls.find(c => c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('models.js') && c.targetNodeId.includes('Repo'))).toBeUndefined(). Or simplify by moving each class into its own file (one user.js with User#save, one repo.js with Repo#save) so the targetFilePath check suffices to confirm disambiguation.
🟡 Gap: Ruby scanConstructorBinding — constant LHS (SERVICE = get_user()) not captured
Location: gitnexus/src/core/ingestion/type-extractors/ruby.ts:229
if (left.type !== 'identifier') return undefined; // ← skips 'constant' LHS for plain callsSERVICE = UserService.new is correctly handled by extractRubyConstructorAssignment (which handles both identifier and constant LHS in shared.ts:203). However, SERVICE = get_user() (constant LHS, plain function call) falls through to the plain-call path at ruby.ts:229, which guards identifier only. The constant assignment would be silently dropped.
This is a narrow gap — constant Ruby assignments from plain factory calls are uncommon in practice. The .new path covers the dominant constructor pattern.
🟡 Observation: Go multi-return user, err := GetUser() — still intentionally excluded
Location: gitnexus/src/core/ingestion/type-extractors/go.ts:151-152
// Single assignment only — skip multi-return like `user, err := GetUser()`
if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined;The Go scanner intentionally skips multi-return patterns. This is the dominant Go factory-call idiom. A targeted improvement would be to extract the first LHS variable when the second is err or _ (standard Go error-idiom discard names). This doesn't block the PR but is the most impactful remaining gap for real-world Go codebases.
Previous Review Accuracy Tracking (All 4 Passes)
| Review | Finding | Accuracy |
|---|---|---|
| 1st review | "TS/JS has no scanner" | ❌ Wrong |
| 1st review | "Ruby constant not in extractVarName" |
❌ Wrong |
| 1st review | "4 fixture dirs have no integration tests" | ❌ Wrong |
| 2nd review | "C# member_access_expression not in extractSimpleTypeName" |
❌ Wrong |
| 2nd review | "PHP member_call_expression skipped" |
❌ Wrong |
| 3rd review | "Ruby false positive, Go selector_expression, Ruby @return" |
✅ All correct — fixed in 36ca3a3 |
| 4th review | "normalizeJsDocType strips Promise<User>, PHP attribute walker, PHP $this receiver" |
✅ All correct — fixed in fda19a9 |
| 5th review (this) | sanitizeReturnType corrupts Promise<models.User>; JS async tests lack negative assertions |
— |
The codebase is in genuinely good shape. The third and fourth review passes were accurate; the fixes were well-implemented. The only confirmed new bug is the sanitizeReturnType dot-stripping edge case — a one-line fix.
Recommended Next Steps (Priority Order)
-
Fix
sanitizeReturnType— Remove 2 lines (dotIdx/slice attypescript.ts:172-173). Low risk, high correctness. Add a fixture test with@returns {Promise<models.User>}to lock this in. -
Strengthen JS async test assertions — Split
models.jsinto separate files or add negative assertions. Prevents silent regressions in the Promise-unwrapping path. -
Go multi-return (
user, err := GetUser()) — Consider extracting first LHS when second iserr/_. High real-world impact for Go codebases. -
Ruby constant LHS for plain calls — Change
ruby.ts:229toif (left.type !== 'identifier' && left.type !== 'constant') return undefined. Minor gap, easy fix. -
Swift — All 11 tests still skipped due to tree-sitter-swift build issues on Node 22. Infrastructure issue, not a code bug.
| | Branch
…tant receiver - Remove redundant dot-path stripping from sanitizeReturnType that corrupted qualified names inside generics (e.g. Promise<models.User> → User>) - Split JS async fixture into separate files and add negative assertions to properly verify disambiguation (mirroring PHP test pattern) - Accept 'constant' node type in Ruby scanConstructorBinding for factory call assignments (SERVICE = build_service()) - Add 'constant' to SIMPLE_RECEIVER_TYPES so extractReceiverName handles Ruby constant receivers (SERVICE.process)
|
@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 7m 2s —— View job Code Review: Constructor Type Resolution — Eighth Pass AnalysisTasks:
Verification of Seventh Review Fixes (Commit
|
| Claimed Fix | Verified |
|---|---|
Swift scanConstructorBinding — suffix?.type === 'simple_identifier' wrong for tree-sitter-swift |
✅ Fixed: swift.ts:108-116 now checks both simple_identifier AND navigation_suffix, unwrapping the inner lastNamedChild for the method name |
Rust lifetime parameters (Ref<'_, User>) — extractFirstGenericArg split on inner comma |
✅ Fixed: extractFirstTypeArg at call-processor.ts:481-492 iterates through lifetime tokens ('a, '_, 'static) and skips them to find the first real type arg |
Kotlin return-type disambiguation unproven — no competing save() methods |
✅ Acknowledged: Repo.kt added with Repo#save, lenient assertion documents the known gap, test now proves User#save is not spuriously emitted |
What This PR Has Accomplished (Eighth-Pass Summary)
After commit 5caaae9, the complete resolution matrix:
| Pattern | TS | JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() / Class.new |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ | ✅ (var) | ✅ | ✅ | ✅ | ✅ | ✅ | ||
var, err = fn() multi-return |
n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a | n/a |
var = pkg.fn() qualified |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | |
var = service.fn() receiver call |
✅ | ✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | |
$this->fn() receiver hint |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a |
| Doc-comment return type | JSDoc ✅ | JSDoc ✅ | n/a | n/a | n/a | n/a | n/a | PHPDoc ✅ | PHPDoc ✅ | YARD ✅ | n/a |
Nested wrapper W<R<T,E>> |
✅ | ✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | n/a | n/a |
Rust lifetime wrappers Ref<'_, T> |
n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a |
New Issues Found
🟡 Gap: Kotlin return type inference — extractMethodSignature likely misses Kotlin return types
Location: gitnexus/src/core/ingestion/utils.ts:662-668
The Kotlin known gap (documented in kotlin.test.ts:484-513) has a likely root cause that hasn't been identified in previous reviews.
extractMethodSignature reads return types via a generic scan:
for (const child of node.children) {
if (child.type === 'type_annotation' || child.type === 'return_type') {
const typeNode = child.children.find((c) => c.isNamed);
if (typeNode) returnType = typeNode.text;
}
}In tree-sitter-kotlin, fun getUser(name: String): User may NOT wrap the return type in a type_annotation node. Instead, the : User portion is likely parsed as a user_type direct child of function_declaration (after the colon punctuation token) — similar to how Go uses a result field (handled explicitly at line 619-634) and Rust uses a return_type field (handled explicitly at line 638-643).
If Kotlin return types are not type_annotation nodes, then callableDefs[0].returnType is always undefined in verifyConstructorBindings, and the return type inference chain never fires for any Kotlin function. This would explain why the test says disambiguation "does not yet resolve competing same-named methods."
The fix would be a Kotlin-specific case in extractMethodSignature, analogous to the Go and Rust cases already there:
// Kotlin: function return type is a user_type direct child (after ':' punctuation)
if (!returnType) {
for (const child of node.children) {
if (child.type === 'user_type' || child.type === 'nullable_type') {
returnType = child.text;
break;
}
}
}However, this must be guarded to only apply to Kotlin functions (not accidentally applied to other languages), and requires verifying the actual tree-sitter-kotlin AST structure for function declarations.
🟡 Gap: Java scanJavaConstructorBinding discards receiver — var user = service.getUser() fails when two classes share the method name
Location: gitnexus/src/core/ingestion/type-extractors/jvm.ts:82-85
if (value.type !== 'method_invocation') return undefined;
const methodName = value.childForFieldName('name');
if (!methodName) return undefined;
return { varName: nameNode.text, calleeName: methodName.text };For var user = adminService.getUser("alice"), calleeName = 'getUser' with no receiver info. If UserService and AdminService both have getUser(), callableDefs.length > 1 → no binding. The PHP $this->method() issue received receiverClassName disambiguation (now at php.ts:199-211). Java instance method calls with var declarations have no equivalent — the receiver variable's type would need to be looked up in the local TypeEnv, a two-pass problem not solvable at the scanner level.
This is a more fundamental architectural gap than a simple bug. Explicit Java typing (UserService user = service.getUser()) is already handled by extractJavaDeclaration. The var path has this limitation for method calls on non-this receivers.
🟡 Gap: Rust scanConstructorBinding fires for let user = User::default() — produces unhelpful calleeName: 'default'
Location: gitnexus/src/core/ingestion/type-extractors/rust.ts:174-177
The scanner excludes ::new calls (returns undefined) since extractInitializer handles them. But ::default() is not excluded:
if (func.type === 'scoped_identifier') {
const methodName = func.lastNamedChild;
if (methodName?.text === 'new') return undefined; // ← only 'new', not 'default'
}
const calleeName = extractSimpleTypeName(func); // returns 'default' for User::default()For let user = User::default(), the scanner produces { calleeName: 'default' }. In verifyConstructorBindings, ctx.resolve('default', filePath) finds many candidates (Rust's Default trait is widely implemented) → callableDefs.length > 1 → no binding. The extractInitializer path in typeEnv already handles User::default() correctly for same-file resolution, so this is a harmless inefficiency (slightly wrong scanner input, but no false binding created). Cross-file User::default() where User is in another file would also not work via the scanner — but neither does it via extractInitializer since that is same-file only.
The safe fix is a one-liner: add || methodName?.text === 'default' to the exclusion check, matching extractInitializer's behavior at rust.ts:125.
Previous Review Accuracy Tracking (All 8 Passes)
| Review | Findings | Accuracy |
|---|---|---|
| 1st | TS/JS no scanner; Ruby extractVarName gaps; 4 fixtures untested | ❌ All wrong — features already existed |
| 2nd | C# member_access_expression; PHP member_call_expression |
❌ Both wrong — already handled |
| 3rd | Ruby false positive; Go selector_expression; Ruby @return |
✅ All correct — fixed in 36ca3a3 |
| 4th | normalizeJsDocType strips generics; PHP attribute walker; PHP $this receiver |
✅ All correct — fixed in fda19a9 |
| 5th | sanitizeReturnType dot corruption; async test false positives; Ruby constant LHS |
✅ All correct — fixed in 9fbbe2d |
| 6th | Sync JSDoc false positive; Ruby factory call false positive; nested wrapper generics | ✅ All correct — fixed in 6465b8d |
| 7th | Kotlin disambiguation unproven; Swift navigation_suffix; Rust lifetime wrapper |
✅ All correct — fixed in 5caaae9 |
| 8th (this) | Kotlin extractMethodSignature gap; Java receiver disambiguation; Rust ::default() |
— |
The pattern is clear: reviews 3–7 all found genuine issues that were promptly fixed. Reviews 1–2 were wrong because they hadn't read the code carefully enough. The codebase is in excellent shape with only three remaining gaps, all of which are either architectural limitations or minor inefficiencies.
Recommended Next Steps (Priority Order)
-
Investigate Kotlin
extractMethodSignature— The root cause of the known gap is likely theutils.tsreturn type scan missing Kotlin's AST structure. Parsefun getUser(name: String): User {}with tree-sitter-kotlin to confirm the node type, then add a specific extraction case. Changing the lenient test to a strict assertion will confirm the fix. -
Rust
::default()exclusion — One-line fix. Add|| methodName?.text === 'default'atrust.ts:176. Prevents the scanner from generating useless cross-file bindings for common Rust trait methods. -
Java
var+ method receiver disambiguation — Architectural gap. Would require two-pass scanning (scan TypeEnv first, then use it to qualify the scanner's output). Best addressed as a separate follow-up issue rather than in this PR. -
Swift infrastructure — All 11 tests remain skipped due to tree-sitter-swift build failure on Node 22. Infrastructure issue beyond PR scope.
| Branch
…Doc alternate order, escapeValue hardening
- Add C# null-conditional call support (user?.Save()): tree-sitter query for
conditional_access_expression, member_binding_expression in MEMBER_ACCESS_NODE_TYPES,
receiver extraction via conditional_access_expression parent walk
- Fix Ruby YARD type parsing for nested generics (Hash<Symbol, User>): replace
naive split(',') with bracket-balanced splitter respecting <> depth
- Add alternate YARD format (@param [Type] name) alongside standard (@param name [Type])
- Add alternate PHPDoc format (@param $name Type) alongside standard (@param Type $name)
- Harden escapeValue in kuzu-adapter.ts: escape \n and \r to prevent Cypher injection
- Integration tests: C# null-conditional fixture (5 tests), Ruby YARD generics fixture (6 tests)
- Unit tests: PHPDoc alternate order (2 tests), C# null-conditional call-form (updated)
|
@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 10m 55s —— View job Code Review: Constructor Type Resolution — Ninth Pass AnalysisTasks:
New Commit Since 8th Pass:
|
| Feature | Status |
|---|---|
C# null-conditional calls user?.Save(): tree-sitter query + member_binding_expression in MEMBER_ACCESS_NODE_TYPES + receiver extraction via conditional_access_expression parent walk |
✅ Correctly implemented |
Ruby YARD bracket-balanced generic splitting — Hash<Symbol, User> no longer breaks on inner comma |
✅ Correctly implemented |
Ruby alternate YARD @param [Type] name format |
✅ Correctly implemented; params.has(paramName) guard gives standard format priority |
PHP alternate PHPDoc @param $name Type format |
✅ Correctly implemented; both regex lastIndex properly reset before each use |
escapeValue \n/\r hardening in lbug-adapter.ts |
Verification of 8th Review Fixes (Commit 5caaae9)
All three items from the 8th review are confirmed correctly addressed:
| Claimed Fix | Verified |
|---|---|
Swift scanConstructorBinding — suffix?.type === 'simple_identifier' wrong, should unwrap navigation_suffix |
✅ swift.ts:108-116 now checks both simple_identifier AND navigation_suffix, with lastNamedChild unwrapping |
Rust lifetime parameters Ref<'_, User> — extractFirstGenericArg split on inner comma |
✅ extractFirstTypeArg at call-processor.ts:481-492 iterates past 'lifetime tokens |
Kotlin disambiguation proof — lenient test with Repo.kt added |
✅ Lenient assertions correctly document the known gap without false failures |
New Issues Found
🔴 Bug: lbug-adapter.ts batch upsert escapeValue missing \n/\r hardening — same Cypher injection vector as CREATE path
Location: gitnexus/src/core/lbug/lbug-adapter.ts:425-430
Commit ec4dca4 hardened the escapeValue in the CREATE path (line 364) to escape \n and \r:
// Line 364 — correctly hardened ✓
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")
.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;But the MERGE/upsert path at line 425 uses a separate escapeValue closure that was not updated:
// Line 425-430 — NOT hardened ❌
const escapeValue = (v: any): string => {
if (v === null || v === undefined) return 'NULL';
if (typeof v === 'number') return String(v);
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
// ↑ missing \n and \r escaping
};The MERGE path is used by the batch upsert function and passes the same content fields (multiline source code) through escapeValue. A file containing raw newlines in its indexed content — e.g. any source file — will produce a malformed Cypher query when upserted via this path. The \n and \r escaping is identical to what was added to line 364; this is a missed instance of the same fix.
🟡 Gap: Rust ::default() still not excluded from scanConstructorBinding
Location: gitnexus/src/core/ingestion/type-extractors/rust.ts:176
First identified in the 8th review, still present:
if (func.type === 'scoped_identifier') {
const methodName = func.lastNamedChild;
if (methodName?.text === 'new') return undefined; // ← 'default' not excluded
}For let user = User::default(), the scanner produces { calleeName: 'default' }. In verifyConstructorBindings, ctx.resolve('default', filePath) returns many candidates (Rust's Default trait is widely implemented) → callableDefs.length > 1 → no binding. This is a harmless no-op (no false binding is created), but it wastes a cross-file lookup for every ::default() call. extractInitializer at rust.ts:125 already excludes default correctly — the scanner should match.
The one-liner fix: change methodName?.text === 'new' to methodName?.text === 'new' || methodName?.text === 'default'.
🟡 Gap: Kotlin return type inference — root cause confirmed as 2-segment import resolution
Location: gitnexus/src/core/ingestion/resolvers/jvm.ts:82
The known Kotlin gap (lenient test at kotlin.test.ts:490-513) has a confirmed root cause:
const segments = importPath.split('.');
if (segments.length < 3) return null; // ← drops 2-segment importsThe fixture's App.kt uses import models.getUser (2 segments: ['models', 'getUser']). This returns null, so getUser is never linked from models/User.kt into services/App.kt's import graph. When scanKotlinConstructorBinding produces { varName: 'user', calleeName: 'getUser' } and verifyConstructorBindings calls ctx.resolve('getUser', 'services/App.kt'), the function may not be found as a cross-file candidate — no binding is produced, and user.save() is not disambiguated.
The guard segments.length < 3 was designed to detect member imports like com.example.Constants.VALUE (3+ segments where the last is a member name). Short package imports like models.getUser or io.getUser — 2-segment Kotlin top-level function imports — are incorrectly dropped. This is a pre-existing gap documented in the PR's deferred items.
What the Latest Commit (ec4dca4) Correctly Added
The C# null-conditional implementation is structurally sound:
- Tree-sitter query at
tree-sitter-queries.ts:370-373capturesinvocation_expression > conditional_access_expression > member_binding_expression > identifier @call.name MEMBER_ACCESS_NODE_TYPESnow includes'member_binding_expression'so the name node is recognizedextractReceiverNameatutils.ts:867-873walks frommember_binding_expression→conditional_access_expression→firstNamedChildto get the receiver identifier- The fixture
csharp-null-conditional/App.csusesUser user = new User()(explicit type, Tier 0), so the test correctly validates call resolution via type-env (not scanner), which is the appropriate path for this pattern
Accuracy Tracking (All 9 Passes)
| Review | Findings | Accuracy |
|---|---|---|
| 1st | TS/JS no scanner; Ruby extractVarName; 4 fixtures untested | ❌ All wrong |
| 2nd | C# member_access_expression; PHP member_call_expression missing |
❌ Both wrong |
| 3rd | Ruby false positive; Go selector_expression; Ruby @return missing |
✅ All correct — fixed in 36ca3a3 |
| 4th | normalizeJsDocType strips generics; PHP attribute walker; PHP $this receiver |
✅ All correct — fixed in fda19a9 |
| 5th | sanitizeReturnType dot corruption; async test false positives; Ruby constant LHS |
✅ All correct — fixed in 9fbbe2d |
| 6th | Sync JSDoc false positive; Ruby factory false positive; nested wrapper generics | ✅ All correct — fixed in 6465b8d |
| 7th | Kotlin disambiguation unproven; Swift navigation_suffix; Rust lifetime wrapper |
✅ All correct — fixed in 5caaae9 |
| 8th | Kotlin extractMethodSignature gap (unconfirmed); Java receiver disambiguation; Rust ::default() |
✅ Rust ::default() confirmed unfixed; Java is architectural; Kotlin root cause confirmed as import resolution |
| 9th (this) | lbug-adapter.ts batch escapeValue missing \n/\r; Rust ::default() still unfixed; Kotlin import resolution confirmed |
— |
Resolution Matrix (Post-ec4dca4)
| Pattern | TS | JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() / Class.new |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ | ✅ (var) | ✅ | ✅ | ✅ | ✅ | ✅ | ||
var, err = fn() multi-return |
n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a | n/a |
var?.method() null-conditional |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ new | n/a | n/a | n/a |
var = pkg.fn() qualified |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | |
$this->fn() receiver hint |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a |
| Doc-comment return type | JSDoc ✅ | JSDoc ✅ | n/a | n/a | n/a | n/a | n/a | PHPDoc ✅ | PHPDoc ✅ | YARD ✅ | n/a |
@param [Hash<Symbol, T>] YARD generics |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ new | n/a |
@param [Type] name alternate YARD |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ new | n/a |
@param $name Type alternate PHPDoc |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ new | n/a | n/a |
Nested wrapper W<R<T,E>> |
✅ | ✅ | n/a | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | n/a | n/a |
Rust Ref<'_, T> lifetime wrappers |
n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a |
Recommended Next Steps (Priority Order)
-
Fix
lbug-adapter.tsbatchescapeValue— Add.replace(/\n/g, '\\n').replace(/\r/g, '\\r')to theescapeValueat line 429. Same fix as line 368, missed instance. High correctness impact for any codebase with multiline file content in the graph. -
Fix Rust
::default()exclusion — One-liner atrust.ts:176. No test needed; the existing behavior is a no-op (no false binding), but wastes a lookup. -
Fix Kotlin 2-segment import resolution — Relax the
segments.length < 3guard inresolvers/jvm.ts:82to handlepackage.memberNamepatterns (2 segments, lowercase last segment). Once fixed, update thekotlin-return-typetest from lenient assertions to strictexpect(userSave.targetFilePath).toContain('User.kt')assertions. -
Swift infrastructure — All 11 tests remain skipped due to tree-sitter-swift build failure on Node 22. Infrastructure issue beyond PR scope.
| | Branch
Verifies that classes using only @staticmethod/@classmethod have HAS_METHOD edges connecting them to their child methods. This was the root cause of issue #289 where context() and impact() returned empty for such classes. Tests cover: HAS_METHOD edge emission, unique static method resolution (create_user, delete_user), and ambiguous same-named method handling (find_user on both UserService and AdminService — safely refused).
…er exclusion - Apply \n/\r escaping to batch upsert escapeValue in lbug-adapter.ts:429 (missed instance of the CREATE-path fix from ec4dca4) - Exclude Rust ::default() from scanConstructorBinding to match extractInitializer behavior — avoids wasted cross-file lookups on the broadly-implemented Default trait - Unit tests: 2 new scanner exclusion tests (::default and ::new) - Integration tests: 6 new Rust ::default() constructor resolution tests with disambiguation fixture (User::default vs Repo::default)
|
@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 16s —— View job Code Review: Constructor Type Resolution — Tenth Pass AnalysisTasks:
Verification of Ninth Review Fixes (Commits
|
| Claimed Fix | Verified |
|---|---|
lbug-adapter.ts batch escapeValue missing \n/\r |
✅ Fixed: lbug-adapter.ts:429 now has .replace(/\n/g, '\\n').replace(/\r/g, '\\r') alongside the existing quote escaping |
Rust ::default() not excluded from scanConstructorBinding |
✅ Fixed: rust.ts:176 now checks methodName?.text === 'new' || methodName?.text === 'default' matching extractInitializer's behavior |
Kotlin 2-segment import resolution — import models.getUser dropped |
Acknowledged as known gap: jvm.ts:82 still guards segments.length < 3. Lenient test at kotlin.test.ts:490-513 correctly documents this. |
New Issues Found
🔴 Bug: C# scanConstructorBinding doesn't unwrap await_expression — async binding missed
Location: gitnexus/src/core/ingestion/type-extractors/csharp.ts:134
if (value.type !== 'invocation_expression') return undefined;For var user = await svc.GetUserAsync("alice"), tree-sitter-c-sharp parses the RHS as await_expression { invocation_expression }. The scanner checks value.type !== 'invocation_expression' but receives await_expression → returns undefined. No binding is emitted for the variable.
The TypeScript scanner (typescript.ts:89–92) handles this correctly with explicit await unwrapping:
let value = declarator.childForFieldName('value');
if (value?.type === 'await_expression') {
value = value.firstNamedChild;
}C# async/await is idiomatic — var user = await repo.GetByIdAsync(id) is the standard pattern in ASP.NET. Any C# codebase using async repository or service patterns will silently miss all constructor bindings from async calls.
Fix: Add the same await_expression unwrap used in typescript.ts:89–92 to csharp.ts:124–131, between where value is extracted from equals_value_clause and where value.type is checked.
🟡 Bug: Rust scanConstructorBinding doesn't unwrap .await — async binding missed
Location: gitnexus/src/core/ingestion/type-extractors/rust.ts:171
const value = node.childForFieldName('value');
if (!value || value.type !== 'call_expression') return undefined;In Rust, let user = get_user_async().await parses as a let_declaration with value being an await_expression (.await is postfix in Rust), not a call_expression. The check value.type !== 'call_expression' returns undefined. No binding is produced.
let user = get_user().await is idiomatic Rust async code inside async fn blocks. This gap means all Rust async bindings are silently dropped by the scanner.
Fix: After extracting value from node.childForFieldName('value'), add unwrapping for await_expression: if (value?.type === 'await_expression') { value = value.firstNamedChild; }. Use unwrapAwait from shared.ts:240 which already exists for exactly this purpose.
🟡 Gap: PHP backslash-namespaced return types fail extractReturnTypeName
Location: gitnexus/src/core/ingestion/call-processor.ts:528–537 and gitnexus/src/core/ingestion/type-extractors/php.ts:57–65
When extractMethodSignature (via the generic loop at utils.ts:661–669) captures a PHP typed function's return type, it stores the raw text from the AST. For function getUser(): \App\Models\User, the return_type child's text is \App\Models\User. This is then passed to extractReturnTypeName.
extractReturnTypeName handles :: and . separators:
if (text.includes('::') || text.includes('.')) {
text = text.split(/::|\./).pop()!;
}But PHP uses \ as its namespace separator. \App\Models\User doesn't match :: or ., so text remains \App\Models\User. The final regex check /^[A-Z_]\w*$/ fails (backslash is not \w) and extractReturnTypeName returns undefined.
normalizePhpType in php.ts:57–65 correctly handles this with type.split('\\'), but that function is only called in extractReturnType (PHPDoc path). The AST-captured typed PHP returns never go through normalizePhpType.
In practice, PHP 7+ codebases using strict type declarations (function getUser(): \Models\User) with fully-qualified namespaces will silently fail return type inference. Code that uses use imports (most modern PHP) writes function getUser(): User directly, which works. This affects legacy or strictly-namespaced PHP code.
🟢 Observation: fallbackRelationshipInserts inline Cypher templates lack \n/\r escaping
Location: gitnexus/src/core/lbug/lbug-adapter.ts:307–311
The fallbackRelationshipInserts function (error-recovery path for failed batch COPY operations) uses inline template literals:
await conn.query(`
MATCH (a:... {id: '${fromId.replace(/'/g, "''")}' }),
(b:... {id: '${toId.replace(/'/g, "''")}' })
CREATE (a)-[:CALLS {type: '${relType}', ... reason: '${reason.replace(/'/g, "''")}', ...}]->(b)
`);These escape ' → '' but not \n/\r. The main escapeValue in batchInsertNodesToLbug was hardened in commit 1daebc0, but this separate fallback path was missed. In practice, fromId and toId are node identifiers (unlikely to contain newlines), and reason is a short descriptor. The risk is low since this is the error-recovery fallback, but it's the same injection vector that was just fixed in the batch path.
What Has Improved Since PR Opening (Accurate Summary)
After commits through 1daebc0, the complete resolution matrix is:
| Pattern | TS/JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() / Class.new |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ (var) | ✅ | ✅ | ✅ | ✅ | ✅ | ||
var = pkg.fn() qualified |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | |
var = await fn() async |
✅ | n/a | n/a | n/a | n/a | ❌ new | ❌ new | n/a | n/a | n/a |
var, err = fn() multi-return |
n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a | n/a |
var?.method() null-conditional |
n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a |
$this->fn() receiver hint |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a |
| Doc-comment return type | JSDoc ✅ | n/a | n/a | n/a | n/a | n/a | PHPDoc ✅ | PHPDoc ✅ | YARD ✅ | n/a |
Nested wrapper W<R<T,E>> |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | n/a |
Rust Ref<'_, T> lifetime wrappers |
n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a |
Accuracy Tracking (All 10 Passes)
| Review | Findings | Accuracy |
|---|---|---|
| 1st | TS/JS no scanner; Ruby extractVarName gaps; 4 fixtures untested | ❌ All wrong |
| 2nd | C# member_access_expression; PHP member_call_expression |
❌ Both wrong |
| 3rd | Ruby false positive; Go selector_expression; Ruby @return missing |
✅ All correct — fixed in 36ca3a3 |
| 4th | normalizeJsDocType strips generics; PHP attribute walker; PHP $this receiver |
✅ All correct — fixed in fda19a9 |
| 5th | sanitizeReturnType dot corruption; async test false positives; Ruby constant LHS |
✅ All correct — fixed in 9fbbe2d |
| 6th | Sync JSDoc false positive; Ruby factory false positive; nested wrapper generics | ✅ All correct — fixed in 6465b8d |
| 7th | Kotlin disambiguation unproven; Swift navigation_suffix; Rust lifetime wrapper |
✅ All correct — fixed in 5caaae9 |
| 8th | Kotlin extractMethodSignature (unconfirmed); Java receiver disambiguation; Rust ::default() |
✅ Rust ::default() fixed in 1daebc0; Kotlin root cause confirmed as import resolution |
| 9th | lbug-adapter.ts batch escapeValue missing; Rust ::default(); Kotlin import resolution |
✅ escapeValue and ::default() fixed in 1daebc0 |
| 10th (this) | C# await not unwrapped; Rust .await not unwrapped; PHP \-separated namespace types |
— |
Recommended Next Steps (Priority Order)
-
Fix C# async
awaitunwrapping — Addif (value?.type === 'await_expression') { value = value.firstNamedChild; }tocsharp.ts:131–134, matching the TS scanner pattern. Add a C# async fixture with two classes sharing an async method to prove disambiguation works. -
Fix Rust async
.awaitunwrapping — Use the existingunwrapAwaitfromshared.ts:240(or replicate the two-line pattern) inrust.ts:171. Add a Rust async fixture withFuture<User>return types. -
Fix PHP backslash namespace in
extractReturnTypeName— Add\\to the namespace separator handling atcall-processor.ts:529. Extend the condition totext.includes('::') || text.includes('.') || text.includes('\\'). -
Fix Kotlin 2-segment import resolution — Relax
jvm.ts:82guard fromsegments.length < 3to also handle 2-segment Kotlin top-level function imports (models.getUser). Once fixed, update the lenientkotlin.test.ts:490assertions to strict positive assertions. -
Swift infrastructure — All 11 tests remain skipped due to tree-sitter-swift build failure on Node 22. Infrastructure issue.
| Branch
10th Pass — All 4 Findings Fixed
Tests added:
All passing: 1349 unit tests, 61 Rust integration, 55 C# integration, 74 PHP integration. |
…caping - C# scanConstructorBinding: unwrap await_expression to find invocation_expression (var user = await svc.GetUserAsync() now produces constructor binding) - Rust scanConstructorBinding: unwrap .await postfix via shared unwrapAwait helper (let user = get_user().await now produces constructor binding) - extractReturnTypeName: handle PHP backslash namespace separator (\App\Models\User → User) - fallbackRelationshipInserts: match batch escapeValue hardening with \n/\r escaping Tests: 2 unit (type-env), 3 unit (call-processor), 7 integration (csharp+rust), 7 fixtures
|
@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 42s —— View job Claude finished @magyargergo's task —— Code Review: Constructor Type Resolution — Eleventh Pass AnalysisTasks:
Verification of Tenth Review Fixes (Commit
|
| Claimed Fix | Verified |
|---|---|
C# scanConstructorBinding missing await_expression unwrap |
✅ csharp.ts:133-135: value = unwrapAwait(value) is called before the invocation_expression type check |
Rust scanConstructorBinding missing .await unwrap |
✅ rust.ts:171: unwrapAwait(node.childForFieldName('value')) replaces the bare field access |
PHP \-separated namespace types fail extractReturnTypeName |
✅ call-processor.ts:529: text.includes('\\') added to condition; [.\\] in the split regex |
fallbackRelationshipInserts missing \n/\r escaping |
✅ lbug-adapter.ts:307: esc helper added with .replace(/\n/g, '\\n').replace(/\r/g, '\\r') |
New Issues Found
🔴 Bug: csharp-async-binding test is a false positive — Save is unique, both services return same type
Location: gitnexus/test/fixtures/lang-resolution/csharp-async-binding/OrderService.cs:5
Both UserService.GetUserAsync and OrderService.GetUserAsync return Task<User> (not Task<User> and Task<Order> as one might expect). Looking at OrderService.cs:
public async Task<User> GetUserAsync(string name) // ← returns User, NOT OrderThere is only one Save() method in the fixture — User.Save() in User.cs. There is no competing Order.Save(). As a result:
ctx.resolve('GetUserAsync', filePath)returns 2 candidates (one from each service), socallableDefs.length === 2→verifyConstructorBindingsproduces no binding (thecallableDefs.length === 1guard atcall-processor.ts:105fails).Saveis the only method with that name, so it resolves via fuzzy matching regardless of whether the binding was produced.- The test at line 689 (
resolves user.Save() after await to User#Save) passes without the await unwrapping doing any work.
The test title says "competing Save methods" but there's exactly one. This is the same false-positive pattern found and fixed in the 5th/6th reviews for the JS sync and Ruby fixture tests.
A correct test would need OrderService.GetUserAsync to return Task<Order>, an Order.cs file with its own Order.Save(), and a negative assertion ensuring user.Save() does NOT resolve to Order.Save().
🔴 Bug: rust-async-binding test is a false positive — both save methods in the same file
Location: gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/models.rs
Both User::save and Repo::save are defined in the same models.rs file. The test checks:
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();Since both structs live in models.rs, targetFilePath.includes('models') is true for both — the assertion cannot distinguish whether the correct struct's save was hit. There are no negative assertions confirming process_user does NOT resolve to Repo::save.
Compare to the rust-return-type-inference fixture (correctly split into user.rs/repo.rs from the 6th review fix) — this new fixture did not apply the same lesson.
🟡 Gap: C# scanConstructorBinding discards receiver for non-this calls — multi-service disambiguation impossible
Location: gitnexus/src/core/ingestion/type-extractors/csharp.ts:139-143
For var user = await userSvc.GetUserAsync("alice"), after unwrapAwait the scanner sees invocation_expression with func = member_access_expression { userSvc . GetUserAsync }. extractSimpleTypeName(func) returns only the method name 'GetUserAsync' — the receiver userSvc is discarded.
When two classes share GetUserAsync, callableDefs.length > 1 → no binding. PHP handles this via receiverClassName when receiver is $this/self. For C#, when the receiver is an arbitrary typed variable (not this), the receiver's type would require a TypeEnv lookup — a two-pass problem that can't be solved at scanner level.
This means var user = await serviceA.GetUser() and var order = await serviceB.GetUser() can never be disambiguated via return type inference when GetUser exists on multiple classes. This is the dominant async DI pattern in ASP.NET. It's an architectural gap, shared with Java (documented in jvm.ts:85), but newly relevant for the C# async path.
🟡 Remaining Gap: Kotlin return type inference — extractMethodSignature likely misses Kotlin return types
Location: gitnexus/src/core/ingestion/utils.ts:662-669
This was flagged in the 8th review. The Kotlin test at kotlin.test.ts:484 now uses a lenient assertion acknowledging the gap. Root cause: extractMethodSignature looks for type_annotation or return_type children, but tree-sitter-kotlin places the fun getUser(): User return type differently than TypeScript. The extractKotlinDeclaration extractor explicitly looks for user_type nodes — suggesting Kotlin's AST differs from what extractMethodSignature expects.
Until extractMethodSignature has a Kotlin-specific return type extraction case, SymbolDefinition.returnType will be undefined for all Kotlin functions, and the lenient test will remain lenient forever.
🟢 Observation: Go scanConstructorBinding multi-return second-var list is narrower than the full idiom
Location: gitnexus/src/core/ingestion/type-extractors/go.ts:155-160
The multi-return handler checks secondVar.text === '_' || err || ok || error. Go also uses:
user, cancel := ctx.WithCancel(...)—cancelis not a discard nameuser, closer := ...— similar pattern- Any two-return factory function where the second isn't an error
These are dropped silently. This is a narrow gap compared to the main user, err := fn() pattern that is now handled, but represents real-world Go code using context cancellation or cleanup functions alongside user objects.
What the PR Has Achieved (Accurate Summary After 10 Review Passes)
The codebase has improved dramatically across 10 iterations. The core type resolution pipeline now works correctly for:
| Pattern | Status |
|---|---|
var: T = ... explicit annotations |
✅ All 11 languages |
var = new Class() constructor inference |
✅ All 11 languages |
var = fn() return type inference |
✅ 9/11 (Kotlin known gap, Swift skipped) |
var = await fn() / let x = fn().await |
✅ C# + Rust (new in c3b635a) |
var, err := fn() Go multi-return |
✅ New in 6465b8d |
@param / @return doc-comment types |
✅ JSDoc, PHPDoc, YARD |
Nested wrapper generics W<R<T,E>> |
✅ Balanced-bracket extraction |
Rust lifetime wrappers Ref<'_, T> |
✅ Lifetime-skipping extractor |
PHP \App\Models\User backslash namespaces |
✅ New in c3b635a |
C# user?.Save() null-conditional |
✅ New in ec4dca4 |
Accuracy Tracking (All 11 Passes)
| Review | Findings | Accuracy |
|---|---|---|
| 1st | TS/JS no scanner; Ruby gaps; 4 fixtures untested | ❌ All wrong |
| 2nd | C# member_access_expression; PHP member_call_expression |
❌ Both wrong |
| 3rd | Ruby false positive; Go selector_expression; Ruby @return |
✅ All correct — fixed |
| 4th | normalizeJsDocType strips generics; PHP attribute walker; PHP $this |
✅ All correct — fixed |
| 5th | sanitizeReturnType dot corruption; async test false positives; Ruby constant LHS |
✅ All correct — fixed |
| 6th | Sync JSDoc false positive; Ruby factory false positive; nested wrapper generics | ✅ All correct — fixed |
| 7th | Kotlin disambiguation unproven; Swift navigation_suffix; Rust lifetime wrapper |
✅ All correct — fixed |
| 8th | Kotlin extractMethodSignature gap; Java receiver disambiguation; Rust ::default() |
✅ Rust ::default() fixed; others confirmed |
| 9th | lbug-adapter.ts batch escapeValue missing; Rust ::default(); Kotlin root cause |
✅ All correct — fixed |
| 10th | C# await unwrap; Rust .await unwrap; PHP \ namespace; fallback escaping |
✅ All correct — fixed in c3b635a |
| 11th (this) | C# async test false positive; Rust async test false positive; Kotlin extractMethodSignature gap remains |
— |
Recommended Next Steps (Priority Order)
-
Fix
csharp-async-bindingfixture and test — ChangeOrderService.GetUserAsyncto returnTask<Order>, addOrder.cswithOrder.Save(), add a negative assertion. This is the same one-time fix applied to JS and Ruby fixtures in passes 5–6. -
Fix
rust-async-bindingfixture and test — Splitmodels.rsintouser.rs/repo.rs, add negative assertions. Direct copy of therust-return-type-inferencefix from pass 6. -
Investigate Kotlin
extractMethodSignature— Check tree-sitter-kotlin's AST forfun getUser(): Userto confirm whether the return type appears as atype_annotationchild or directly asuser_type. If the latter, add a Kotlin-specific extraction case inutils.ts(like the existing Go and Rust cases at lines 619-643). Once fixed, the lenient test becomes strict. -
C# receiver disambiguation (architectural) — For
var user = await serviceA.GetUser()where multiple classes shareGetUser, the scanner currently discards the receiver. A targeted improvement: when the receiver is a typed variable already in the localTypeEnv, use its type to qualify the callee lookup — analogous to PHP'sreceiverClassNamefor$this. Requires a two-pass approach within the scanner or a post-scanner narrowing step.
| Branch
Eleventh Pass Response — Both False-Positive Tests FixedFindings evaluation:
C# async-binding fix:
Rust async-binding fix:
Both fixes follow the same disambiguation pattern established in passes 5–6 for JS and Ruby fixtures: separate files per class/struct + explicit negative assertions proving cross-contamination doesn't occur. |
… and negative assertions C# fixture: add Order.cs with Order.Save(), change OrderService to return Task<Order> via GetOrderAsync, add negative assertion proving user.Save() does not resolve to Order#Save. Rust fixture: split models.rs into user.rs/repo.rs, make process_user and process_repo async fn, add bidirectional negative assertions proving no cross-contamination between User#save and Repo#save.
|
@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 7m 55s —— View job Code Review: Constructor Type Resolution — Twelfth Pass AnalysisTasks:
Verification of Eleventh Review Fixes (Commit
|
| Claimed Fix | Verified |
|---|---|
C# csharp-async-binding — both services returned Task<User>, only one Save() |
✅ Fixed: OrderService.GetOrderAsync now returns Task<Order>, Order.cs has Order.Save(), Program.cs calls both user.Save() and order.Save() separately |
Rust rust-async-binding — both structs in same models.rs, no negative assertions |
✅ Fixed: split into user.rs/repo.rs, process_user/process_repo in main.rs, strict expect(wrongSave).toBeUndefined() at rust.test.ts:793 |
New Issues Found
🔴 Bug: C# async-binding negative assertion is logically broken
Location: gitnexus/test/integration/resolvers/csharp.test.ts:698–710
The 11th pass correctly fixed the fixture, but the negative assertion remains logically unsound:
it('user.Save() does NOT resolve to Order#Save', () => {
const wrongSave = calls.find(c =>
c.target === 'Save' && c.source === 'Main' && c.targetFilePath.includes('Order.cs'),
);
// If both are resolved, at least one must be from the correct file
if (wrongSave) {
const correctSave = calls.find(c =>
c.target === 'Save' && c.source === 'Main' && c.targetFilePath.includes('User.cs'),
);
expect(correctSave).toBeDefined();
}
});order.Save() is also called from Main and correctly resolves to Order#Save (in Order.cs). So wrongSave is always found (the legitimate Main → Order#Save edge exists). The if (wrongSave) branch always executes and then just confirms correctSave also exists — which it always will. The test passes regardless of whether user.Save() was incorrectly resolved to both User#Save AND Order#Save.
Compare to the Rust fix which correctly uses strict expect(wrongSave).toBeUndefined() — that works because process_user and process_repo are separate source functions.
Fix: Replace Main with separate ProcessUser() and ProcessOrder() methods in the fixture, then check c.source === 'ProcessUser' to make the negative assertion meaningful — the same pattern the Rust test now uses.
🟡 Bug: extractReturnTypeName returns bare wrapper type names instead of undefined
Location: gitnexus/src/core/ingestion/call-processor.ts:494
The WRAPPER_GENERICS set (Task, Promise, Future, Option, Result, etc.) is only checked inside the genericMatch branch — when the type string contains <...>. For bare wrapper type names without a type argument, the function falls through to the uppercase identifier check and returns the wrapper name itself:
// genericMatch requires the pattern ^(\w+)\s*<(.+)>$
// Does NOT match bare 'Task', 'Promise', 'Option', etc.
const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
if (genericMatch) { /* WRAPPER_GENERICS checked here only */ }
// Falls through to:
if (!/^[A-Z_]\w*$/.test(text)) return undefined;
return text; // ← 'Task', 'Promise', 'Option' all pass this checkImpact: For C# public Task GetAsync() (void-like, no generic argument), extractReturnTypeName('Task') returns 'Task'. The scanner creates a binding result → 'Task'. In verifyConstructorBindings, ctx.resolve('Task', filePath) is called — for most projects this resolves to nothing (system types not indexed), so the binding silently fails. However, if user code defines a class named Task, Option, or Result (legitimate in Rust/functional-style code), a false binding is created.
The same applies to Go function declarations returning error (caught by PRIMITIVE_TYPES), but not to Rust None, Kotlin Nothing, or any wrapper type used bare.
Fix: After the genericMatch block, add a check: if (WRAPPER_GENERICS.has(text)) return undefined; to prevent bare wrapper type names from creating bindings.
🟡 Gap: JSDoc optional @param {Type} [optionalName] not captured
Location: gitnexus/src/core/ingestion/type-extractors/typescript.ts:36
const JSDOC_PARAM_RE = /@param\s*\{([^}]+)\}\s+(\w+)/g;
// ^^^^ only captures simple identifiersThe JSDoc convention for optional parameters uses brackets: @param {User} [user] or @param {User} [user=null]. The (\w+) pattern requires a plain word character sequence and will not match [user] or [user=null]. Any function with optional parameters documented via JSDoc will have those parameter type bindings silently dropped.
This means that in JavaScript codebases using JSDoc to document optional typed parameters, the type resolution for those parameters won't work even when @param {Type} [name] is correctly documented.
Fix: Update JSDOC_PARAM_RE to optionally match bracketed parameter names: replace (\w+) with \[?(\w+) (or a more complete pattern that handles default values like [name=default]).
🟢 Observation: Kotlin extractMethodSignature gap — still documented but unfixed
Location: gitnexus/src/core/ingestion/utils.ts:661–669 and gitnexus/test/integration/resolvers/kotlin.test.ts:490
The lenient Kotlin test documents a known gap: fun getUser(): User return types are not captured by extractMethodSignature because it only looks for type_annotation and return_type children, but Kotlin uses user_type directly. The extractKotlinDeclaration extractor (which correctly uses findChildByType(varDecl, 'user_type')) confirms this AST difference.
Until a Kotlin-specific extraction case is added to extractMethodSignature (analogous to the existing Go result field and Rust return_type field cases at lines 618–643), SymbolDefinition.returnType will remain undefined for all Kotlin functions, and the lenient test will never become strict.
Accuracy Tracking (All 12 Passes)
| Review | Findings | Accuracy |
|---|---|---|
| 1st | TS/JS no scanner; Ruby extractVarName gaps; 4 fixtures untested | ❌ All wrong |
| 2nd | C# member_access_expression; PHP member_call_expression |
❌ Both wrong |
| 3rd | Ruby false positive; Go selector_expression; Ruby @return missing |
✅ All correct — fixed in 36ca3a3 |
| 4th | normalizeJsDocType strips generics; PHP attribute walker; PHP $this receiver |
✅ All correct — fixed in fda19a9 |
| 5th | sanitizeReturnType dot corruption; async test false positives; Ruby constant LHS |
✅ All correct — fixed in 9fbbe2d |
| 6th | Sync JSDoc false positive; Ruby factory false positive; nested wrapper generics | ✅ All correct — fixed in 6465b8d |
| 7th | Kotlin disambiguation unproven; Swift navigation_suffix; Rust lifetime wrapper |
✅ All correct — fixed in 5caaae9 |
| 8th | Kotlin extractMethodSignature; Java receiver disambiguation; Rust ::default() |
✅ Rust fixed; Java architectural; Kotlin confirmed root cause |
| 9th | lbug-adapter.ts batch escapeValue; Rust ::default(); Kotlin import resolution |
✅ All correct — fixed in 1daebc0 |
| 10th | C# await unwrap; Rust .await unwrap; PHP \ namespace; fallback escaping |
✅ All correct — fixed in c3b635a |
| 11th | C# async fixture false positive; Rust async fixture false positive; Kotlin extractMethodSignature |
✅ C#/Rust fixtures fixed in bb420d2; Kotlin acknowledged |
| 12th (this) | C# negative assertion logically broken; bare wrapper type returns wrong value; JSDoc optional params | — |
Final Resolution Matrix (Post-bb420d2)
| Pattern | TS/JS | Python | Java | Kotlin | Go | Rust | C# | PHP | Ruby | Swift |
|---|---|---|---|---|---|---|---|---|---|---|
var: T = ... explicit |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | n/a | ✅ |
var = new Class() / Class.new |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
var = fn() return inference |
✅ | ✅ | ✅ (var) | ✅ | ✅ | ✅ | ✅ | ✅ | ||
var = await fn() / .await |
✅ | n/a | n/a | n/a | n/a | ✅ | ✅ | n/a | n/a | n/a |
var, err := fn() multi-return |
n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a | n/a | n/a |
var?.method() null-conditional |
n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a | n/a |
$this->fn() receiver hint |
n/a | n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | n/a |
| Doc-comment return type | JSDoc ✅ | n/a | n/a | n/a | n/a | n/a | PHPDoc ✅ | PHPDoc ✅ | YARD ✅ | n/a |
Bare wrapper type names (Task, Promise) |
— | — | — | — | — | — | — | |||
JSDoc @param {Type} [optionalName] |
❌ not captured | — | — | — | — | — | — | — | — | — |
Recommended Next Steps (Priority Order)
-
Fix C# async-binding negative assertion — Split
MainintoProcessUser()/ProcessOrder()in the fixture. This enables a strictexpect(wrongSave).toBeUndefined()onc.source === 'ProcessUser', proving true disambiguation rather than just "both edges exist." -
Fix bare wrapper type in
extractReturnTypeName— Addif (WRAPPER_GENERICS.has(text)) return undefined;after the generic match block. PreventsextractReturnTypeName('Task')from returning'Task', which creates a useless binding. Low risk, high correctness. -
Fix JSDoc optional parameter regex — Change
(\w+)to match[optionalName]form inJSDOC_PARAM_RE. Common in real-world JSDoc APIs. -
Kotlin
extractMethodSignaturereturn type — Add a Kotlin-specific extraction for function return type (user_typechild offunction_declaration) analogous to the Goresultfield and Rustreturn_typefield cases. Once fixed, update the lenientkotlin.test.ts:490assertions to strict disambiguation checks. -
Swift infrastructure — All 11 Swift tests remain skipped due to tree-sitter-swift build failure on Node 22. Infrastructure issue beyond PR scope.
| Branch |
… optional params
- Split Program.cs Main into ProcessUser/ProcessOrder so negative
assertions use strict toBeUndefined() (matching Rust pattern)
- Guard bare wrapper types (Task, Promise, Option…) in
extractReturnTypeName — return undefined instead of the wrapper name
- Update JSDOC_PARAM_RE to capture @param {Type} [optionalName] syntax
…pe extractors (abhigyanpatwari#284) * feat: Phase 3 — return type inference, generic args extraction, Ruby YARD type extractor Three architectural improvements to the type resolution system: 1. Return type inference — wire extractMethodSignature returnType through SymbolDefinition into call-processor. When var = callee() and callee has a known return type, bind var to that type. Handles Promise<T> unwrapping, nullable stripping, pointer/reference removal. 2. Generic type argument extraction — new extractGenericTypeArgs() utility that extracts type parameters from List<User> → ['User']. Handles TS/Java/Kotlin/C#/Rust generic syntax. Building block for for-loop variable typing. 3. Ruby dedicated type extractor — replaces the stub with YARD annotation parsing (@param name [Type]), handling qualified types, nullable types, and singleton methods. Ruby now has real type resolution. Unit tests: 127 → 192+ (type-env) + 65 (symbol-table, call-processor) + 18 (generics) Integration tests: 8+ new test cases with fixtures across TS/Python/Go/Java/Ruby * fix: Phase 3 gaps — WRAPPER_GENERICS correctness, Ruby :: qualifier, namespaced constructors - Remove collection types (List, Array, Vec, Set) from WRAPPER_GENERICS to prevent false CALLS edges (e.g. List<User> no longer unwraps to User) - Add :: qualifier handling in extractReturnTypeName for Ruby/C++/Rust namespaced types - Add Ruby `constant` and `scope_resolution` node types to shared extractors - Extract shared extractRubyConstructorAssignment helper (dedup type-env.ts + ruby.ts) - Add integration tests for return type inference: Python, TypeScript, Go, Java, Ruby - Add Ruby namespaced constructor fixture (Models::UserService.new) - Add unit tests for collection reclassification and :: qualifiers * feat: Phase 4 — CONSTRUCTOR_BINDING_SCANNERS for all languages + return type inference tests Add CONSTRUCTOR_BINDING_SCANNERS for 6 missing languages, completing return type inference coverage across all 11 supported languages: - TypeScript/JS: variable_declarator with call_expression, unwraps await - Go: short_var_declaration single-assignment (skips multi-return, new/make) - Java: local_variable_declaration with `var` type + method_invocation - C#: variable_declaration with implicit_type (var) + invocation_expression - Rust: let_declaration without type annotation, handles mut_pattern - PHP: assignment_expression with function_call_expression Also adds property_identifier to extractSimpleTypeName for qualified member calls (repo.getUser → getUser), fixing namespaced constructor inference that was previously a known limitation. Integration tests added for all 11 languages with correct label assertions (Function vs Method per language's tree-sitter queries). * refactor: merge CONSTRUCTOR_BINDING_SCANNERS into per-language LanguageTypeConfig Eliminates the parallel dispatch map in type-env.ts by moving all 11 constructor binding scanners into their respective type-extractors/*.ts files as `scanConstructorBinding` on LanguageTypeConfig. - Add ConstructorBindingScanner type to types.ts - Add shared helpers: hasTypeAnnotation, unwrapAwait, extractCalleeName - Move scanners to typescript.ts, jvm.ts, python.ts, php.ts, go.ts, rust.ts, swift.ts, c-cpp.ts, csharp.ts, ruby.ts - Fix `any` types in C# scanner → SyntaxNode | null - Delete ~300 lines from type-env.ts (CONSTRUCTOR_BINDING_SCANNERS map) - Update buildTypeEnv to use config.scanConstructorBinding All 143 type-env unit tests and all 10 language integration suites pass. * fix: remove unused import, fix any type in Java scanner, update stale comment - Remove unused extractCalleeName import from jvm.ts - Fix (c: any) → (c: SyntaxNode) in Java scanner - Update stale CONSTRUCTOR_BINDING_SCANNERS reference in ruby.ts comment * fix: C# and PHP return type inference — scanner fixes, method signature extraction, and cross-file resolution Addresses code review findings on PR abhigyanpatwari#284: C# scanner (csharp.ts): - Fix type node lookup: iterate children instead of childForFieldName('type') which returns undefined in tree-sitter-c-sharp - Fix initializer lookup: handle direct invocation_expression children (no equals_value_clause wrapper in tree-sitter-c-sharp) C# return type extraction (utils.ts): - Add 'returns' field check to extractMethodSignature — tree-sitter-c-sharp uses 'returns', not 'type', for method return types C# cross-file resolution (call-processor.ts + fixture): - Add constructor binding verification to sequential processCalls path (was only in the worker processCallsFromExtracted path) - Add ReturnType.csproj to csharp-return-type fixture - Update fixture namespaces to use ReturnType.Models/ReturnType.Services prefix (matches real C# project conventions) PHP scanner (php.ts): - Extend scanConstructorBinding to handle member_call_expression ($this->getUser() patterns), not just function_call_expression Shared (shared.ts): - Add member_access_expression to extractSimpleTypeName qualified-names block (C# method calls like svc.GetUser()) Tests: - Add Repo.cs/Repo.php disambiguation fixtures (two Save methods) - Strengthen C# and PHP return type tests with hard disambiguation assertions - Add C# scanner unit tests and return type extraction test * feat: per-language ReturnTypeExtractor + doc-comment @param parsing for PHP, JS, Ruby Add ReturnTypeExtractor to LanguageTypeConfig interface with implementations for Ruby (YARD @return), PHP (PHPDoc @return), and JS/TS (JSDoc @returns). The fallback is wired in both parsing-processor and parse-worker paths, activating only when extractMethodSignature finds no AST-based return type. Also add doc-comment @param type extraction for PHP and JS/TS, following Ruby's existing collectYardParams pattern. This enables parameter.method() resolution in loosely-typed codebases using PHPDoc @param or JSDoc @param. Additional fixes from PR abhigyanpatwari#284 code review: - Go: add selector_expression + field_identifier to extractSimpleTypeName (enables package-qualified factory calls like models.NewUser()) - Ruby: broaden scanConstructorBinding to capture plain call assignments (user = get_user()) in addition to Class.new patterns - Ruby: harden return-type fixture with disambiguation (two save methods) Test coverage: +14 new integration tests across Go, Ruby, PHP, JS/TS * fix: JSDoc async return type, PHP attribute walkers, and $this receiver disambiguation Three fixes from fourth-pass code review on PR abhigyanpatwari#284: 1. JSDoc `@returns {Promise<User>}` no longer stripped to `Promise` — extractReturnType now uses sanitizeReturnType (preserves generics) instead of normalizeJsDocType (which stripped them before extractReturnTypeName could unwrap WRAPPER_GENERICS). 2. PHP 8+ `#[Attribute]` and JS `@decorator` nodes no longer break doc-comment walkers. Both extractReturnType and collect*Params functions now skip attribute_list/decorator nodes instead of breaking on them as named siblings. 3. PHP `$this->method()` now provides receiverClassName for disambiguation. When two classes define the same method, the enclosing class narrows candidates via ownerId matching in call-processor, preventing false no-binding results. * fix: sanitizeReturnType dot corruption, JS test assertions, Ruby constant receiver - Remove redundant dot-path stripping from sanitizeReturnType that corrupted qualified names inside generics (e.g. Promise<models.User> → User>) - Split JS async fixture into separate files and add negative assertions to properly verify disambiguation (mirroring PHP test pattern) - Accept 'constant' node type in Ruby scanConstructorBinding for factory call assignments (SERVICE = build_service()) - Add 'constant' to SIMPLE_RECEIVER_TYPES so extractReceiverName handles Ruby constant receivers (SERVICE.process) * fix: nested generic arg splitting, JS/Ruby test false positives - Replace naive comma split in extractReturnTypeName with bracket-balanced extractFirstGenericArg so nested types like Future<Result<User, Error>> unwrap correctly instead of producing malformed "Result<User" - Add CompletableFuture to WRAPPER_GENERICS for Java async unwrapping - Split js-jsdoc-return-type fixture models.js into user.js/repo.js and add negative assertions to prove disambiguation (not just file match) - Split ruby-constant-factory-call fixture into separate service files and add negative assertions against AdminService resolution * fix: review findings — receiverClassName parity, Rust wrappers, Go multi-return, Kotlin/Swift qualified calls P1: Sequential path now includes receiverClassName narrowing for PHP $this->method() disambiguation (was missing vs worker path). P2: Added Rc/Arc/Weak/MutexGuard/Cow + 6 more Rust Deref types to WRAPPER_GENERICS (Box excluded — Java Swing collision). Extended Kotlin/Swift scanners to handle navigation_expression callees. Added Go multi-return support (user, err := f()) with blank/_/err/ok guard + AST-level first-return extraction in extractMethodSignature. P3: Extracted shared verifyConstructorBindings() eliminating 60 lines of duplication between sequential and worker paths. Added return-type inference integration tests for C++, Rust, Swift with competing methods and negative disambiguation assertions. * fix: Swift navigation_suffix unwrapping, Rust lifetime skipping, Kotlin disambiguation tests - Swift scanConstructorBinding: handle tree-sitter wrapping qualified identifiers in navigation_suffix nodes - Add extractFirstTypeArg to skip Rust lifetime parameters ('a, '_) when unwrapping wrapper generics like Ref<'_, User> - Kotlin tests: add Repo class fixture with competing save() methods to prove disambiguation; assert no spurious edges on known gap - Remove tree-sitter-kotlin from optionalDependencies (now regular dep) * fix: C# null-conditional calls, Ruby YARD bracket-balanced split, PHPDoc alternate order, escapeValue hardening - Add C# null-conditional call support (user?.Save()): tree-sitter query for conditional_access_expression, member_binding_expression in MEMBER_ACCESS_NODE_TYPES, receiver extraction via conditional_access_expression parent walk - Fix Ruby YARD type parsing for nested generics (Hash<Symbol, User>): replace naive split(',') with bracket-balanced splitter respecting <> depth - Add alternate YARD format (@param [Type] name) alongside standard (@param name [Type]) - Add alternate PHPDoc format (@param $name Type) alongside standard (@param Type $name) - Harden escapeValue in kuzu-adapter.ts: escape \n and \r to prevent Cypher injection - Integration tests: C# null-conditional fixture (5 tests), Ruby YARD generics fixture (6 tests) - Unit tests: PHPDoc alternate order (2 tests), C# null-conditional call-form (updated) * test: add Python static/classmethod integration tests (issue abhigyanpatwari#289) Verifies that classes using only @staticmethod/@classmethod have HAS_METHOD edges connecting them to their child methods. This was the root cause of issue abhigyanpatwari#289 where context() and impact() returned empty for such classes. Tests cover: HAS_METHOD edge emission, unique static method resolution (create_user, delete_user), and ambiguous same-named method handling (find_user on both UserService and AdminService — safely refused). * fix: lbug batch escapeValue newline hardening, Rust ::default() scanner exclusion - Apply \n/\r escaping to batch upsert escapeValue in lbug-adapter.ts:429 (missed instance of the CREATE-path fix from ec4dca4) - Exclude Rust ::default() from scanConstructorBinding to match extractInitializer behavior — avoids wasted cross-file lookups on the broadly-implemented Default trait - Unit tests: 2 new scanner exclusion tests (::default and ::new) - Integration tests: 6 new Rust ::default() constructor resolution tests with disambiguation fixture (User::default vs Repo::default) * fix: C#/Rust async await unwrap, PHP backslash namespace, fallback escaping - C# scanConstructorBinding: unwrap await_expression to find invocation_expression (var user = await svc.GetUserAsync() now produces constructor binding) - Rust scanConstructorBinding: unwrap .await postfix via shared unwrapAwait helper (let user = get_user().await now produces constructor binding) - extractReturnTypeName: handle PHP backslash namespace separator (\App\Models\User → User) - fallbackRelationshipInserts: match batch escapeValue hardening with \n/\r escaping Tests: 2 unit (type-env), 3 unit (call-processor), 7 integration (csharp+rust), 7 fixtures * fix: C#/Rust async-binding test false positives — add competing types and negative assertions C# fixture: add Order.cs with Order.Save(), change OrderService to return Task<Order> via GetOrderAsync, add negative assertion proving user.Save() does not resolve to Order#Save. Rust fixture: split models.rs into user.rs/repo.rs, make process_user and process_repo async fn, add bidirectional negative assertions proving no cross-contamination between User#save and Repo#save. * fix: C# async-binding broken assertion, bare wrapper type leak, JSDoc optional params - Split Program.cs Main into ProcessUser/ProcessOrder so negative assertions use strict toBeUndefined() (matching Rust pattern) - Guard bare wrapper types (Task, Promise, Option…) in extractReturnTypeName — return undefined instead of the wrapper name - Update JSDOC_PARAM_RE to capture @param {Type} [optionalName] syntax * fix: update symbol and relationship counts in documentation

Summary
Adds return-type-based call resolution, doc-comment type extraction (JSDoc, PHPDoc, YARD), and per-language type extractors across all 12 supported languages. Includes a major refactor consolidating constructor binding scanners into the existing
LanguageTypeConfigpattern, plus targeted fixes from 7 automated code review passes.Return Type Inference
returnTypetoSymbolDefinitionand wired it throughparsing-processor→symbol-table→call-processorvar = callee()and callee has a known return type, the variable is bound to that typePromise<User>→User,User | null→User,*User→User,models.User→UserDoc-Comment Type Extraction
@return ClassNameand@param Type $nameparsing, handles?Type,Type|null, namespaced types. Now also supports alternate format@param $name Type@returns {Type}and@param {Type} nameparsing, withsanitizeReturnTypepreserving generics for downstream unwrapping@return [Type]and@param name [Type]parsing, qualified type handling (Models::User→User). Now also supports alternate format@param [Type] nameand bracket-balanced generic splitting (Hash<Symbol, User>no longer breaks on inner commas)Per-Language Constructor Binding Scanners
scanConstructorBindingto all 12 languages (TS/JS, Java, Kotlin, C#, Go, Rust, PHP, C++, Swift, Python, Ruby)var = callee()patterns where no explicit type annotation existsLanguageTypeConfig—type-env.tsshrank by 305 linesGeneric Type Argument Extraction
extractGenericTypeArgs()utility:List<User>→['User'],Map<String, User>→['String', 'User']Ruby Type Extractor
ruby.tsextractor replacing the previous empty stubUser.new)C# Null-Conditional Call Support
user?.Save()viaconditional_access_expression→member_binding_expressionmember_binding_expressiontoMEMBER_ACCESS_NODE_TYPESand receiver extraction inextractReceiverNameSecurity Hardening
escapeValueinkuzu-adapter.tsnow escapes\nand\rto prevent Cypher query structure breaks from untrusted type stringsBug Fixes (7 review passes)
sanitizeReturnTypepreservesPromise<User>for async return type resolutioncollectJsDocParamsbackward sibling walk#[Attribute]nodes between PHPDoc and method no longer break extraction$this/self/staticreceiver disambiguation viareceiverClassNameselector_expressionfix in return type extractionanytype in scanner →SyntaxNode | nullHash<Symbol, User>comma bug)@param [Type] nameformat supportnavigation_suffixunwrapping inscanConstructorBindingRef<'_, User>→User)extractFirstGenericArgforPromise<Map<string, User>>Deferred Items (Follow-Up PRs)
Collected from 7 code review passes and reconciled against branch state. Full plan:
docs/plans/2026-03-15-feat-type-resolution-deferred-items-plan.mdNext PR: Correctness & Language Gaps
receiverKeycollision — same-name overloads share composite key, needs scope-aware prefix-scanresolveJvmMemberImport()requires 3+ segments butimport models.getUserhas 2 (import resolution gap injvm.ts:82)User.new.tap {},User.new.freeze) — outer call hides.newfrom scannerFuture PRs: Architecture Items (only with evidence of real missed edges)
extractGenericTypeArgs+ newLanguageTypeConfighook)a.b.c()(requires TypeEnv redesign, depth limit 3)if let,guard let,ispattern matching)Infrastructure
extractGenericTypeArgshas 0 production callers (60+219 LOC) — keep for for-loop typing or deletecollectPrecedingCommentsshared helper (dedup ~40 LOC across TS/PHP/Ruby)Test Plan
type-env.test.ts)call-processor.test.ts)call-form.test.ts)method-signature.test.ts)tsc --noEmit)