Skip to content

feat: show re-exported symbols for barrel files#515

Merged
carlos-alm merged 7 commits intomainfrom
feat/barrel-exports-v2
Mar 19, 2026
Merged

feat: show re-exported symbols for barrel files#515
carlos-alm merged 7 commits intomainfrom
feat/barrel-exports-v2

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • codegraph exports on barrel/re-export files (e.g. src/db/index.js) previously returned "No exported symbols found" despite being heavily-imported entry points
  • Now follows outgoing reexports edges to gather exported symbols from target modules, displaying them grouped by origin file with consumer info
  • Supports --unused filtering, JSON/MCP output via new reexportedSymbols field
  • Adds totalReexported and totalReexportedUnused counters so barrel file statistics are accurate
  • reexportedSymbols is paginated with the same limit/offset as results

Supersedes #513 (scoped to barrel exports only — interface tracking split out separately).

Test plan

  • 3 integration tests: barrel shows re-exported symbols, --unused filters them, non-barrel files have empty array
  • Tests verify totalReexported and totalReexportedUnused counters
  • All 13 exports tests pass
  • Lint clean
  • JSON output includes reexportedSymbols with originFile annotation
  • MCP tool returns reexportedSymbols automatically (passes through exportsData)

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
Copy link

claude bot commented Mar 19, 2026

Claude finished @carlos-alm's task —— View job


PR Review Complete ⭐⭐⭐⭐☆

Reviewing PR #515: feat: show re-exported symbols for barrel files

Todo List

  • Phase 1: Root cause analysis - understand the problem being solved
  • Phase 2: Check for configuration changes and rule relaxation
  • Phase 3: Assess test integrity and coverage
  • Phase 4: Review code quality, bugs, performance, security
  • Phase 5: Grade against BACKLOG.md compliance criteria
  • Final Assessment: Provide structured feedback and rating

Root Cause Analysis

Problem Being Solved: codegraph exports on barrel/re-export files (e.g. src/db/index.js) previously returned "No exported symbols found" despite these being heavily-imported entry points. This was misleading - barrel files do have symbols to export, just indirectly through re-exports.

Why This Approach: The solution follows outgoing reexports edges to gather exported symbols from target modules and displays them grouped by origin file. This builds on existing graph relationships and is architecturally sound.

Risk Assessment: Low risk - purely additive enhancement that doesn't change existing behavior for non-barrel files.

Backlog Compliance

  • Zero-dep: ✓ — Uses only existing tree-sitter AST data, SQLite tables, and graph edges
  • Foundation-aligned: ✓ — Directly improves structural code understanding (P1), maintains embeddable-first design (P5), optimizes for development workflows (P8)
  • Problem-fit: 4 — Directly improves agent accuracy by providing complete API surface information, reducing round-trips for barrel file analysis
  • Breaking: No — Purely additive, all new fields (reexportedSymbols, counters) are additional
  • Tier: 1 (zero-dep ✓ + foundation-aligned ✓)

Critical Concerns

Performance considerations:

  • Nested loops (exports.js:154-169) with per-file getFileLines() calls could be expensive for large barrel files
  • N+1 query pattern in buildSymbolResult - each symbol triggers a separate DB query for consumer lookup
  • Recommendation: Consider batching consumer queries for all symbols in a file

Code quality issues (minor):

  • Missing JSDoc comments on new functions like printReexportedSymbols
  • Could benefit from extracting barrel detection logic into a named predicate

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE WITH CONDITIONS
  • Reasoning:

Strengths:

Minor concerns:

  • Performance could be optimized for large barrel files, but not blocking
  • The feature is high-value for codebases using barrel exports (very common pattern)

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.

@carlos-alm
Copy link
Contributor Author

@greptileai

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR extends the codegraph exports command to handle barrel/re-export files by following outgoing reexports edges and surfacing symbols from their target modules, grouped by origin file. It adds reexportedSymbols, totalReexported, and totalReexportedUnused to the domain response, co-paginates them with results, and updates the CLI presentation layer accordingly.

  • Adds buildSymbolResult helper (extracted from the .map() callback) and reuses it when collecting symbols from re-export targets
  • New SQL query (SELECT DISTINCT n.file … WHERE e.source_id = ? AND e.kind = 'reexports') correctly finds all files a barrel re-exports, matching the deduplication pattern already used for the reverse direction
  • CLI correctly uses data.totalReexported / data.totalReexportedUnused for the section header rather than the (already-paginated) array length
  • _pagination.hasMore is patched to account for re-exported symbols so barrel-only files don't falsely report the end of the page, but _pagination.total and _pagination.returned are not updated — they remain 0 for pure barrel files, producing misleading metadata for MCP consumers
  • Three new integration tests cover the barrel happy path and --unused filtering but no test exercises pagination on a barrel file, leaving the metadata bug undetected

Confidence Score: 3/5

  • Mostly safe to merge; the core data (reexportedSymbols array) is correct, but the pagination metadata is broken for pure barrel files under MCP usage.
  • The feature logic, SQL queries, consumer-count calculation, and CLI rendering are all correct and well-tested for the non-paginated path. However, _pagination.total and _pagination.returned are not updated to include re-exported symbols, meaning any MCP consumer (which always applies a default limit of 20) will see total: 0, returned: 0 for a pure barrel file even though the response carries items in reexportedSymbols. This is a concrete, reproducible metadata bug that could cause client-side iteration logic to stop prematurely or misrender counts.
  • src/domain/analysis/exports.js — the _pagination update block (lines 73–78) needs to also increment total and returned by the re-exported symbol counts.

Important Files Changed

Filename Overview
src/domain/analysis/exports.js Adds barrel-file re-export resolution via outgoing reexports edges; extracts buildSymbolResult; adds pagination for reexportedSymbols. The _pagination.total and _pagination.returned fields are not updated to reflect re-exported symbols, producing misleading metadata for MCP consumers on pure barrel files.
src/presentation/queries-cli/exports.js Adds printReexportedSymbols (grouped by origin file) and updates fileExports to display barrel-file sections with accurate totals. Correctly uses data.totalReexported / data.totalReexportedUnused for the header rather than the paginated array length. No issues found.
tests/integration/exports.test.js Adds three integration tests for barrel-file re-export: shows symbols, filters by --unused, and verifies non-barrel files return an empty array. Coverage is good for the happy path but misses pagination scenarios for barrel files (no test exercises limit/offset on a barrel, so the _pagination metadata bug is not caught).

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]
Loading

Last reviewed commit: "Merge remote-trackin..."

}

if (hasReexported) {
const totalReexported = data.reexportedSymbols.length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
const totalReexported = data.reexportedSymbols.length;
const totalReexported = opts.unused ? data.totalReexportedUnused : data.totalReexported;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +146 to +151
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +68 to +72
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@carlos-alm
Copy link
Contributor Author

@greptileai

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
@carlos-alm
Copy link
Contributor Author

Addressed both concerns from the latest review:

  1. hasExportedCol = false fallback — reexported symbols now use the same findCrossFileCallTargets fallback as direct exports instead of silently returning an empty array.

  2. _pagination.hasMore for barrel-only files_pagination.hasMore now accounts for reexportedSymbols total, so API/MCP consumers correctly see when more pages exist even when direct results is empty.

Both fixes in commit 3c87ea2.

@carlos-alm
Copy link
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit e38c256 into main Mar 19, 2026
15 checks passed
@carlos-alm carlos-alm deleted the feat/barrel-exports-v2 branch March 19, 2026 06:21
@github-actions github-actions bot locked and limited conversation to collaborators Mar 19, 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.

1 participant