Skip to content

fix: classify destructured bindings as constant, not function#1902

Open
carlos-alm wants to merge 1 commit into
fix/issue-1772-dedupe-callref-tests-renderingfrom
fix/issue-1773-destructured-binding-kind
Open

fix: classify destructured bindings as constant, not function#1902
carlos-alm wants to merge 1 commit into
fix/issue-1772-dedupe-callref-tests-renderingfrom
fix/issue-1773-destructured-binding-kind

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Both engines' destructured object-pattern extraction (extractDestructuredBindings/extract_destructured_bindings) unconditionally assigned kind: 'function' to every binding target, regardless of the destructured value's actual nature. Since these bindings have no call-graph edges pointing at them by name under that wrong kind, roles --role dead flagged them dead-unresolved even when read repeatedly.
  • Fixed to kind: 'constant' (verified against shared/kinds.ts as the already-used correct kind for JS/TS const bindings — all three extraction call sites are unconditionally gated on const) in both engines. Confirmed this doesn't break call-target resolution: 'constant' is already in NODE_KIND_FILTER_SQL/TOP_LEVEL_BINDING_KINDS, so the flags -> parseArgs call-attribution edge survives via the correct tier instead of the old bogus "function scope" tier.
  • Checked array-pattern destructuring for the same bug class: TS/WASM already correctly emits kind: 'constant'; native emits no definition at all for array-pattern const declarations — a different bug shape (missing node, not wrong kind) and a genuine engine-parity gap. Filed separately as Engine parity: native JS extractor emits no definition for const array-pattern destructuring #1901, out of scope for this fix.

Closes #1773

Test plan

  • npm test — full suite green (3564 passed, 0 failed)
  • npm run lint — clean
  • cargo test --package codegraph-core — 466 passed
  • Native addon rebuilt, codesigned, verified directly
  • Resolution benchmark run twice (fix applied vs. both extractor files temporarily reverted, native rebuilt each time) — zero differences in precision/recall/TP/FP/FN across all 40 fixture languages
  • Both exact repros confirmed fixed: flags now kind: constant, role leaf (not dead); dbPath/name/force now kind: constant, role dead-leaf (not the misleading dead-unresolved) — verified this matches how any isolated referenced-but-unproven-live constant is classified via a control test

Stacked on #1900 (base branch fix/issue-1772-dedupe-callref-tests-rendering) — only the extractor/test diff is this issue's change.

…heck acknowledged)

Object-pattern destructuring targets (const { a, b } = x; and renamed
const { a: b } = x;) were hardcoded to kind: 'function' in both the TS/WASM
extractor (extractDestructuredBindings, shared by the walk and query paths)
and its native Rust mirror (extract_destructured_bindings), regardless of
what the destructured value actually held. Non-function bindings (e.g.
const { dbPath } = workerData) had no call-graph edges pointing at them by
name, so the dead-code classifier risked flagging them dead-unresolved even
when read repeatedly elsewhere in the file.

Both engines now emit kind: 'constant', matching the existing convention for
plain `const x = <literal>` bindings and array-pattern destructuring (which
was already correct). Every call site of these functions is const-gated, so
'constant' is unconditionally correct — no let/var case exists here. Call-
target resolution is kind-agnostic and constant is already included in the
top-level-binding caller-attribution fallback, so destructured
callback-style bindings (e.g. `const { handler } = router; handler(req)`)
still resolve correctly; verified via the resolution-benchmark suite showing
identical precision/recall across all 40 fixture languages before and after.

