| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…ine (Phase 8.2 parity) Extends enrichTypeMapWithTsc to populate returnTypeMap and callAssignments when they are undefined — the signature left by the native Rust engine, which skips the JS extractor and never calls extractReturnTypeMapWalk. With this change, propagateReturnTypesAcrossFiles in build-edges.ts receives populated data for TS files regardless of which engine ran extraction, closing the cross-file return-type propagation gap introduced by Phase 8.2 (#1279). Two new helpers added to ts-resolver.ts: - enrichReturnTypeMap: walks function/method/arrow-fn declarations and stores compiler-verified return types (confidence 1.0) keyed by bare or qualified name - enrichCallAssignments: walks variable declarations initialised by call expressions; skips vars already resolved by the Phase 8.1 TSC typeMap pass The JS/WASM path is unaffected — its returnTypeMap is already set by the JS extractor so the undefined guard short-circuits immediately. Closes #1280
|
Claude finished @carlos-alm's task —— View job PR Review — feat(resolver): backfill returnTypeMap/callAssignments for native engine (Phase 8.2 parity)Root Cause AnalysisProblem Being Solved: The native Rust engine never runs extractReturnTypeMapWalk, leaving returnTypeMap and callAssignments as undefined on ExtractorOutput. The propagateReturnTypesAcrossFiles function in build-edges.ts guards on symbols.callAssignments?.length, which evaluates to falsy for undefined, so cross-file return-type propagation was silently skipped for all native-engine parse results. Why This Approach: Rather than patching every downstream consumer to handle undefined defensively, the fix runs a TSC-backed backfill inside the already-existing enrichTypeMapWithTsc pass — the only place where both the TS compiler and fileSymbols are available together. The guard (returnTypeMap === undefined) is a clean sentinel that distinguishes native from WASM without needing an explicit engine flag. This is architecturally sound. Risk Assessment: Low. The guard ensures WASM/JS paths are entirely unaffected. The new helpers touch only in-memory state; no DB I/O, no schema changes. The approach degrades gracefully (no-op) when typescript is unavailable. Phase 2: Configuration Change DetectionNo biome/lint rules relaxed, no timeouts increased, no TypeScript strict flags disabled, no module resolution changes. Clean. Phase 3: Test IntegrityTests are solid:
No weakened assertions. The WASM guard test (ts-resolver.test.ts:131) correctly asserts the exact same object reference (toBe(preExisting)) — strong. One note: the guard test uses confidence: 0.85 on a pre-existing entry and does not assert that enrichTypeMapWithTsc left it at 0.85 (it does not upgrade low-confidence entries in returnTypeMap when already set). This is correct behavior — the guard bails entirely when returnTypeMap !== undefined. Phase 4: Code QualityConcerns (minor to moderate): 1. enrichCallAssignments does not de-duplicate — ts-resolver.ts:385 2. Arrow function backfill may miss anonymous initializers — ts-resolver.ts:311–316 3. currentClass state tracking uses function-scoped mutation — ts-resolver.ts:289–299 4. Checker can be called after program goes out of scope in a future refactor 5. callAssignments linked to returnTypeMap === undefined is a surprising coupling — ts-resolver.ts:125–131 if (symbols.returnTypeMap === undefined) {
symbols.returnTypeMap = new Map();
symbols.callAssignments = []; // ← both are set together
...
}The assumption is that both fields are either both defined (WASM) or both undefined (native). This matches the current ExtractorOutput contract. However, the type allows returnTypeMap and callAssignments to be independently undefined. If a future extractor sets returnTypeMap but not callAssignments, this branch skips enrichCallAssignments silently. A more robust guard would be: if (symbols.returnTypeMap === undefined) {
symbols.returnTypeMap = new Map();
enrichReturnTypeMap(ts, sourceFile, checker, symbols.returnTypeMap);
}
if (symbols.callAssignments === undefined) {
symbols.callAssignments = [];
enrichCallAssignments(ts, sourceFile, symbols.typeMap, symbols.callAssignments);
}6. backfilledFiles counter includes files where backfill ran but produced zero results Phase 5: Backlog Compliance
Critical ConcernsNone that block merge. Concern #5 (independent guards for returnTypeMap vs callAssignments) is the most actionable — a small defensive improvement worth considering before shipping. Final Recommendation## Root Cause Analysis Problem Being Solved: Native engine left returnTypeMap/callAssignments undefined, silently disabling cross-file return-type propagation for all native-parsed TS files. Why This Approach: Backfill inside the existing TSC pass is the right seam — both the compiler and fileSymbols are already available there. Risk Assessment: Low — WASM/JS paths unaffected by the undefined sentinel guard; no DB changes; degrades gracefully without typescript package. ## Backlog Compliance - Zero-dep: ✓ - Foundation-aligned: ✓ - Problem-fit: 5 — directly fixes silent accuracy regression for native-engine users - Breaking: No - Tier: 1 ## Critical Concerns - Minor: callAssignments push without dedup (safe today, fragile for future watch-mode) - Minor: coupled guard for returnTypeMap+callAssignments could silently skip callAssignments if a future extractor sets one but not the other (consider independent guards) ## Final Recommendation - Rating: ⭐⭐⭐⭐ (4/5) - Action: APPROVE WITH CONDITIONS - Reasoning: Correct fix, well-scoped, good test coverage, no regressions. Suggest addressing the coupled guard (concern #5 above) before or shortly after merge — either inline or as a follow-up issue — to keep the contract explicit as the extractor landscape grows. |
Sorry, something went wrong.
Greptile SummaryThis PR extends the Phase 8.1 TSC enrichment pass to backfill returnTypeMap and callAssignments for the native Rust engine, which leaves those fields undefined after extraction. Two new helpers — enrichReturnTypeMap and enrichCallAssignments — walk each TypeScript source file using the compiler API and populate these fields when they are absent, including Promise unwrapping for async functions.
Confidence Score: 3/5The backfill logic has a gap in its recursion guard that allows local variable declarations inside constructor and accessor bodies to enter returnTypeMap under bare names, causing incorrect cross-file type resolution. The body-descent stop in enrichReturnTypeMap covers FunctionDeclaration and MethodDeclaration but misses ConstructorDeclaration, GetAccessorDeclaration, and SetAccessorDeclaration. A local arrow function inside a constructor is matched by the VariableDeclaration branch and added to returnTypeMap under its bare name, exactly the cross-file pollution the doc comment says is prevented. The existing test only exercises a MethodDeclaration body so CI does not catch this gap. src/domain/graph/resolver/ts-resolver.ts — the visit function inside enrichReturnTypeMap needs additional early-return guards for ConstructorDeclaration, GetAccessorDeclaration, and SetAccessorDeclaration. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[enrichTypeMapWithTsc] --> B{sourceFile found?}
B -- No --> SKIP[skip file]
B -- Yes --> C[enrichSourceFile typeMap Phase 8.1]
C --> D{returnTypeMap === undefined?}
D -- No JS/WASM path --> F
D -- Yes native engine --> E[enrichReturnTypeMap walk SourceFile]
E --> E1{node kind?}
E1 -- FunctionDeclaration --> E2[writeEntry stop]
E1 -- MethodDeclaration --> E3[writeEntry stop]
E1 -- ClassDeclaration/Expression --> E4[enter class scope forEachChild stop]
E1 -- VariableDeclaration arrow/fn init --> E5[writeEntry stop]
E1 -- ConstructorDeclaration GetAccessor SetAccessor --> E6[forEachChild body descent NOT guarded]
E1 -- other --> E7[forEachChild]
E --> F{callAssignments === undefined?}
F -- No --> G
F -- Yes --> H[enrichCallAssignments two-pass dedup]
H --> H1[collect candidates by varName]
H1 --> H2[push only where uniqueCallees.size===1]
H2 --> G[debug log backfilledFiles count]
Reviews (5): Last reviewed commit: "fix(resolver): exclude ambiguous callAss..." | Re-trigger Greptile |
Sorry, something went wrong.
| if (fnName && sigNode) { | ||
| try { | ||
| const sig = checker.getSignatureFromDeclaration(sigNode); | ||
| if (sig) { | ||
| const retType = checker.getReturnTypeOfSignature(sig); | ||
| const sym = retType.getSymbol() ?? retType.aliasSymbol; | ||
| if (sym) { | ||
| const typeName = sym.getName(); | ||
| if ( | ||
| typeName && | ||
| typeName !== '__type' && | ||
| typeName !== '__object' && | ||
| !SKIP_TYPE_NAMES.has(typeName) | ||
| ) { | ||
| const existing = returnTypeMap.get(fnName); | ||
| if (!existing || existing.confidence < 1.0) | ||
| returnTypeMap.set(fnName, { type: typeName, confidence: 1.0 }); | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| // skip — checker can throw on malformed nodes | ||
| } |
There was a problem hiding this comment.
Async functions silently produce no returnTypeMap entry
checker.getReturnTypeOfSignature for an async function createUser(): Promise<User> returns a Promise<User> type whose getSymbol() yields Promise — which is in SKIP_TYPE_NAMES. The entry is silently skipped, so async functions never appear in returnTypeMap. Because the majority of real-world TypeScript service/repository methods are async, the Phase 8.2 backfill will have little practical effect on the native engine path for most codebases. All six new tests use synchronous return types, so this gap is invisible in CI. A call to checker.getTypeArguments(retType)[0] would let you unwrap the inner type when the outer type is Promise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed in ad9bf59. Added Promise unwrapping in resolveReturnTypeName: when the outer return type symbol is Promise, we call checker.getTypeArguments(retType)[0] to unwrap the inner type. Added a test (backfills returnTypeMap for async functions by unwrapping Promise<T>) that confirms async function fetchOrder(): Promise<Order> and async loadOrder(): Promise<Order> both produce { type: 'Order', confidence: 1.0 } entries.
Sorry, something went wrong.
| function enrichCallAssignments( | ||
| ts: TsModule, | ||
| sourceFile: import('typescript').SourceFile, | ||
| typeMap: Map<string, TypeMapEntry>, | ||
| callAssignments: CallAssignment[], | ||
| ): void { | ||
| function visit(node: import('typescript').Node): void { | ||
| if ( | ||
| ts.isVariableDeclaration(node) && | ||
| ts.isIdentifier(node.name) && | ||
| node.initializer && | ||
| ts.isCallExpression(node.initializer) | ||
| ) { | ||
| const varName = node.name.text; | ||
| if (!typeMap.has(varName)) { | ||
| const call = node.initializer; | ||
| let calleeName: string | null = null; | ||
| let receiverTypeName: string | undefined; | ||
|
|
||
| if (ts.isIdentifier(call.expression)) { | ||
| calleeName = call.expression.text; | ||
| } else if (ts.isPropertyAccessExpression(call.expression)) { | ||
| calleeName = call.expression.name.text; | ||
| const obj = call.expression.expression; | ||
| if (ts.isIdentifier(obj)) { | ||
| const entry = typeMap.get(obj.text); | ||
| if (entry && typeof entry === 'object') receiverTypeName = entry.type; | ||
| } | ||
| } | ||
|
|
||
| if (calleeName) callAssignments.push({ varName, calleeName, receiverTypeName }); | ||
| } | ||
| } | ||
|
|
||
| ts.forEachChild(node, visit); | ||
| } | ||
|
|
||
| ts.forEachChild(sourceFile, visit); | ||
| } |
There was a problem hiding this comment.
Local (non-exported) function return types captured in returnTypeMap
enrichReturnTypeMap walks the entire source file recursively, including function bodies. Arrow functions and function expressions assigned to local variables inside method bodies (e.g., const helper = (): HelperType => ... inside MyClass.doWork) are added to returnTypeMap under their bare variable name (helper). If another file has a callAssignment whose calleeName is also helper, propagateReturnTypesAcrossFiles would incorrectly resolve the cross-file variable to HelperType. The enrichSourceFile pass avoids this with its "unambiguous names only" heuristic; enrichReturnTypeMap has no equivalent guard.
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed in ad9bf59. The visitor now stops recursion at function/method body boundaries: isFunctionDeclaration, isMethodDeclaration all return early after recording the entry, so the visitor never descends into their bodies. Any VariableDeclaration with an arrow/function-expression initializer that we encounter is therefore guaranteed to be at module scope — never inside a method body. Added a test (does NOT capture local (method-body-scoped) helper functions in returnTypeMap) that verifies a const helper = (): InnerResult => ... inside MyService.doWork does NOT appear in returnTypeMap under the bare name helper.
Sorry, something went wrong.
Codegraph Impact Analysis7 functions changed → 9 callers affected across 3 files
|
Sorry, something went wrong.
- Unwrap Promise<T> in enrichReturnTypeMap so async functions produce a returnTypeMap entry for their inner type (fixes silent gap for async-heavy codebases where SKIP_TYPE_NAMES would otherwise swallow all entries) - Stop recursion at function/method body boundaries to exclude locally-scoped helper functions from returnTypeMap, preventing spurious cross-file type matches - Split coupled returnTypeMap/callAssignments guard into independent checks so a future extractor that sets one but not the other is handled correctly - Add two tests: async Promise<T> unwrapping, local-function exclusion
|
Addressed Claude's feedback from the automated review: Concern #5 (coupled returnTypeMap/callAssignments guard): Split into independent guards in ad9bf59. Each field now has its own === undefined check so a future extractor that sets one but not the other is handled correctly without silently skipping either enrichment pass. Lint (format error): Fixed the long ternary in the debug log call — reformatted to wrap at 80 chars (formatter required the break). Greptile P2 — async Promise: Addressed — see inline reply. resolveReturnTypeName now unwraps Promise<T> → T via checker.getTypeArguments. Greptile P2 — local functions in returnTypeMap: Addressed — see inline reply. Visitor stops recursion at function/method body boundaries, with a test to confirm. Concern #1 (callAssignments dedup): Not addressed — the push without dedup is safe today because the pass runs once per build. Adding dedup would require comparing all fields of CallAssignment objects. Given the current call-once contract, this is a valid future hardening but not a correctness issue today. If watch-mode ever calls this twice, the dedup logic should be added then. |
Sorry, something went wrong.
Sorry, something went wrong.
Sorry, something went wrong.
|
Claude finished @carlos-alm's task —— View job Follow-up Review
Verification of Addressed ConcernsAll concerns from the original review and both Greptile P2 issues have been properly addressed in ad9bf59. Here's what I found: Concern #5 (coupled guard) — ✅ Fixed correctly ts-resolver.ts:128–138 — now uses two independent === undefined guards: if (symbols.returnTypeMap === undefined) {
symbols.returnTypeMap = new Map();
enrichReturnTypeMap(ts, sourceFile, checker, symbols.returnTypeMap);
if (symbols.returnTypeMap.size > 0) didBackfill = true;
}
if (symbols.callAssignments === undefined) {
symbols.callAssignments = [];
enrichCallAssignments(ts, sourceFile, symbols.typeMap, symbols.callAssignments);
if (symbols.callAssignments.length > 0) didBackfill = true;
}Each field is handled independently. A future extractor that sets one but not the other is handled correctly. Greptile P2 — Async Promise<T> unwrapping — ✅ Fixed correctly ts-resolver.ts:312–333 — resolveReturnTypeName now unwraps Promise<T>: const outerSym = retType.getSymbol() ?? retType.aliasSymbol;
if (outerSym?.getName() === 'Promise') {
const args = checker.getTypeArguments(retType as import('typescript').TypeReference);
if (args.length > 0) retType = args[0]!;
}Test at line 154 confirms both async function fetchOrder(): Promise<Order> and async loadOrder(): Promise<Order> produce { type: 'Order', confidence: 1.0 }. Solid. Greptile P2 — Local function body scoping — ✅ Fixed correctly ts-resolver.ts:349–392 — the visitor now stops recursion at function/method body boundaries. isFunctionDeclaration and isMethodDeclaration branches both return after recording the entry (no ts.forEachChild into the body). isVariableDeclaration with arrow/function initializer also returns without recursing. Only "other node kinds" fall through to ts.forEachChild. Test at line 179 asserts helper (defined inside MyService.doWork) does NOT appear in returnTypeMap. This is the correct fix. One small observation (non-blocking): enrichCallAssignments at ts-resolver.ts:405–434 still recurses fully into function bodies via ts.forEachChild — there's no boundary stopping. This is consistent behavior since callAssignments are about variable assignments, and unlike returnTypeMap keys, varNames in callAssignments are not used as bare lookup keys in returnTypeMap — they're the target of resolution, not the source. The cross-file spurious match risk does not apply here. This is intentionally different from enrichReturnTypeMap and is correct. Format fix — ✅ ts-resolver.ts:141–147 — the debug log ternary is correctly wrapped to stay within 80 chars. Test Coverage Assessment8 tests total (was 6, now 8 with the async and local-scope tests):
All originally-missing cases are now covered. The test for async functions uses both a module-level async function and an async method in a class, which exercises both code paths in enrichReturnTypeMap. Final AssessmentAll raised concerns have been addressed correctly. The implementation is sound, the guard split is clean, the async unwrapping is correct, and the scoping fix is verified by test. No remaining concerns. Rating: ⭐⭐⭐⭐⭐ (5/5) — LGTM |
Sorry, something went wrong.
|
Fixed in 0b1740b: enrichCallAssignments now uses the same two-pass "unambiguous names only" heuristic as enrichSourceFile. First pass collects all {varName, calleeName} candidates; second pass only pushes entries where a given varName maps to exactly one distinct calleeName. Ambiguous bindings (same variable name, different callees across scopes) are excluded to prevent propagateReturnTypesAcrossFiles from silently resolving the wrong type. New test: 'excludes ambiguous callAssignments where same varName maps to different callees' verifies that const result = getA() in one method and const result = getB() in another produces zero entries in callAssignments for that varName. |
Sorry, something went wrong.
Sorry, something went wrong.
Sorry, something went wrong.
|
Claude finished @carlos-alm's task —— View job Follow-up Review — 0b1740b
Verification of 0b1740b — Two-Pass Unambiguous Heuristic for enrichCallAssignmentsFix is correct and complete. enrichCallAssignments at ts-resolver.ts:412–471 now mirrors the exact two-pass strategy from enrichSourceFile:
for (const entries of candidates.values()) {
const uniqueCallees = new Set(entries.map((e) => e.calleeName));
if (uniqueCallees.size === 1) {
callAssignments.push(entries[0] as CallAssignment);
}
}This directly prevents the Greptile-flagged scenario where const result = getA() and const result = getB() in two different methods both landing in callAssignments, causing propagateReturnTypesAcrossFiles to silently pick the wrong type. Test coverage — new test at ts-resolver.test.ts:131: The test 'excludes ambiguous callAssignments where same varName maps to different callees' uses a file with:
Assertions:
This is the exact scenario that was problematic. The test is strong. Full Concern Resolution Summary
Rating: ⭐⭐⭐⭐⭐ (5/5) — LGTM All concerns raised in the original review and both Greptile P2 issues have been addressed. The implementation is correct, well-tested, and architecturally consistent with the existing enrichSourceFile strategy. |
Sorry, something went wrong.
| // Second pass: only push entries where varName maps to exactly one distinct | ||
| // calleeName. Ambiguous varNames (same name, different callees across scopes) | ||
| // are excluded to avoid silently resolving the wrong type cross-file. | ||
| for (const entries of candidates.values()) { | ||
| const uniqueCallees = new Set(entries.map((e) => e.calleeName)); | ||
| if (uniqueCallees.size === 1) { | ||
| callAssignments.push(entries[0] as CallAssignment); | ||
| } | ||
| } |
There was a problem hiding this comment.
Receiver-type ambiguity not caught by calleeName deduplication
The uniqueness check only tests calleeName, but receiverTypeName can differ across entries with the same varName and calleeName. For example, const x = a.process() (where a: TypeA) and const x = b.process() (where b: TypeB) inside two different method bodies both produce calleeName = "process", so uniqueCallees.size === 1 — yet entries[0] carries the first occurrence's receiverTypeName. propagateReturnTypesAcrossFiles then performs globalReturnTypeMap.get("TypeA.process") instead of "TypeB.process", silently injecting the wrong type into typeMap. The fix is to include receiverTypeName in the uniqueness key: new Set(entries.map((e) => \${e.receiverTypeName ?? ''}:${e.calleeName}`))`.
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Summary
Test plan
Closes #1280