fix: correct exported-symbol detection for literal and object-literal exports#1820
Conversation
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 SummaryThis PR fixes two related bugs in exported-symbol detection: (1) the TS and Rust extractors only recognized function/class/interface/type-alias
Confidence Score: 5/5Safe 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 ( No files require special attention. Important Files Changed
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
%%{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
Reviews (4): Last reviewed commit: "Merge origin/main into fix/issue-1728-wh..." | Re-trigger Greptile |
Codegraph Impact Analysis8 functions changed → 17 callers affected across 8 files
|
…ol fallback (#1820) Impact: 3 functions changed, 9 affected
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
Summary
handleExportCapture/handleExportStmt(JS/TS extractor, both query and walk paths) and the nativehandle_export_declarationonly 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 theexportedDB column unset for every exported constant.whereFileImplcomputed itsexportedarray via cross-filecallsedges instead of theexportedDB column (unlikeexportsFileImpl, which already did this correctly) — so even DB data alone wasn't the full picture forwhere --file.handleVariableDeclaratordoes;whereFileImplnow sharesexportsFileImpl's correct logic via a newfindExportedNodesByFilehelper.Closes #1728
Test plan
npm test— full suite green (3378 passed, 0 failed)npm run lint— cleanMAX_WALK_DEPTH(bare-literal init) andcommand(object-literal-with-methods, all 49 CLI command files) now appear inexportedFiled #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.