fix(extractors): capture top-level const definitions regardless of initializer shape#1964
Conversation
…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
Codegraph Impact Analysis3 functions changed → 9 callers affected across 1 files
|
Greptile SummaryBroadens top-level
Confidence Score: 4/5Safe 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
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]
%%{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]
Reviews (1): Last reviewed commit: "fix(extractors): capture top-level const..." | Re-trigger Greptile |
| @@ -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(); | |||
There was a problem hiding this comment.
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.
| 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!
| 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(), |
There was a problem hiding this comment.
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.
Summary
handleVariableDeclarator/extractConstDeclarators(WASM/TS extractor, both walk and query paths) and the nativehandle_var_declonly recognized a fixed whitelist of "literal-shaped" initializers (number/string/array/object/new-expression/unary/binary/etc) for top-levelconstdeclarations. Any other initializer shape — call expressions, member expressions, parenthesized expressions, plain identifier aliases — silently produced no Definition at all. The symbol never enteredsymbols/definitions, so it was invisible to every downstream query:where --file,codegraph exports, dead-code audits,codegraph query, etc.Repro (from the issue)
CODEGRAPH_VERSION's initializer is a parenthesized member-expression — not a recognized "literal" shape — socodegraph where --fileshowed 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(includingconst [a, b] = ...array-pattern destructuring) now becomes a"constant"Definition, independent of initializer shape.collectExportedDeclarationsand its Rust mirrorcollect_exported_var_declarationsgot the identical change, since the two predicates must stay in sync — theexported = 1UPDATE 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_literalwhitelist helpers in both engines.Native/WASM decoupling
The Rust walk previously conflated Definition creation with pts
fn_ref_bindingcollection (const alias = handler) inside a single mutually-exclusiveif/else ifchain. Naively widening the "constant" branch there would have silently droppedfn_ref_bindingtracking 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 runscollectFnRefBindingsas a fully independent pass ("must run before any early return") — soconst alias = handlernow gets both a"constant"Definition foraliasand the pts alias tracking, matching parity between engines.Test plan
npx vitest run tests/parsers/javascript.test.ts— 202/202 passed (new#1819describe 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 dedicatedconst_alias_gets_both_definition_and_fn_ref_bindingregression guard for the decoupling)npm test— full suite green (3728 passed, 30 skipped, 2 todo)npm run lint— cleannpm run build+ native addon rebuilt locally (napi build --platform --release+ codesign) and verified end-to-end against a real repro file:codegraph where --filenow listsCODEGRAPH_VERSION,config(call-expression init), andalias(identifier init) asconstantdefinitions, with the exported ones correctly marked — identical output from both the native and WASM engines (same node/edge counts, same symbol list)Fixes #1819