feat: show re-exported symbols for barrel files#515
Conversation
Barrel/re-export files like src/db/index.js previously showed "No exported symbols found" despite being heavily-imported entry points. The exports command now follows outgoing reexport edges to gather symbols from target modules, displaying them grouped by origin file. Supports --unused filtering and JSON/MCP output. Impact: 5 functions changed, 4 affected
- Add totalReexported and totalReexportedUnused fields so barrel file statistics accurately reflect re-exported symbols (previously both were always 0 for pure barrel files) - Apply limit/offset pagination to reexportedSymbols (previously returned in full regardless of pagination options) - Update integration tests to verify new counter fields
|
Claude finished @carlos-alm's task —— View job PR Review Complete ⭐⭐⭐⭐☆Reviewing PR #515: feat: show re-exported symbols for barrel files Todo List
Root Cause AnalysisProblem Being Solved: Why This Approach: The solution follows outgoing Risk Assessment: Low risk - purely additive enhancement that doesn't change existing behavior for non-barrel files. Backlog Compliance
Critical ConcernsPerformance considerations:
Code quality issues (minor):
Final Recommendation
Strengths:
Minor concerns:
Condition: Monitor performance in production use. If barrel files with >50 re-exported symbols show latency issues, consider batching the consumer queries in a future optimization. This is well-executed incremental improvement that directly helps agents understand barrel file API surfaces - exactly the kind of agent-helping structural analysis codegraph should excel at. |
Greptile SummaryThis PR extends the
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[exportsData called for barrel.js] --> B[exportsFileImpl]
B --> C[Find file node for barrel.js]
C --> D[Query exported symbols\nfrom barrel.js itself\n→ results]
D --> E{hasExportedCol?}
E -- yes --> F[SELECT WHERE exported=1]
E -- no --> G[Fallback: cross-file call targets]
F & G --> H[buildSymbolResult for each\nown exported symbol]
H --> I[Query outgoing reexports edges\nSELECT DISTINCT n.file WHERE\nsource_id=fn.id AND kind='reexports'\n→ reexportTargets]
I --> J{For each target file}
J --> K[Query exported symbols\nfrom target file]
K --> L[buildSymbolResult + originFile]
L --> M[Push to reexportedSymbols]
M --> J
J -- done --> N[Apply unused filter to\nboth results + reexportedSymbols]
N --> O[Compute totalReexported\ntotalReexportedUnused]
O --> P[paginateResult on results field]
P --> Q{opts.limit != null?}
Q -- yes --> R[Slice reexportedSymbols\nUpdate _pagination.hasMore\n⚠️ total + returned NOT updated]
Q -- no --> S[Return as-is]
R & S --> T[Return paginated object]
Last reviewed commit: "Merge remote-trackin..." |
| } | ||
|
|
||
| if (hasReexported) { | ||
| const totalReexported = data.reexportedSymbols.length; |
There was a problem hiding this comment.
Header shows page count instead of total count
When pagination is active (e.g., --limit 5 with 20 total re-exported symbols), data.reexportedSymbols has already been sliced to the current page. Using .length here will display the page size (e.g., 5) instead of the true total in the header.
The fix should use the pre-pagination counters from the domain layer:
| const totalReexported = data.reexportedSymbols.length; | |
| const totalReexported = opts.unused ? data.totalReexportedUnused : data.totalReexported; |
There was a problem hiding this comment.
Fixed — the CLI header now uses data.totalReexported or data.totalReexportedUnused (with fallback to .length for backward compatibility) instead of reading from the paginated array.
| const reexportTargets = db | ||
| .prepare( | ||
| `SELECT DISTINCT n.id, n.file FROM edges e JOIN nodes n ON e.target_id = n.id | ||
| WHERE e.source_id = ? AND e.kind = 'reexports'`, | ||
| ) | ||
| .all(fn.id); |
There was a problem hiding this comment.
DISTINCT n.id, n.file may yield duplicate target files
This query selects DISTINCT n.id, n.file, so if a barrel file has multiple reexports edges targeting different symbol nodes within the same file (e.g., barrel.add → lib.add, barrel.multiply → lib.multiply), each distinct (id, file) pair produces a separate target entry. The inner loop then queries all exported symbols from target.file once per row, leading to duplicate reexportedSymbols entries for that file.
The existing "reverse reexports" query (line 139) correctly deduplicates on file alone:
SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
WHERE e.target_id = ? AND e.kind = 'reexports'The new query should follow the same pattern:
| const reexportTargets = db | |
| .prepare( | |
| `SELECT DISTINCT n.id, n.file FROM edges e JOIN nodes n ON e.target_id = n.id | |
| WHERE e.source_id = ? AND e.kind = 'reexports'`, | |
| ) | |
| .all(fn.id); | |
| const reexportTargets = db | |
| .prepare( | |
| `SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.target_id = n.id | |
| WHERE e.source_id = ? AND e.kind = 'reexports'`, | |
| ) | |
| .all(fn.id); |
And the loop below should be updated to use target.file directly (as it already does), removing the now-unused target.id.
There was a problem hiding this comment.
Fixed — SQL now uses SELECT DISTINCT n.file instead of SELECT DISTINCT n.id, n.file, matching the pattern used by the reverse reexports query. This prevents duplicate symbol entries when multiple reexport edges target the same file.
src/domain/analysis/exports.js
Outdated
| if (opts.limit != null || opts.offset != null) { | ||
| const off = opts.offset || 0; | ||
| const lim = opts.limit != null ? opts.limit : paginated.reexportedSymbols.length; | ||
| paginated.reexportedSymbols = paginated.reexportedSymbols.slice(off, off + lim); | ||
| } |
There was a problem hiding this comment.
Offset-only pagination diverges from
paginateResult behaviour
paginateResult (line 87–96 of paginate.js) is a no-op when limit === undefined; it ignores offset entirely in that case. The manual slice here triggers whenever opts.offset != null, even when opts.limit is absent. This means a caller passing only { offset: 5 } would get results unsliced (from paginateResult) but reexportedSymbols sliced from index 5 onwards — an inconsistency that would silently misalign the two arrays for any consumer expecting them to be co-paginated.
Consider guarding this block on opts.limit != null only (matching paginateResult's own guard), or delegating to paginateResult for both fields:
if (opts.limit != null) {
const off = opts.offset || 0;
paginated.reexportedSymbols = paginated.reexportedSymbols.slice(off, off + opts.limit);
}There was a problem hiding this comment.
Fixed — pagination guard now only triggers when opts.limit \!= null, matching paginateResult behaviour. An offset-only call no longer causes a divergence between results and reexportedSymbols.
- SQL query now selects DISTINCT n.file (was DISTINCT n.id, n.file) to avoid duplicate symbol entries when multiple reexport edges target the same file - Pagination guard for reexportedSymbols now triggers only when opts.limit is set, matching paginateResult's own behaviour Impact: 2 functions changed, 2 affected
The CLI header used reexportedSymbols.length which reflects the current page size after pagination. Now uses totalReexported/totalReexportedUnused from the domain layer to display accurate totals. Impact: 1 functions changed, 0 affected
When hasExportedCol is false (older databases), reexported symbols now use the same findCrossFileCallTargets fallback as direct exports instead of silently returning an empty array. _pagination.hasMore now accounts for reexportedSymbols in barrel-only files where direct results are empty, preventing API consumers from truncating results without knowing more pages exist. Impact: 2 functions changed, 1 affected
…t/barrel-exports-v2 Impact: 19 functions changed, 9 affected
|
Addressed both concerns from the latest review:
Both fixes in commit 3c87ea2. |
Summary
codegraph exportson barrel/re-export files (e.g.src/db/index.js) previously returned "No exported symbols found" despite being heavily-imported entry pointsreexportsedges to gather exported symbols from target modules, displaying them grouped by origin file with consumer info--unusedfiltering, JSON/MCP output via newreexportedSymbolsfieldtotalReexportedandtotalReexportedUnusedcounters so barrel file statistics are accuratereexportedSymbolsis paginated with the samelimit/offsetasresultsSupersedes #513 (scoped to barrel exports only — interface tracking split out separately).
Test plan
--unusedfilters them, non-barrel files have empty arraytotalReexportedandtotalReexportedUnusedcountersreexportedSymbolswithoriginFileannotationreexportedSymbolsautomatically (passes throughexportsData)