Skip to content

feat(analysis): inter-procedural return-type propagation (Phase 8.2)#1279

Merged
carlos-alm merged 4 commits into
mainfrom
feat/inter-procedural-type-propagation
Jun 2, 2026
Merged

feat(analysis): inter-procedural return-type propagation (Phase 8.2)#1279
carlos-alm merged 4 commits into
mainfrom
feat/inter-procedural-type-propagation

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Adds extractReturnTypeMapWalk to the JS/TS extractor: scans every function/method definition and records its return type (confidence 1.0 from explicit TS annotations, 0.85 from return new Constructor())
  • Propagates return types intra-file: const x = createUser()x gets type User in the typeMap; method chains like getService().getRepo() are resolved recursively up to 3 hops with confidence decaying 0.1 per hop (0.9 → 0.8 → 0.7)
  • Cross-file propagation in build-edges.ts: unresolved call assignments are resolved against imported files' returnTypeMaps before both native and JS call-edge paths run
  • Adds analysis.typePropagationDepth: 3 to DEFAULTS/CodegraphConfig for future tunability

Test plan

  • npm test — all 2790 tests pass, 12 new tests covering: returnTypeMap extraction (annotations, inferred, method qualification, arrow fns), single-hop propagation, method-call propagation, chain resolution, confidence values, and edge cases (no-override, missing return info)
  • npx tsc --noEmit — clean

Closes #[roadmap Phase 8.2]

Extends type tracking beyond single-function scope. The JS/TS extractor
now records each function's declared or inferred return type in a new
`returnTypeMap`, then propagates that information to variables assigned
from call expressions before call-edge resolution runs.

Intra-file: `const x = createUser()` resolves x's type when `createUser`
has a TS return annotation or a `return new Constructor()` body. Method
chains (`getService().getRepo()`) are resolved recursively up to three
hops with confidence decaying 0.1 per hop (1.0 → 0.9 → 0.8 → 0.7).

Cross-file: `build-edges.ts` collects unresolved call assignments from
each extractor output and propagates return types from imported files
before both the native and JS call-edge paths run, so both engines
benefit automatically.

Expected impact: +10–15 pp on caller coverage for factory patterns,
builder patterns, and method chains.
@claude

claude Bot commented Jun 1, 2026

Copy link
Copy Markdown

Claude finished @carlos-alm's task —— View job


PR Review: feat(analysis): inter-procedural return-type propagation (Phase 8.2)

Tasks:

  • Gather context
  • Read changed files
  • Phase 1: Root cause analysis
  • Phase 2: Configuration change detection
  • Phase 3: Test integrity
  • Phase 4: Code quality
  • Phase 5: Backlog compliance
  • Post final review

Root Cause Analysis

Problem Being Solved: When AI agents query codegraph for const user = createUser() followed by user.getProfile(), the call edge to getProfile couldn't be resolved because the extractor didn't know user's type. The PR adds a two-phase propagation: (1) scan every function/method definition to build a returnTypeMap, then (2) use it to annotate variables assigned from call results — both intra-file and cross-file.

Why This Approach: This is the right architectural fit — building on the existing typeMap mechanism, which already drives method-call resolution in resolveCallTargets. The new returnTypeMap feeds into typeMap, so no call-edge resolution logic needs changes. The cross-file propagation is cleanly inserted in build-edges.ts before both native and JS call-edge paths run, so both engines benefit.

Risk Assessment: Medium. The new walk (extractReturnTypeMapWalk) runs on every JS/TS file every build. The change is JS/TS only — no other language extractors or the Rust native engine are updated. This creates a dual-engine gap (see Critical Concerns below).


Phase 2: Configuration Changes

No lint/biome rules relaxed. Config changes are:

  • src/infrastructure/config.ts:82 — adds typePropagationDepth: 3 to DEFAULTS.analysis
  • src/types.ts:1217 — adds the corresponding field to CodegraphConfig.analysis

