Skip to content

fix: correct exported-symbol detection for literal and object-literal exports#1820

Merged
carlos-alm merged 7 commits into
mainfrom
fix/issue-1728-where-file-exported-omissions
Jul 6, 2026
Merged

fix: correct exported-symbol detection for literal and object-literal exports#1820
carlos-alm merged 7 commits into
mainfrom
fix/issue-1728-where-file-exported-omissions

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • handleExportCapture/handleExportStmt (JS/TS extractor, both query and walk paths) and the native handle_export_declaration only recognized function/class/interface/type-alias declarations as exports — export const/let/var … was never marked exported in either engine, regardless of initializer shape. This left the exported DB column unset for every exported constant.
  • Separately, whereFileImpl computed its exported array via cross-file calls edges instead of the exported DB column (unlike exportsFileImpl, which already did this correctly) — so even DB data alone wasn't the full picture for where --file.
  • Fixed both: extractor now classifies exported var/const/let declarations the same way handleVariableDeclarator does; whereFileImpl now shares exportsFileImpl's correct logic via a new findExportedNodesByFile helper.

Closes #1728

Test plan

  • npm test — full suite green (3378 passed, 0 failed)
  • npm run lint — clean
  • Native Rust rebuilt, codesigned, and used for end-to-end verification
  • Both repro cases confirmed fixed against the real native engine: MAX_WALK_DEPTH (bare-literal init) and command (object-literal-with-methods, all 49 CLI command files) now appear in exported

Filed #1817, #1818, #1819 for out-of-scope findings surfaced during investigation (pre-existing Rust warning, native/WASM ordering-only divergence, a separate "not extracted as a definition at all" gap for member/call-expression initializers).

Stacked on #1816 (base branch fix/issue-1726-fnimpact-query-file-crash) — only the extractor/repository/analysis/test diff is this issue's change.

BATCH_COMMANDS entries with sig: 'dbOnly' (currently only complexity)
always wrote their target into opts.target. complexityData treats
opts.target as a symbol-name filter and opts.file as the file-path
filter, so batch complexity <file> never matched anything and the
function list silently fell back to empty with a whole-repo summary
regardless of which file was requested.

Add an optional targetKey on BatchCommandEntry so each dbOnly command
can declare which opts key its targets map to, and set it to 'file'
for complexity. batchData and multiBatchData now route the target
through the declared key instead of assuming opts.target.

Fixes #1721

Impact: 4 functions changed, 6 affected
…ssification

codegraph roles --role dead flagged every function parameter as dead-leaf and
every interface/type member as dead-unresolved, regardless of actual usage.

Root cause: the role classifier's fan-in-based "no callers = dead" heuristic
is meaningless for these two symbol categories, since neither can ever have
inbound call edges by construction:

- Parameters: `kind = 'parameter'` nodes were unconditionally force-assigned
  dead-leaf via a fast-path bypass in classifyNodeRolesFull/Incremental (TS)
  and do_classify_full/do_classify_incremental (native), before any fan-in
  was even computed. A parameter's liveness is a local dataflow question (is
  it referenced within its own function body), not a call-graph reachability
  question, so this produced ~90% noise in --role dead output.

- Interface/type members: every language extractor qualifies interface/type
  members as `Owner.member` top-level definitions (mirroring class method
  qualification), and they never receive inbound call edges (they're
  consumed via type annotations, not calls). Lacking any recognition of
  this, they fell through the normal fan-in/fan-out path and landed in
  dead-unresolved.

Fix:
- Parameters are now fully excluded from role classification (role stays
  NULL), the same treatment already given to file/directory nodes.
- Interface/type members are now recognized in the classifier by resolving
  the Owner.member prefix against same-file TYPE_DEF_KINDS declarations
  (interface/type/struct/enum/trait/record) and classified `leaf`
  unconditionally. Class methods use the identical Owner.member naming
  convention but are unaffected since `class` is not in TYPE_DEF_KINDS, so
  real dead methods/functions remain detected.

Applied to both the TS/WASM classifier (graph/classifiers/roles.ts,
features/structure.ts) and the native Rust classifier
(graph/classifiers/roles.rs) per the dual-engine parity requirement; verified
both engines produce identical results end-to-end against this repo's own
graph.

Fixes #1723

Impact: 6 functions changed, 5 affected
codegraph exports <file> --json (and the --unused dead-export filter it
feeds) reported zero consumers for interfaces/types that are demonstrably
imported and used elsewhere via `import type { X }`, even though
`codegraph deps <file>` correctly showed the importing file in
`importedBy`. Example: ChaContext in src/domain/graph/builder/cha.ts is
imported (type-only) by build-edges.ts and native-orchestrator.ts, but
`codegraph exports` showed consumerCount: 0.