Array-pattern destructuring was checked and does not have this bug (already
kind: 'constant' in both engines' TS path); a separate, pre-existing native
vs WASM parity gap for array patterns was found and filed as #1901.

Docs check acknowledged: kind-classification bug fix only, no new
languages/features/architecture changes — README/ROADMAP tables unaffected.

Fixes #1773

Impact: 2 functions changed, 7 affected
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a misclassification in both the TypeScript/WASM and Rust/native JavaScript extractors where extractDestructuredBindings/extract_destructured_bindings unconditionally emitted kind: 'function' for every object-pattern destructured binding, regardless of the actual value being bound. The correct kind is 'constant', consistent with how plain const x = … and array-pattern destructuring are classified.

  • Both engines' two affected code paths (shorthand bindings and renamed bindings) are updated in a single-line change each; every call site is const-gated so the fix is unconditionally correct.
  • Tests are added at three levels: unit (Rust), parser (TS), and end-to-end integration (WASM + native), with two real-world repro fixtures and a verification that the flags → parseArgs call-graph edge survives the kind change in the WASM engine.

Confidence Score: 4/5

Safe to merge. The two-character change per extractor is narrowly scoped to object-pattern destructuring inside const declarations, call-target resolution is kind-agnostic so no regressions are expected, and the fix is backed by unit, parser, and integration tests across both engines.

The core change is straightforward and correct in both engines. The one gap worth noting is that the native integration describe block omits the call-edge preservation test that the WASM block includes, leaving a small blind spot for future native-engine regressions on that specific behaviour.

tests/integration/issue-1773-destructured-binding-kind.test.ts — the native describe block covers only the kind assertion, not the call-edge preservation check that the WASM block verifies.

Important Files Changed

Filename Overview
src/extractors/javascript.ts Two kind: 'function' literals in extractDestructuredBindings replaced with kind: 'constant' — one for shorthand bindings, one for renamed bindings. Change is unconditionally correct since every call site is already gated on const. Doc-comment updated to match.
crates/codegraph-core/src/extractors/javascript.rs Rust mirror of the TS fix: two kind: "function".to_string() literals in extract_destructured_bindings changed to "constant". Three new/updated unit tests guard shorthand, non-renamed, and renamed binding kinds. Detailed doc-comment added explaining the rationale.
tests/integration/issue-1773-destructured-binding-kind.test.ts New end-to-end test covering both WASM and native engines. Fixtures mirror real-world repros. The WASM suite has two assertions (kind + call-edge preservation); the native suite only covers the kind assertion — the call-edge preservation test is absent from the native describe block, leaving a minor coverage gap.
tests/parsers/javascript.test.ts Existing assertions updated from kind: 'function' to kind: 'constant' for shorthand and renamed destructured bindings. New regression test added for non-renamed destructuring from a non-call RHS (workerData).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["AST: object_pattern node\n(inside a const declaration)"] --> B{child node type}
    B --> C["shorthand_property_identifier_pattern\nor shorthand_property_identifier\ne.g. const { handleToken } = …"]
    B --> D["pair_pattern or pair\ne.g. const { values: flags } = …"]
    B --> E["other (ignored)"]
    C --> F["emit Definition\nname = child.text\nkind = 'constant' ✅\n(was 'function' ❌)"]
    D --> G["value = child.childForFieldName('value')\n(the local alias, e.g. flags)"]
    G --> H["emit Definition\nname = value.text\nkind = 'constant' ✅\n(was 'function' ❌)"]
    F --> I["DB: nodes table"]
    H --> I
    I --> J{role classification}
    J --> K["kind='constant'\n→ dead-leaf (isolated)\nor leaf (active siblings)"]
    J --> L["kind='function' (old)\n→ dead-unresolved\n(misleading)"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["AST: object_pattern node\n(inside a const declaration)"] --> B{child node type}
    B --> C["shorthand_property_identifier_pattern\nor shorthand_property_identifier\ne.g. const { handleToken } = …"]
    B --> D["pair_pattern or pair\ne.g. const { values: flags } = …"]
    B --> E["other (ignored)"]
    C --> F["emit Definition\nname = child.text\nkind = 'constant' ✅\n(was 'function' ❌)"]
    D --> G["value = child.childForFieldName('value')\n(the local alias, e.g. flags)"]
    G --> H["emit Definition\nname = value.text\nkind = 'constant' ✅\n(was 'function' ❌)"]
    F --> I["DB: nodes table"]
    H --> I
    I --> J{role classification}
    J --> K["kind='constant'\n→ dead-leaf (isolated)\nor leaf (active siblings)"]
    J --> L["kind='function' (old)\n→ dead-unresolved\n(misleading)"]
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix: classify destructured bindings as c..." | Re-trigger Greptile

Comment on lines +142 to +163
describe.skipIf(!isNativeAvailable())(
'destructured binding kind classification (#1773) — native',
() => {
let tmpDir: string;

beforeAll(async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1773-native-'));
for (const [rel, content] of Object.entries(FIXTURE)) {
fs.writeFileSync(path.join(tmpDir, rel), content);
}
await buildGraph(tmpDir, { engine: 'native', incremental: false, skipRegistry: true });
}, 60_000);

afterAll(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('classifies renamed and non-renamed destructured bindings as kind constant, not dead-unresolved', () => {
expectFixedKindAndRole(path.join(tmpDir, '.codegraph', 'graph.db'));
});
},
);

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 Native describe block missing call-edge preservation test

The WASM describe block has two assertions — kind correctness (expectFixedKindAndRole) and a flags → parseArgs call-edge preservation check — but the native block only covers the first. If a future change to the native engine's call-attribution for top-level const bindings silently drops that edge, this test suite won't catch it. Parity with the WASM block's second it(...) would close the gap.

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!

Fix in Claude Code

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

2 functions changed7 callers affected across 1 files

  • extractDestructuredBindings in src/extractors/javascript.ts:1133 (6 transitive callers)
  • handleConstObjectPatternAssignment in src/extractors/javascript.ts:1271 (3 transitive callers)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant