Skip to content

fix(extractors): qualify object literals returned from factory functions - #2263

Merged
carlos-alm merged 2 commits into
mainfrom
fix/issue-2033-factory-returned-closure-attribution
Aug 4, 2026
Merged

fix(extractors): qualify object literals returned from factory functions#2263
carlos-alm merged 2 commits into
mainfrom
fix/issue-2033-factory-returned-closure-attribution

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

Closes #2033.

extractObjectLiteralFunctions (the mechanism that creates qualified varName.propName definitions so calls inside an object-literal property's closure attribute to the property, not the enclosing scope) only fired for object literals assigned via a variable declarator (const x = {...}). It never fired for object literals appearing in a return statement inside a function body — so calls inside those closures fell through to generic caller-attribution, which resolved to the enclosing factory function itself, even though the factory's own body never executes that call.

function computeDeltaCPM(s, v) { return s + v; }
export function makePartition(seed) {
  const s = seed;
  return { deltaCPM: (v) => computeDeltaCPM(s, v) };
}

Before: computeDeltaCPM's caller showed as makePartition. After: makePartition.deltaCPM.

Changes

  • src/extractors/javascript.ts: findEnclosingFunctionQualifier/qualifierForFunctionScopeNode walk up to the nearest enclosing function scope and derive its qualifier name (function declaration name, ClassName.method, or the variable a function/arrow is directly assigned to — anonymous non-assigned closures get no qualifier and fall back to prior behavior). handleReturnStmtObjectLiteral wires this into extractObjectLiteralFunctions + handleObjectLiteralTypeMap, hooked into runCollectorWalk's return_statement case — shared by both the walk and query extraction paths.
  • Added a self-referential return-type inference tier to storeReturnType: a function whose body directly returns an object literal with callable properties is typed as itself, so const p = makePartition(42); p.deltaModularity(1) resolves p's type and finds the qualified definition — this is what lets roles --role dead: non-transitive fan-in means a function called only by another dead function is never flagged dead #2032's reachability-based dead-code detection close the loop end-to-end (only the unused property becomes dead, not a used sibling property).
  • crates/codegraph-core/src/extractors/javascript.rs: mirrored extraction (handle_return_stmt, handle_return_stmt_type_map, find_enclosing_function_qualifier, find_return_object_literal_self_type). Also fixed a same-file gap in Rust's Phase 8.2 inter-procedural return-type propagation (handle_var_declarator_type_map's call_expression branch only handled cross-file/imported callees; added the same-file identifier lookup TS already had) and reordered JsExtractor::extract's walks so return_type_map is fully populated before match_js_type_map reads it — both engines now produce identical graphs for this shape.

Verification

  • Repro confirmed on both engines: computeDeltaCPM's caller is now makePartition.deltaCPM (not makePartition); p.deltaModularity(1) resolves to makePartition.deltaModularity.
  • End-to-end with roles --role dead: non-transitive fan-in means a function called only by another dead function is never flagged dead #2032 (already merged): codegraph roles --role dead now correctly flags computeDeltaCPM as dead (its only caller, makePartition.deltaCPM, is itself unreachable) while computeDeltaModularity is correctly NOT flagged (reachable via useIt).
  • npm test (4412 passed, 30 skipped, 2 todo), npm run lint, cargo test --lib/cargo test --release (742 passed), node scripts/parity-compare.mjs (34 languages) — no new divergences.
  • New tests: unit tests in tests/parsers/javascript.test.ts, a query/walk parity case in tests/engines/query-walk-parity.test.ts, and an end-to-end WASM+native integration test in tests/integration/issue-2033-factory-returned-closure-attribution.test.ts.

Follow-ups filed (out of scope, discovered during validation)

Test plan

  • npm test
  • npm run lint
  • cargo test --lib / cargo test --release
  • node scripts/parity-compare.mjs
  • codegraph diff-impact --staged -T

…ons (#2033)

extractObjectLiteralFunctions only fired for object literals assigned via a
variable declarator (const x = {...}), so calls inside a closure property
returned from a function body (return { prop: () => f() }) attributed to the
enclosing factory itself rather than the property — misleading call-graph
edges since the factory's own body never executes that call.

Extend the mechanism to return_statement object literals, qualifying against
the nearest enclosing named function (or ClassName.method for methods, or the
variable a function/arrow is directly assigned to). Also add a self-typing
return-type inference so const p = factory(); p.prop() resolves through the
qualified definition, closing the loop with #2032's reachability-based dead
code detection. Mirrored in both WASM (shared runCollectorWalk, used by both
extraction paths) and native (match_js_node/match_js_type_map/store_return_type).

docs check acknowledged: internal extractor/resolver fix, no new commands,
languages, or architecture changes — README/CLAUDE.md/ROADMAP unaffected.

Impact: 9 functions changed, 14 affected
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR extends JavaScript factory-return extraction while preventing the previously reported impossible call edges for async and generator wrappers.

  • Qualifies callable properties on directly returned object literals using the enclosing function or method name.
  • Adds self-referential return-type inference and same-file propagation for synchronous factories.
  • Skips inferred return types for async and generator functions in both TypeScript/WASM and Rust/native implementations.
  • Adds parser, engine-parity, and end-to-end graph tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/extractors/javascript.ts Adds returned-object qualification and synchronous self-type propagation while correctly excluding async and generator wrappers from inferred return types.
crates/codegraph-core/src/extractors/javascript.rs Mirrors the TypeScript extraction behavior and reorders return-type collection before same-file type propagation.
tests/parsers/javascript.test.ts Covers qualification shapes and verifies that async and generator factories are not self-typed.
tests/engines/query-walk-parity.test.ts Adds parity coverage for factory-returned callable object properties.
tests/integration/issue-2033-factory-returned-closure-attribution.test.ts Verifies caller attribution, property-call resolution, and dead-code classification through both graph engines.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Factory["Synchronous factory"] --> Returned["Returned object literal"]
  Returned --> Qualified["Qualified property definitions"]
  Factory --> SelfType["Factory self return type"]
  SelfType --> Variable["Call-result variable type"]
  Variable --> Call["Resolved property call edge"]
  Async["Async or generator factory"] --> Wrapper["Promise or generator wrapper"]
  Wrapper --> Skip["Skip self-type inference"]
Loading

Reviews (2): Last reviewed commit: "fix(extractors): skip return-type self-i..." | Re-trigger Greptile

Comment thread src/extractors/javascript.ts Outdated
const body = fnNode.childForFieldName('body');
if (body) {
const inferred = findReturnNewExprType(body);
const inferred = findReturnNewExprType(body) ?? findReturnObjectLiteralSelfType(body, fnName);

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 Async and generator return types

When an async or generator factory directly returns an object literal with callable properties, this inference types the immediate call result as the object itself. Calls such as make().run() can then resolve to make.run even though the runtime result is a Promise or generator object, producing impossible call edges and incorrect reachability and dead-code results.

Knowledge Base Used:

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 in b1fbfbd: added isAsyncFunctionNode/isGeneratorFunctionNode guards (mirrored in both TS and native) that skip the return-type self-inference — and the pre-existing return new Ctor() inference, which had the identical flaw — for async/generator factories. The qualified property definitions are still extracted regardless; only the self-typing that would let a caller resolve through the wrapper without await/iteration is skipped. Added regression tests on both engines (async factory, generator factory, and the pre-existing return-new-Constructor case).

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

11 functions changed16 callers affected across 3 files

  • findEnclosingFunctionQualifier in src/extractors/javascript.ts:584 (13 transitive callers)
  • qualifierForFunctionScopeNode in src/extractors/javascript.ts:599 (3 transitive callers)
  • findReturnedObjectLiteral in src/extractors/javascript.ts:1873 (15 transitive callers)
  • handleReturnStmtObjectLiteral in src/extractors/javascript.ts:1904 (14 transitive callers)
  • storeReturnType in src/extractors/javascript.ts:2475 (14 transitive callers)
  • isAsyncFunctionNode in src/extractors/javascript.ts:2521 (13 transitive callers)
  • isGeneratorFunctionNode in src/extractors/javascript.ts:2535 (13 transitive callers)
  • findReturnObjectLiteralSelfType in src/extractors/javascript.ts:2570 (13 transitive callers)
  • objectLiteralHasCallableProperty in src/extractors/javascript.ts:2586 (3 transitive callers)
  • runCollectorWalk in src/extractors/javascript.ts:4885 (5 transitive callers)
  • walk in src/extractors/javascript.ts:4886 (14 transitive callers)

…factories

An async or generator function's runtime return value is a Promise/Generator
wrapper around the returned expression, not the expression itself. Both the
new #2033 self-type inference and the pre-existing `return new Ctor()`
inference wrongly typed such factories as their own return shape, which would
let `const p = asyncMakeThing(); p.method()` resolve without the required
await/iteration. Gate both inferences on isAsyncFunctionNode/
isGeneratorFunctionNode in TS and native; the qualified property definitions
themselves are still extracted regardless.

docs check acknowledged: internal extractor/resolver fix, no docs impact.

Impact: 3 functions changed, 10 affected
@carlos-alm
carlos-alm merged commit f29d346 into main Aug 4, 2026
34 of 36 checks passed
@carlos-alm
carlos-alm deleted the fix/issue-2033-factory-returned-closure-attribution branch August 4, 2026 05:37
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 4, 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.

Calls inside object-literal-property closures returned from a factory are misattributed to the enclosing factory, not the property

1 participant