Root cause: exportsFileImpl's per-symbol consumers query
(domain/analysis/exports.ts) only looked at kind = 'calls' edges. The
builder already emits a symbol-level `imports-type` edge for `import
type { X }` statements (source = importing file node, target = the
specific imported symbol -- see emitTypeOnlySymbolEdges in
build-edges.ts/incremental.ts), which `codegraph deps` reads from, but
the exports consumer query never looked at this edge kind. Role
classification (features/structure.ts) already includes 'imports-type'
in its fan-in formula, so `codegraph roles --role dead` was unaffected --
this was purely a gap in exports's independent consumer list.

Fix: widen the consumers query to `kind IN ('calls', 'imports-type')`,
matching the edge-kind set already used everywhere else in the codebase
for cross-file usage credit (structure.ts, graph-enrichment.ts,
boundaries.ts, dependencies.ts). No native Rust changes needed --
domain/analysis/exports.ts is pure query-layer code that reads the
already-built edges table and has no engine-specific mirror.

Deliberately does NOT add `extends`/`implements`: investigation found
those edges are resolved by symbol name only, with no file/import
scoping (buildClassHierarchyEdges in both build-edges.ts/incremental.ts
and the native emit_hierarchy_edges), so they link same-named
declarations across unrelated files (verified: this repo's own graph has
false `implements`/`extends` edges between unrelated fixture classes
across languages and a `Repository` interface in src/types.ts). Filed
as #1812. Also filed #1813 for a related but distinct gap: `import {
type X }` inline per-specifier modifiers aren't tracked as type-only in
either engine's extractor, so such X still gets no credit even after
this fix.

Added tests/integration/exports.test.ts coverage: an interface consumed
only via a symbol-level `imports-type` edge gets consumerCount >= 1 and
is excluded from --unused, while a genuinely unreferenced interface
still shows 0 consumers.

Verified against this repo's own graph: `codegraph exports
src/domain/graph/builder/cha.ts -T --json` now credits ChaContext with
2 consumers (build-edges.ts, native-orchestrator.ts). Full test suite
(201 files, 3359 tests) and lint pass clean.

docs check acknowledged: internal bug fix to exports consumer
computation, no new feature/language/CLI surface/architecture change --
README.md, CLAUDE.md, and ROADMAP.md are unaffected.

Fixes #1724

Impact: 1 functions changed, 3 affected
The CLI's `-f/--file` option is a repeatable Commander accumulator
(collectFile) that always produces a string[], even on first use. The
native composite bindings backing fn-impact (findNodesWithFanIn) and
query (fnDeps) forwarded that array straight into napi-rs functions
whose Rust signatures only accepted a single String, so any use of
-f/--file crashed with "Failed to convert JavaScript value ... into
rust type `String`" regardless of engine defaults. context worked
only because its code path never touches the native repository for
symbol lookup.

Widen the native Rust signatures (find_nodes_with_fan_in, fn_deps) to
accept Vec<String> and build an OR-of-LIKE clause for multiple files,
mirroring buildFileConditionSQL/NodeQuery.fileFilter on the JS side.
Normalize the file option before calling into the native binding on
the TS side, and thread the widened string | string[] type through
QueryOpts, fnDeps's opts, and the call chain down to
findMatchingNodes. This also makes repeated -f/--file genuinely
multi-file end to end for fn-impact/query, matching context's
existing behavior, instead of crashing on any use.

findNodesByScope shares QueryOpts but has no CLI caller and its
native binding still only accepts one file; it now takes the first
value defensively rather than crash or fail to type-check.

Fixes #1726

docs check acknowledged: pure bug fix, no new CLI options/languages/
architecture — the -f/--file "repeatable" docs are now accurate
rather than needing correction.

Impact: 14 functions changed, 34 affected
… exports

Two compounding root causes made `where --file`'s `exported` array (and
`codegraph exports`) unreliable for entire classes of exports:

1. The JS/TS export-statement handler (WASM query path, WASM walk path, and
   the mirrored native Rust extractor) only recognized `export function`,
   `export class`, `export interface`, and `export type` declarations. It
   never matched `lexical_declaration`/`variable_declaration`, so
   `export const/let/var …` was never added to the extractor's export list —
   regardless of the initializer's shape — leaving the `exported=1` DB column
   permanently unset for every exported constant in the codebase (including
   ones that happened to look "correct", like a `new Set(...)`-initialized
   constant referenced as a call argument elsewhere).

2. `where --file`'s `exported` list ignored the `exported` DB column entirely
   and computed membership from `findCrossFileCallTargets` (symbols targeted
   by a cross-file `calls` edge) — a heuristic that only "worked" by
   coincidence for symbols that happened to be passed as call arguments
   elsewhere. `codegraph exports` already had the right idea (prefer the
   `exported` column, fall back to the heuristic only for pre-migration DBs)
   but duplicated that logic locally instead of sharing it.

Fixes:
- src/extractors/javascript.ts: extract the function/class/interface/type
  kind map plus a new lexical/variable-declaration branch into a shared
  `collectExportedDeclarations`, used by both `handleExportCapture` (query
  path) and `handleExportStmt` (walk path) so they can't drift apart again.
  Declarator values are classified with the same predicate already used to
  build the matching Definition (function-valued -> kind 'function',
  literal/array/object/new-expression-valued `const` -> kind 'constant').
- crates/codegraph-core/src/extractors/javascript.rs: mirror the same fix in
  `handle_export_declaration` via a new `collect_exported_var_declarations`.
- src/db/repository/nodes.ts: add `findExportedNodesByFile`, the single
  shared implementation of "prefer exported=1, fall back to cross-file calls
  for pre-migration DBs" — re-exported through db/repository/index.ts and
  db/index.ts.
- src/domain/analysis/exports.ts: use the shared helper instead of a locally
  duplicated hasExportedCol/exportedNodesStmt implementation.
- src/domain/analysis/symbol-lookup.ts: `whereFileImpl` now uses the shared
  helper instead of `findCrossFileCallTargets` directly, so `where --file`
  and `exports` agree on what "exported" means.

Tests:
- tests/parsers/javascript.test.ts: unit coverage for bare-literal, new
  Set(...), object-literal-with-methods, and arrow-function exported consts,
  plus a non-exported-const negative case and a function/class regression
  guard for the shared-helper refactor.
- tests/engines/query-walk-parity.test.ts, tests/engines/parity.test.ts:
  cross-path/cross-engine regression cases for the same shapes.
- tests/integration/queries.test.ts: new fixture node with exported=1 but
  zero incoming edges of any kind, proving `where --file`'s exported list
  no longer depends on cross-file call presence.

No user-facing command/language/architecture changes, so README/CLAUDE.md/
ROADMAP.md are unaffected — docs check acknowledged.

Fixes #1728

Impact: 8 functions changed, 17 affected
@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two related bugs in exported-symbol detection: (1) the TS and Rust extractors only recognized function/class/interface/type-alias export statements and silently dropped every export const/let/var …, leaving exported = 0 in the DB; (2) whereFileImpl used a cross-file calls-edge heuristic to determine the exported surface instead of the exported column, so value-only exports (e.g. constants never called from other files) were invisible to codegraph where --file.

  • Extractor fix: New collectExportedDeclarations (TS) and collect_exported_var_declarations (Rust) classify each declarator in a lexical_declaration/variable_declaration using the same kind predicate (isConstantValue/is_js_literal) as the matching Definition-building paths, ensuring the (name, kind, file, line) tuple used by the UPDATE SET exported = 1 query matches the already-inserted row.
  • Repository refactor: findExportedNodesByFile is extracted into db/repository/nodes.ts as a shared helper that prefers the exported = 1 column and falls back to the cross-file-calls heuristic for pre-column DBs, replacing duplicated inline logic in exports.ts and symbol-lookup.ts.
  • Test coverage: Parser unit tests, query-vs-walk parity tests, native-vs-WASM engine parity tests, and an integration test with a zero-call-edge exported constant all added.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to export detection and the shared-helper extraction, both paths are covered by parity and integration tests, and the UPDATE predicate alignment was verified against the DB schema.

Both the TS and Rust extractor changes mirror the existing definition-building kind predicates exactly (isConstantValue/is_js_literal, same function-type list), so the (name, kind, file, line) tuple written by the export collector matches the row already inserted for the definition. The shared findExportedNodesByFile helper is a straightforward consolidation of logic that was already correct in exportsFileImpl; the only material change is that whereFileImpl now uses the same correct path. Test coverage spans parser unit tests, query-vs-walk parity, native-vs-WASM engine parity, and an integration fixture with a zero-edge exported constant specifically targeting the regression.

No files require special attention.

Important Files Changed

Filename Overview
src/extractors/javascript.ts Adds collectExportedDeclarations shared helper that classifies each declarator in an exported const/let/var declaration by the same kind predicate used in the definition-building path; both handleExportCapture and handleExportStmt now delegate to it, eliminating the drift risk.
crates/codegraph-core/src/extractors/javascript.rs Adds collect_exported_var_declarations called from handle_export_declaration for lexical_declaration/variable_declaration; uses the same is_js_literal predicate and is_const check as handle_var_decl, and is consistent with the start_line(node) used in definition inserts.
src/db/repository/nodes.ts Introduces findExportedNodesByFile with a cached column-presence probe and a knownSymbols optimisation parameter for the legacy-DB fallback path; logic is a clean extraction of what was previously duplicated in exports.ts.
src/domain/analysis/exports.ts Removes all inline hasExportedCol/_hasExportedColCache logic and replaces it with calls to findExportedNodesByFile; exportsFileImpl passes its already-fetched symbols array to avoid a second query in the legacy-DB path.
src/domain/analysis/symbol-lookup.ts Replaces the findCrossFileCallTargets heuristic in whereFileImpl with findExportedNodesByFile, fixing the bug where value-only exports were omitted from codegraph where --file's exported list.
tests/integration/queries.test.ts Adds API_VERSION constant node with exported=1 but no incoming edges as a regression fixture for #1728; asserts it appears in where --file's exported list independent of any call graph membership.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["export const X = 42 / export const fn = () => {}"] --> B{Extractor Path}
    B -- "Query (TS)" --> C["handleExportCapture\n→ collectExportedDeclarations"]
    B -- "Walk (TS)" --> D["handleExportStmt\n→ collectExportedDeclarations"]
    B -- "Native (Rust)" --> E["handle_export_declaration\n→ collect_exported_var_declarations"]
    C --> F["ExportInfo {name, kind, line}"]
    D --> F
    E --> F
    F --> G["INSERT OR IGNORE nodes …\nUPDATE SET exported=1\nWHERE name=? AND kind=? AND file=? AND line=?"]

    H["codegraph exports / where --file"] --> I["findExportedNodesByFile(db, file)"]
    I --> J{hasExportedColumn?}
    J -- "Yes (modern DB)" --> K["SELECT * FROM nodes\nWHERE exported=1"]
    J -- "No (legacy DB)" --> L["findCrossFileCallTargets\n(heuristic fallback)"]
    K --> M["Correct exported surface\n(includes value-only exports)"]
    L --> M
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["export const X = 42 / export const fn = () => {}"] --> B{Extractor Path}
    B -- "Query (TS)" --> C["handleExportCapture\n→ collectExportedDeclarations"]
    B -- "Walk (TS)" --> D["handleExportStmt\n→ collectExportedDeclarations"]
    B -- "Native (Rust)" --> E["handle_export_declaration\n→ collect_exported_var_declarations"]
    C --> F["ExportInfo {name, kind, line}"]
    D --> F
    E --> F
    F --> G["INSERT OR IGNORE nodes …\nUPDATE SET exported=1\nWHERE name=? AND kind=? AND file=? AND line=?"]

    H["codegraph exports / where --file"] --> I["findExportedNodesByFile(db, file)"]
    I --> J{hasExportedColumn?}
    J -- "Yes (modern DB)" --> K["SELECT * FROM nodes\nWHERE exported=1"]
    J -- "No (legacy DB)" --> L["findCrossFileCallTargets\n(heuristic fallback)"]
    K --> M["Correct exported surface\n(includes value-only exports)"]
    L --> M
Loading

Reviews (4): Last reviewed commit: "Merge origin/main into fix/issue-1728-wh..." | Re-trigger Greptile

Comment thread src/db/repository/nodes.ts Outdated
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

8 functions changed17 callers affected across 8 files

  • hasExportedColumn in src/db/repository/nodes.ts:193 (6 transitive callers)
  • findExportedNodesByFile in src/db/repository/nodes.ts:226 (7 transitive callers)
  • collectReexportedSymbols in src/domain/analysis/exports.ts:98 (3 transitive callers)
  • exportsFileImpl in src/domain/analysis/exports.ts:120 (3 transitive callers)
  • whereFileImpl in src/domain/analysis/symbol-lookup.ts:169 (3 transitive callers)
  • collectExportedDeclarations in src/extractors/javascript.ts:223 (6 transitive callers)
  • handleExportCapture in src/extractors/javascript.ts:257 (3 transitive callers)
  • handleExportStmt in src/extractors/javascript.ts:1491 (3 transitive callers)

…ol fallback (#1820)

Impact: 3 functions changed, 9 affected
@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

Base automatically changed from fix/issue-1726-fnimpact-query-file-crash to main July 6, 2026 01:34
Resolves conflicts from stacked-branch divergence: main now has the
findNodesByScope warn()/TODO(#1815) fix and the fn-impact --file test
from #1816 (merged after this branch forked), and the pre-refactor
inline hasExportedCol/exportedNodesStmt block in exports.ts that this
PR's own commit already replaced with the shared findExportedNodesByFile.

docs check acknowledged — conflict resolution only, no new functionality.

Impact: 2 functions changed, 6 affected
@carlos-alm carlos-alm merged commit 701d89e into main Jul 6, 2026
29 checks passed
@carlos-alm carlos-alm deleted the fix/issue-1728-where-file-exported-omissions branch July 6, 2026 02:03
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 6, 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.

codegraph where --file omits genuinely exported symbols from the 'exported' array

1 participant