Both follow the documented pattern for adding behavioral constants. The value 3 is meaningful (constant MAX_PROPAGATION_DEPTH in javascript.ts:83 is also hardcoded to 3, and it doesn't actually read the config value — see Critical Concerns). ✓ No rules relaxed.


Phase 3: Test Integrity

Tests in tests/parsers/javascript.test.ts are additive — 108 new lines, 0 deletions. All new tests have concrete toEqual assertions on confidence values and types. No assertions weakened. The test coverage looks thorough:

  • Explicit TS annotation → confidence 1.0 ✓
  • Inferred from return new Constructor() → confidence 0.85 ✓
  • Annotation wins over inferred ✓
  • Qualified method names (UserService.getUser) ✓
  • Arrow functions ✓
  • Intra-file single-hop propagation ✓
  • Method chain resolution ✓
  • Chain confidence decay ✓
  • No-override (already resolved locally) ✓
  • Missing return-type info (no entry added) ✓

tests/unit/config.test.ts — 1 line added, verifying typePropagationDepth: 3 appears in defaults. Clean.


Phase 4: Code Quality

Bug: typePropagationDepth config is added but never consumed

src/infrastructure/config.ts:82 adds typePropagationDepth: 3 to DEFAULTS. src/types.ts:1217 adds it to CodegraphConfig. But javascript.ts:83 has:

const MAX_PROPAGATION_DEPTH = 3;

This is a module-level constant that ignores the config entirely. The propagateReturnTypesAcrossFiles function in build-edges.ts:442 hardcodes - 0.1 instead of using PROPAGATION_HOP_PENALTY. The config field is wired nowhere. Either wire it or don't add it — dead config fields are confusing and violate the codebase's own "always add to DEFAULTS" rule by doing so incorrectly (adding a field no one reads).

File: src/extractors/javascript.ts:83, src/domain/graph/builder/stages/build-edges.ts:442

Native engine gap (dual-engine parity violation)

extractReturnTypeMapWalk only runs in the WASM/JS path. The native Rust engine (crates/codegraph-core/) parses files independently and builds its own typeMap for call-edge resolution. The PR's propagateReturnTypesAcrossFiles in build-edges.ts does run for both engines (it reads fileSymbols which comes from WASM when native parses files in JS-fallback mode). However, the intra-file propagation from extractReturnTypeMapWalk feeds into typeMap entries that the native call-edge path also consumes (via buildCallEdgesNative). The native extractor doesn't populate returnTypeMap, so:

  • WASM: const user = createUser()user in typeMapuser.getProfile() resolved ✓
  • Native: returnTypeMap is empty (undefined for native parse results) → callAssignments not populated → user missing from typeMapuser.getProfile() unresolved ✗

This is a parity gap. Per CLAUDE.md: "Both engines must produce identical results. If they diverge, the less-accurate engine has a bug — fix it, don't document the gap." The PR adds no corresponding logic to the Rust extractor or a backfill path for native results.

File: src/domain/graph/builder/stages/build-edges.ts, crates/codegraph-core/

propagateReturnTypesAcrossFiles builds a global returnType index that ignores scoping

build-edges.ts:418-424 merges all files' returnTypeMap entries into one flat globalReturnTypeMap, with conflicts resolved by highest confidence. This means if fileA.ts exports getUser(): User and fileB.ts also exports getUser(): Profile, the lower-confidence one is silently dropped globally. The per-file index (returnTypeIndex) is correctly consulted first for import-tracked callees, but the receiverTypeName path falls through to globalReturnTypeMap which loses file provenance. This is a correctness risk for large codebases with common method names in different classes.

File: src/domain/graph/builder/stages/build-edges.ts:418-435

extractReturnTypeMapWalk in walk path re-walks nodes already walked by walkJavaScriptNode

In extractSymbolsWalk (javascript.ts:571-588), walkJavaScriptNode is called first, then extractReturnTypeMapWalk does a second full walk. Similarly extractTypeMapWalk is a third full walk. For large files, this is 3+ AST traversals. The query-based path (extractSymbolsQuery) does a single query.matches() pass plus 3 targeted walks. The walk path's 3 sequential full traversals are O(3n) in AST nodes. This wasn't introduced by this PR (the typeMap walk predates it) but the PR adds a third traversal.

File: src/extractors/javascript.ts:571-588

Minor: extractReturnTypeMapWalk doesn't handle nested classes correctly

At javascript.ts:1192-1194, after processing a class body, the walk recurses with currentClass set to the class name — but methods defined in a nested class would incorrectly inherit the outer class name. The code sets currentClass in the class branch and then continues recursing with it, but doesn't reset currentClass to null when leaving the class. For the common case (no nested classes) this is fine.

File: src/extractors/javascript.ts:1155-1196

Cross-file propagation hardcodes - 0.1 instead of PROPAGATION_HOP_PENALTY

build-edges.ts:442: const propagatedConf = returnEntry.confidence - 0.1; — this should use the named constant from javascript.ts (or a shared constant) to keep penalty logic consistent. Currently the cross-file penalty is defined independently of the intra-file one.

File: src/domain/graph/builder/stages/build-edges.ts:442


Phase 5: Backlog Compliance

  • Zero-dep: ✓ — No new npm runtime dependencies. Builds entirely on existing tree-sitter AST infrastructure.
  • Foundation-aligned: ✓ — Purely local analysis. No cloud calls, no code generation, no external toolchain. Improves call-edge accuracy, which reduces hallucinated dependencies for AI agents.
  • Problem-fit: 5 — This directly reduces hallucinated call edges. When an AI agent asks "what does user.getProfile() call?", the graph currently has no edge because user's type is unknown. After this PR, the edge exists. This is exactly the "hallucinate dependencies" problem from the stated mission.
  • Breaking: No — Purely additive. New optional fields (returnTypeMap, callAssignments) on ExtractorOutput, new config key. No existing API signatures changed.
  • Tier: 1 — zero-dep + foundation-aligned.

Critical Concerns

  1. Native engine parity gap — The Rust extractor won't populate returnTypeMap or callAssignments, so user.getProfile() remains unresolved in native mode. This violates the dual-engine parity requirement. Either backfill the returnTypeMap from WASM output for native parse results, or implement equivalent logic in the Rust extractor. This is a blocking concern per CLAUDE.md.

  2. Dead config fieldtypePropagationDepth is configured but never consumed by MAX_PROPAGATION_DEPTH. If the intent is future tunability, the config field should be threaded through to the extractor (requires passing config to extractSymbols). If not yet wired, remove it from this PR and add it when the wiring is done.


Final Recommendation

## Root Cause Analysis
Problem Being Solved: Improve call-edge accuracy for method calls on variables
  returned from factory/constructor functions (the "lost type" problem).
Why This Approach: Builds naturally on the existing typeMap mechanism, no schema
  changes, cleanly inserted before edge-building. Architecturally sound.
Risk Assessment: Medium — dual-engine parity gap means native mode users get no
  benefit (and different graph results than WASM mode users).

## Backlog Compliance
- Zero-dep: ✓
- Foundation-aligned: ✓
- Problem-fit: 5 — directly reduces hallucinated call edges for AI agents
- Breaking: No
- Tier: 1

## Critical Concerns
1. Native engine parity gap — returnTypeMap not populated for native parse results;
   native call-edge path gets no benefit (CLAUDE.md violation).
2. typePropagationDepth config field added but never consumed by the implementation.

## Final Recommendation
- Rating: ⭐⭐⭐☆☆ (3/5)
- Action: REQUEST CHANGES
- Reasoning: The feature is architecturally correct and high-value (problem-fit 5),
  but it ships with a known engine parity gap that CLAUDE.md explicitly forbids
  accepting. The native engine will produce different call graphs than WASM for
  any codebase using factory functions. Fix the parity gap (backfill returnTypeMap
  from WASM output when native is the parser, or implement in Rust), remove or
  wire the dead config field, and this is ready to merge.

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Phase 8.2 adds inter-procedural return-type propagation for JS/TS: the extractor now records each function's return type in a returnTypeMap, uses it to resolve intra-file call assignments, and build-edges.ts does a cross-file pass before the SQLite transaction to propagate imported function return types into callers' typeMaps.

  • extractReturnTypeMapWalk in javascript.ts records return types at 1.0 (explicit annotation) or 0.85 (inferred from return new Constructor()), and resolveCallExprReturnType applies a 0.1-per-hop confidence decay for up to 3-hop method chains.
  • propagateReturnTypesAcrossFiles in build-edges.ts resolves cross-file call assignments against a merged globalReturnTypeMap (method calls) or the specific importer file's returnTypeMap (plain function calls), running before computeEdgesTx() as pure in-memory work.
  • CallAssignment and returnTypeMap / callAssignments fields are added to ExtractorOutput; typePropagationDepth: 3 is added to CodegraphConfig with a TODO noting it isn't yet wired to the hardcoded constant (deferred to Phase 8.3).

Confidence Score: 5/5

Safe to merge; the propagation is purely additive in-memory work that feeds existing call-edge logic without touching the DB schema or edge insertion paths.

All previously identified issues (partial mutation inside a transaction, currentClass leaking into function bodies, the annotation guard being always-true, and the dead config knob) have been addressed. The only remaining finding is that 3-hop method chains yield confidence 0.8 instead of the documented 0.7 — a minor discrepancy in a probabilistic value that does not affect correctness of edge construction.

resolveCallExprReturnType in src/extractors/javascript.ts — the effectiveDepth accounting for chains longer than 2 hops.

Important Files Changed

Filename Overview
src/extractors/javascript.ts Adds extractReturnTypeMapWalk, resolveCallExprReturnType, recordCallAssignment, and associated helpers. Core logic is sound but the 3-hop chain confidence calculation yields 0.8 (same as 2-hop) instead of the documented 0.7.
src/domain/graph/builder/stages/build-edges.ts Adds propagateReturnTypesAcrossFiles, correctly placed before computeEdgesTx() to avoid partial in-memory mutation inside a SQLite transaction. Global/per-file returnTypeMap routing logic is correct.
src/types.ts Adds CallAssignment interface and extends ExtractorOutput with optional returnTypeMap and callAssignments fields; also adds typePropagationDepth to CodegraphConfig. Clean additions.
src/infrastructure/config.ts Adds typePropagationDepth: 3 to DEFAULTS with a TODO comment clarifying it is not yet wired to the hardcoded MAX_PROPAGATION_DEPTH constant (deferred to Phase 8.3).
tests/parsers/javascript.test.ts 12 new tests cover returnTypeMap extraction, intra-file propagation, method chain, and edge cases. Missing a 3-hop chain test that would expose the confidence discrepancy (0.8 vs expected 0.7).
tests/unit/config.test.ts Adds assertion for the new typePropagationDepth DEFAULTS entry. Minimal and correct.

Sequence Diagram

sequenceDiagram
    participant Extractor as JS/TS Extractor
    participant RTM as returnTypeMap
    participant TM as typeMap
    participant BE as build-edges.ts
    participant DB as SQLite

    Extractor->>RTM: "extractReturnTypeMapWalk()<br/>(fn → {type, confidence})"
    Extractor->>TM: "extractTypeMapWalk(returnTypeMap)<br/>resolveCallExprReturnType (intra-file, ≤3 hops)"
    Extractor->>Extractor: "recordCallAssignment()<br/>(unresolved calls → callAssignments[])"

    BE->>BE: "propagateReturnTypesAcrossFiles()<br/>build globalReturnTypeMap + importedNamesMap"
    BE->>TM: "setTypeMapEntry(varName, type,<br/>confidence − 0.1) [cross-file]"

    BE->>DB: "computeEdgesTx()<br/>buildCallEdgesNative / buildCallEdgesJS"
Loading

Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(analysis): move cross-file propagati..." | Re-trigger Greptile

briefImporterDepth: 5,
briefHighRiskCallers: 10,
briefMediumRiskCallers: 3,
typePropagationDepth: 3,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 typePropagationDepth config value is never read

The config field is added to DEFAULTS and CodegraphConfig but the actual depth limit used throughout is MAX_PROPAGATION_DEPTH = 3, a hardcoded module-level constant in javascript.ts. Changing analysis.typePropagationDepth in a project config will have zero effect on propagation behaviour. If this is intentionally deferred, the field should at minimum carry a // TODO in the source so it isn't mistaken for a live control knob.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the config field now carries a TODO comment pointing to MAX_PROPAGATION_DEPTH and explaining this will be wired through when config is threaded into extractSymbols (Phase 8.3). The comment explicitly notes the current code uses the hardcoded constant, so it won't be mistaken for a live control knob.

Comment on lines +1155 to +1195
function walk(node: TreeSitterNode, depth: number, currentClass: string | null): void {
if (depth >= MAX_WALK_DEPTH) return;
const t = node.type;

if (t === 'class_declaration' || t === 'class') {
const nameNode = node.childForFieldName('name');
const className = nameNode?.text ?? null;
for (let i = 0; i < node.childCount; i++) {
walk(node.child(i)!, depth + 1, className);
}
return;
}

if (t === 'function_declaration') {
const nameNode = node.childForFieldName('name');
if (nameNode?.type === 'identifier' && nameNode.text !== 'constructor') {
const fnName = currentClass ? `${currentClass}.${nameNode.text}` : nameNode.text;
storeReturnType(node, fnName, returnTypeMap);
}
} else if (t === 'method_definition') {
const nameNode = node.childForFieldName('name');
if (nameNode && currentClass && nameNode.text !== 'constructor') {
storeReturnType(node, `${currentClass}.${nameNode.text}`, returnTypeMap);
}
} else if (t === 'variable_declarator') {
// const foo = (): ReturnType => … or const foo = function(): ReturnType { … }
const nameN = node.childForFieldName('name');
const valueN = node.childForFieldName('value');
if (nameN?.type === 'identifier' && valueN) {
const vt = valueN.type;
if (vt === 'arrow_function' || vt === 'function_expression') {
const fnName = currentClass ? `${currentClass}.${nameN.text}` : nameN.text;
storeReturnType(valueN, fnName, returnTypeMap);
}
}
}

for (let i = 0; i < node.childCount; i++) {
walk(node.child(i)!, depth + 1, currentClass);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 currentClass leaks into function bodies, producing incorrect qualified keys

After entering a class_declaration, currentClass is set and propagated to all children. When the walk later visits a function_declaration or method_definition node, it stores the entry correctly — but because neither handler contains an early return before the fall-through child loop, the walk continues into the function/method body still carrying currentClass. Any function_declaration or arrow-function variable_declarator nested inside a method body is then stored in returnTypeMap under ClassName.innerFnName rather than just innerFnName. This creates a false returnTypeMap entry that can trigger incorrect type propagation for calls like svc.formatUser(...) on any UserService-typed receiver. The fix is to pass null as currentClass when recursing into a function body, so nested function declarations are treated as module-scoped.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — when recursing into the children of function_declaration and method_definition nodes, the walk now passes null as currentClass. This prevents any function_declaration or arrow-function variable_declarator nested inside a method body from inheriting the enclosing class name. Added early return statements after the null-class recursive calls to avoid double-visiting.

Comment on lines +1206 to +1212
if (returnTypeNode) {
const typeName = extractSimpleTypeName(returnTypeNode);
if (typeName) {
const existing = returnTypeMap.get(fnName);
if (!existing || 1.0 > existing.confidence)
returnTypeMap.set(fnName, { type: typeName, confidence: 1.0 });
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Annotation guard condition is always true — misleading intent

The condition if (!existing || 1.0 > existing.confidence) is designed to mean "don't overwrite a higher-confidence entry", but since 1.0 is the maximum possible confidence, 1.0 > existing.confidence is always true for any real entry (you'd need existing.confidence === 1.0 to avoid overwriting, in which case 1.0 > 1.0 is false). In practice this means the last annotation for a duplicated function name wins rather than the first. Consider existing.confidence < 1.0 or simply removing the redundant guard.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed 1.0 > existing.confidence to existing.confidence < 1.0. This is semantically identical in effect but clearly expresses the intent: "overwrite unless already at maximum confidence", making it obvious the first annotation at 1.0 wins for duplicate function names.

Comment on lines +905 to +910
buildImportEdges(ctx, getNodeIdStmt, allEdgeRows);
}

// Phase 8.2: Augment typeMaps with cross-file return-type propagation before
// call-edge building so both native and JS paths see the enriched typeMaps.
propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 In-memory mutation runs inside a SQLite transaction — partial-write risk on throw

propagateReturnTypesAcrossFiles mutates symbols.typeMap on the live ctx.fileSymbols entries. If it throws unexpectedly, the SQLite transaction aborts (rolling back the already-computed import edges) while the in-memory typeMap mutations are left in a partial state. Since this function performs no DB I/O it would be safer to call it immediately before computeEdgesTx is opened.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — moved propagateReturnTypesAcrossFiles to just before computeEdgesTx() is called (outside the transaction). Since it performs no DB I/O, it needs no transactional protection; running it inside was a risk for leaving ctx.fileSymbols in a partial mutated state if the transaction rolled back.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

19 functions changed13 callers affected across 5 files

  • propagateReturnTypesAcrossFiles in src/domain/graph/builder/stages/build-edges.ts:405 (3 transitive callers)
  • buildEdges in src/domain/graph/builder/stages/build-edges.ts:856 (4 transitive callers)
  • extractSymbolsQuery in src/extractors/javascript.ts:308 (1 transitive callers)
  • extractSymbolsWalk in src/extractors/javascript.ts:571 (1 transitive callers)
  • extractReturnTypeMapWalk in src/extractors/javascript.ts:1151 (3 transitive callers)
  • walk in src/extractors/javascript.ts:1155 (6 transitive callers)
  • storeReturnType in src/extractors/javascript.ts:1212 (6 transitive callers)
  • findReturnNewExprType in src/extractors/javascript.ts:1240 (5 transitive callers)
  • resolveCallExprReturnType in src/extractors/javascript.ts:1266 (5 transitive callers)
  • recordCallAssignment in src/extractors/javascript.ts:1322 (5 transitive callers)
  • extractTypeMapWalk in src/extractors/javascript.ts:1357 (3 transitive callers)
  • walk in src/extractors/javascript.ts:1363 (6 transitive callers)
  • handleVarDeclaratorTypeMap in src/extractors/javascript.ts:1379 (6 transitive callers)
  • CallAssignment.varName in src/types.ts:532 (0 transitive callers)
  • CallAssignment.calleeName in src/types.ts:534 (0 transitive callers)
  • CallAssignment.receiverTypeName in src/types.ts:536 (0 transitive callers)
  • ExtractorOutput.returnTypeMap in src/types.ts:552 (0 transitive callers)
  • ExtractorOutput.callAssignments in src/types.ts:557 (0 transitive callers)
  • CodegraphConfig.analysis in src/types.ts:1206 (0 transitive callers)

carlos-alm and others added 3 commits June 1, 2026 18:05
…eturnTypeMapWalk

- Pass null as currentClass when recursing into function_declaration and
  method_definition bodies, preventing nested function declarations from
  being stored under the enclosing class name (e.g. ClassName.innerFn)
- Fix annotation guard condition: `1.0 > existing.confidence` was always
  true (since 1.0 is maximum confidence); replace with `existing.confidence < 1.0`
  so the first annotation wins for duplicate function names
…t penalty constant

- Move propagateReturnTypesAcrossFiles call to before computeEdgesTx opens,
  avoiding partial in-memory typeMap mutations if the transaction rolls back
- Export PROPAGATION_HOP_PENALTY from javascript.ts and use it in build-edges.ts
  instead of a hardcoded 0.1 literal
- Add TODO comment to typePropagationDepth config entry clarifying it is not yet
  wired to MAX_PROPAGATION_DEPTH (planned for Phase 8.3)
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback:

Fixed:

  1. currentClass leak (P1 bug)function_declaration and method_definition handlers now recurse with null as currentClass, preventing nested function declarations inside method bodies from being keyed under the enclosing class name. Added early return to avoid double-visiting. Commit: fc9eb64.

  2. Annotation guard condition — Changed 1.0 > existing.confidence to existing.confidence < 1.0. Semantically equivalent but clearly expresses the intent. Commit: fc9eb64.

  3. Transaction placement — Moved propagateReturnTypesAcrossFiles to immediately before computeEdgesTx() (outside the transaction), as it performs no DB I/O and should not risk leaving in-memory typeMap in a partial state on rollback. Commit: f9b306a.

  4. Hardcoded - 0.1 — Exported PROPAGATION_HOP_PENALTY from javascript.ts and imported it in build-edges.ts to replace the literal. The cross-file and intra-file paths now share the same constant. Commit: f9b306a.

  5. typePropagationDepth config field — Added a TODO comment on the config entry and on CodegraphConfig.analysis type pointing to the hardcoded MAX_PROPAGATION_DEPTH constant and noting Phase 8.3 is where the wiring will happen. Commit: f9b306a.

Tracked as issue (out of scope for this PR):
6. Native engine parity gapextractReturnTypeMapWalk + recordCallAssignment are JS/TS WASM-only. Implementing equivalent logic in the Rust extractor is a separate feature. Filed as #1280 to ensure it is tracked per CLAUDE.md requirement that both engines produce identical results.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@claude

@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Claude finished @carlos-alm's task —— View job


Re-review: feat(analysis): inter-procedural return-type propagation (Phase 8.2)

Tasks:

  • Gather context
  • Read changed files to verify fixes
  • Verify currentClass leak fix
  • Verify annotation guard condition fix
  • Verify transaction placement fix
  • Verify PROPAGATION_HOP_PENALTY constant sharing
  • Verify typePropagationDepth TODO comment
  • Post updated review

All five code fixes from the original review have been correctly implemented. Here is a verification of each:

1. currentClass leak — ✅ Fixed

javascript.ts:1168-1190 — Both function_declaration and method_definition handlers now recurse into their children with null as currentClass, followed by an explicit return to avoid the fall-through child loop. Nested function declarations inside method bodies will no longer be stored under ClassName.innerFn.

// function_declaration handler
for (let i = 0; i < node.childCount; i++) {
  walk(node.child(i)!, depth + 1, null);  // ← null, not currentClass
}
return;  // ← prevents fall-through to the generic child loop at line 1204

The variable_declarator handler at lines 1191-1202 does not have an early return, so it falls through to the generic child loop at line 1204 which propagates currentClass. This is correct for variable_declarator — it stores the arrow/function-expression for the declarator itself and then lets the generic walk continue into the right-hand side with the same currentClass. However, if the RHS is an arrow_function or function_expression, those will be re-visited as children. In the current code, the walk function only handles function_declaration and method_definition specifically — arrow_function and function_expression nodes fall through to the generic loop with whatever currentClass is in scope. For a const foo = () => {} inside a method, the arrow function's own children (the body) would be walked with currentClass still set. Since no arrow function body is checked for variable_declarator children (arrow bodies are typically statement_block, not variable_declarator at the top level), this is unlikely to cause the original bug, but it is worth noting that the variable_declarator path still propagates currentClass into nested scopes. This is a minor pre-existing edge case, not introduced by the fix.

2. Annotation guard condition — ✅ Fixed

javascript.ts:1222: if (!existing || existing.confidence < 1.0) — semantically correct and clearly expresses intent. The first annotation at confidence 1.0 wins for duplicate keys.

3. Transaction placement — ✅ Fixed

build-edges.ts:927-932:

// propagateReturnTypesAcrossFiles is called BEFORE computeEdgesTx()
propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);
computeEdgesTx();

The comment at lines 927-930 explains the reasoning inline. No partial-mutation risk from transaction rollback.

4. PROPAGATION_HOP_PENALTY constant sharing — ✅ Fixed

javascript.ts:85: export const PROPAGATION_HOP_PENALTY = 0.1;
build-edges.ts:11: import { PROPAGATION_HOP_PENALTY } from '../../../../extractors/javascript.js';
build-edges.ts:443: const propagatedConf = returnEntry.confidence - PROPAGATION_HOP_PENALTY;

The cross-file and intra-file paths now share the same constant. No independently-defined magic numbers.

5. typePropagationDepth TODO — ✅ Fixed

config.ts:82-85: The config field now has a clear comment: "TODO(Phase 8.3): wire this into PROPAGATION_HOP_PENALTY / MAX_PROPAGATION_DEPTH once config is threaded through to extractSymbols. Currently the depth is controlled by the hardcoded MAX_PROPAGATION_DEPTH constant in src/extractors/javascript.ts."

types.ts:1216: JSDoc /** Maximum chain depth for inter-procedural return-type propagation (Phase 8.2). */ — documents what it represents, though it could also note that it is not yet consumed. Minor — the config.ts TODO is sufficient.

6. Native engine parity — tracked as #1280

Per CLAUDE.md: filing an issue is acceptable when the fix is out of scope for the current PR. This is correctly tracked.


One Remaining Minor Issue

The variable_declarator handler in extractReturnTypeMapWalk at javascript.ts:1191-1206 does not have an early return after processing, so the generic child loop at line 1204 runs with currentClass still set. This means arrow functions or function expressions assigned to variables inside a method body could theoretically leak currentClass into deeper nodes. In practice, the only types specifically handled are function_declaration and method_definition (neither of which is typically a direct child of an arrow function body), so this path doesn't reproduce the original bug. Still, for completeness, adding a return after the variable_declarator handler's storeReturnType call would be more defensive. This is a low-severity nit, not a blocker.


Updated Rating: ⭐⭐⭐⭐☆ (4/5) — APPROVE with nit

All critical issues from the initial review have been resolved. The architecture is sound, the engine parity gap is tracked, the config field is documented, and the two concrete bugs (currentClass leak, transaction placement) are fixed. The remaining variable_declarator fall-through is a low-severity edge case that doesn't reproduce the original bug. Ready to merge.

@carlos-alm carlos-alm merged commit f9e1f94 into main Jun 2, 2026
38 checks passed
@carlos-alm carlos-alm deleted the feat/inter-procedural-type-propagation branch June 2, 2026 00:36
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant