Skip to content

fix(extractors): capture top-level const definitions regardless of initializer shape#1964

Open
carlos-alm wants to merge 1 commit into
fix/issue-1818-native-wasm-definitions-array-order-divergesfrom
fix/issue-1819-exported-consts-with-non-recognized
Open

fix(extractors): capture top-level const definitions regardless of initializer shape#1964
carlos-alm wants to merge 1 commit into
fix/issue-1818-native-wasm-definitions-array-order-divergesfrom
fix/issue-1819-exported-consts-with-non-recognized

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

handleVariableDeclarator/extractConstDeclarators (WASM/TS extractor, both walk and query paths) and the native handle_var_decl only recognized a fixed whitelist of "literal-shaped" initializers (number/string/array/object/new-expression/unary/binary/etc) for top-level const declarations. Any other initializer shape — call expressions, member expressions, parenthesized expressions, plain identifier aliases — silently produced no Definition at all. The symbol never entered symbols/definitions, so it was invisible to every downstream query: where --file, codegraph exports, dead-code audits, codegraph query, etc.

Repro (from the issue)

export const CODEGRAPH_VERSION: string = (
  JSON.parse(fs.readFileSync(path.join(__sharedDir, '..', '..', 'package.json'), 'utf-8')) as {
    version: string;
  }
).version;

CODEGRAPH_VERSION's initializer is a parenthesized member-expression — not a recognized "literal" shape — so codegraph where --file showed zero symbols for the file.

Fix

Broadened the recognition to match how function declarations are already captured regardless of body complexity: any non-function-valued top-level const (including const [a, b] = ... array-pattern destructuring) now becomes a "constant" Definition, independent of initializer shape. collectExportedDeclarations and its Rust mirror collect_exported_var_declarations got the identical change, since the two predicates must stay in sync — the exported = 1 UPDATE matches DB rows by (name, kind, file, line), so a mismatch there would silently no-op instead of marking the symbol exported (#1728).

Removed the now-unused isConstantValue/is_js_literal whitelist helpers in both engines.

Native/WASM decoupling

The Rust walk previously conflated Definition creation with pts fn_ref_binding collection (const alias = handler) inside a single mutually-exclusive if/else if chain. Naively widening the "constant" branch there would have silently dropped fn_ref_binding tracking for const aliases whenever the alias's initializer now also qualified as a constant. Decoupled the two concerns in the Rust walk to mirror the WASM/TS extractor, which already runs collectFnRefBindings as a fully independent pass ("must run before any early return") — so const alias = handler now gets both a "constant" Definition for alias and the pts alias tracking, matching parity between engines.

Test plan

  • npx vitest run tests/parsers/javascript.test.ts — 202/202 passed (new #1819 describe block covers member-expression, call-expression, bare-identifier, array-pattern-with-call, and function-scope-exclusion cases)
  • cargo test --release (codegraph-core) — 527/527 passed (5 new tests mirroring the TS cases, plus a dedicated const_alias_gets_both_definition_and_fn_ref_binding regression guard for the decoupling)
  • npm test — full suite green (3728 passed, 30 skipped, 2 todo)
  • npm run lint — clean
  • npm run build + native addon rebuilt locally (napi build --platform --release + codesign) and verified end-to-end against a real repro file: codegraph where --file now lists CODEGRAPH_VERSION, config (call-expression init), and alias (identifier init) as constant definitions, with the exported ones correctly marked — identical output from both the native and WASM engines (same node/edge counts, same symbol list)

Fixes #1819

…itializer shape

Both handleVariableDeclarator (WASM/TS walk path) and extractConstDeclarators
(WASM/TS query path), plus their Rust mirror handle_var_decl, only recognized
a fixed whitelist of "literal-shaped" initializers (number/string/array/
object/new-expression/etc) for top-level `const` declarations. Any other
shape -- call expressions, member expressions, parenthesized expressions,
plain identifier aliases -- silently produced no Definition at all, making
the symbol invisible to every downstream query (where --file, exports,
dead-code audits, codegraph query).

Broaden the recognition to match how function declarations are already
captured regardless of body complexity: any non-function-valued top-level
`const` (or `const [a, b] = ...` array-pattern) now becomes a "constant"
Definition, independent of initializer shape. collectExportedDeclarations
and its Rust mirror collect_exported_var_declarations get the identical
change so `export const x = <anything>` is marked exported consistently
with the new Definition (the two must stay in sync, per #1728).

The Rust walk previously conflated Definition creation with pts
fn_ref_binding collection (const alias = handler) in a single mutually
exclusive if/else chain, so widening the constant branch would have
silently dropped fn_ref_binding tracking for const aliases. Decoupled the
two concerns to mirror the WASM/TS extractor, which already runs
fnRefBindings collection independently of Definition creation.

Fixes #1819
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

3 functions changed9 callers affected across 1 files

  • collectExportedDeclarations in src/extractors/javascript.ts:219 (6 transitive callers)
  • extractConstDeclarators in src/extractors/javascript.ts:722 (3 transitive callers)
  • handleVariableDeclarator in src/extractors/javascript.ts:1222 (3 transitive callers)

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Broadens top-level const definition capture in both the TypeScript/WASM extractor and the Rust/native extractor: any non-function-valued const declarator now produces a "constant" Definition regardless of initializer shape (call expressions, member expressions, identifier aliases, parenthesized expressions, etc.), closing a gap where symbols were silently absent from definitions and therefore invisible to all downstream queries.

  • Removes the isConstantValue/is_js_literal whitelists from both extractors and updates collectExportedDeclarations/collect_exported_var_declarations identically so that the exported=1 UPDATE continues to match definitions by (name, kind, file, line).
  • Decouples fn_ref_binding tracking from the definition if-else chain in the Rust walk path (mirroring the already-decoupled WASM pass), so const alias = handler now gets both a "constant" Definition and the pts alias entry.
  • Adds five TS tests and five Rust tests covering member-expression, call-expression, bare-identifier, array-pattern-with-call, and function-scope-exclusion cases, plus a dedicated regression guard for the alias/fn_ref_binding decoupling.

Confidence Score: 4/5

Safe to merge; the fix is well-scoped and backed by a comprehensive test matrix across both engines.

The core logic is correct and symmetrically applied to both extractors. The fn_ref_binding decoupling is sound and the test coverage explicitly guards the regression. The only rough edges are a minor loop-invariant hoist in the Rust walk and a pre-existing object_pattern/export mismatch that the PR slightly widens in scope but does not worsen in observable behaviour.

crates/codegraph-core/src/extractors/javascript.rs — specifically handle_var_decl (loop-invariant in_function_scope) and collect_exported_var_declarations (no object_pattern guard).

Important Files Changed

Filename Overview
crates/codegraph-core/src/extractors/javascript.rs Removes is_js_literal whitelist; broadens handle_var_decl to capture any identifier/array_pattern const regardless of initializer shape; decouples fn_ref_binding tracking from the definition if-else chain; extracts VAR_DECL_FN_SCOPE_TYPES constant. Minor: in_function_scope computed inside the declarator loop. collect_exported_var_declarations extended to else-if-is_const, introducing phantom export entries for object_pattern consts with non-literal initialisers.
src/extractors/javascript.ts Removes isConstantValue whitelist; handleVariableDeclarator walk path and extractConstDeclarators query path now unconditionally emit constant definitions for non-function-valued const declarators; collectExportedDeclarations similarly broadened. Pre-existing array_pattern and object_pattern walk branches are unchanged.
tests/parsers/javascript.test.ts Adds a #1819 describe block with five new tests covering member-expression, call-expression, bare-identifier, array-pattern-with-call, and function-scope-exclusion cases; also verifies fnRefBindings tracking is preserved alongside the new constant definition for identifier aliases.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[top-level variable_declaration] --> B{is const?}
    B -- no --> C{let/var identifier + object value + !in_fn_scope?}
    C -- yes --> D[extract_object_literal_functions]
    C -- no --> E[skip]
    B -- yes --> F{value is function-typed?}
    F -- yes --> G[Definition kind=function]
    F -- no --> H{name is object_pattern + !in_fn_scope?}
    H -- yes --> I[extract_destructured_bindings — individual definitions]
    H -- no --> J{name is identifier OR array_pattern + !in_fn_scope?}
    J -- yes --> K[Definition kind=constant — any initializer shape]
    J -- no --> L[skip]
    K --> M[if value==object: extract_object_literal_functions]
    A --> N{name==identifier && value==identifier?}
    N -- yes --> O[fn_ref_binding — runs independently of definition branch]
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[top-level variable_declaration] --> B{is const?}
    B -- no --> C{let/var identifier + object value + !in_fn_scope?}
    C -- yes --> D[extract_object_literal_functions]
    C -- no --> E[skip]
    B -- yes --> F{value is function-typed?}
    F -- yes --> G[Definition kind=function]
    F -- no --> H{name is object_pattern + !in_fn_scope?}
    H -- yes --> I[extract_destructured_bindings — individual definitions]
    H -- no --> J{name is identifier OR array_pattern + !in_fn_scope?}
    J -- yes --> K[Definition kind=constant — any initializer shape]
    J -- no --> L[skip]
    K --> M[if value==object: extract_object_literal_functions]
    A --> N{name==identifier && value==identifier?}
    N -- yes --> O[fn_ref_binding — runs independently of definition branch]
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(extractors): capture top-level const..." | Re-trigger Greptile

Comment on lines 1315 to +1325
@@ -1314,6 +1322,8 @@ fn handle_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
let value_n = declarator.child_by_field_name("value");
let (Some(name_n), Some(value_n)) = (name_n, value_n) else { continue };
let vt = value_n.kind();
let in_function_scope = find_parent_of_types(node, &VAR_DECL_FN_SCOPE_TYPES).is_some();

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_function_scope is recomputed on every iteration of the declarator loop, but it depends solely on node (the variable_declaration), which is constant across all declarators in the same statement. Move the call above the loop to avoid the redundant find_parent_of_types tree-walk on each declarator.

Suggested change
let is_const = node.child(0)
.map(|c| node_text(&c, source) == "const")
.unwrap_or(false);
let in_function_scope = find_parent_of_types(node, &VAR_DECL_FN_SCOPE_TYPES).is_some();
for i in 0..node.child_count() {
let Some(declarator) = node.child(i) else { continue };
if declarator.kind() != "variable_declarator" { continue; }
let name_n = declarator.child_by_field_name("name");
let value_n = declarator.child_by_field_name("value");
let (Some(name_n), Some(value_n)) = (name_n, value_n) else { continue };
let vt = value_n.kind();

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

Comment on lines 1779 to 1784
kind: "function".to_string(),
line,
});
} else if is_const && is_js_literal(&value_n) {
} else if is_const {
symbols.exports.push(ExportInfo {
name: node_text(&name_n, source).to_string(),

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 object_pattern export creates a phantom export entry that can never match a definition

collect_exported_var_declarations now accepts any is_const declarator and pushes name_n's raw text as the export name. For export const {a, b} = callExpr(), that name is {a, b}. But handle_var_decl's object_pattern branch calls extract_destructured_bindings, which emits separate definitions for a and b — so the UPDATE keyed on (name="{a, b}", kind="constant", …) silently no-ops and neither a nor b gets marked exported. This mismatch existed before for literal-valued object_pattern consts, so the practical outcome is unchanged, but the PR now adds the phantom export record for non-literal initializers too. A guard like name_n.kind() != "object_pattern" would prevent the spurious entry.

Fix in Claude Code

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