Skip to content

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

Closed
carlos-alm wants to merge 3 commits intomainfrom
feat/barrel-exports
Closed

feat: show re-exported symbols for barrel files#513
carlos-alm wants to merge 3 commits intomainfrom
feat/barrel-exports

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 (86 fan-in)
  • 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, and renamed "Re-exported by" label for clarity

Test plan

  • 3 new integration tests: barrel shows re-exported symbols, --unused filters them, non-barrel files have empty array
  • All 1926 existing tests pass (0 failures)
  • Verified CLI output with both native and WASM engines
  • JSON output includes reexportedSymbols with originFile annotation
  • MCP tool returns reexportedSymbols automatically (passes through exportsData)

Extract implements/extends relationships from tree-sitter AST and store
as implements edges. New commands and impact integration:

- `codegraph implementations <name>` — find all concrete types
  implementing a given interface/trait
- `codegraph interfaces <name>` — find all interfaces/traits a
  class/struct implements
- fn-impact and diff-impact now include implementors in blast radius
  by default (--no-implementations to opt out)
- context command shows implementors for interfaces and implements
  for classes
- MCP tools: implementations, interfaces
- buildClassHierarchyEdges now matches struct/record/enum source
  kinds and trait/interface target kinds (was class-only)

Covers: TypeScript interfaces, Java interfaces/abstract classes,
Rust traits, C# interfaces, Go structs, PHP interfaces

Impact: 20 functions changed, 22 affected
Add typescript devDependency with strict tsconfig.json (allowJs for
incremental migration, nodenext modules, path aliases matching module
structure). Build emits to dist/ with declarations, source maps, and
incremental compilation. Package exports/bin/files now point to dist/.

- CI: add tsc --noEmit typecheck gate to ci-pipeline
- Publish: add explicit build step to both dev and stable workflows
- Scripts: build, typecheck, clean, prepack; prepare includes build
- Tests: update index-exports assertion for dist/ paths
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
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR delivers three related features: (1) barrel/re-export file support in codegraph exports, surfacing symbols from re-export targets grouped by origin file; (2) implementations and interfaces commands (CLI + MCP) for navigating interface/trait hierarchies; and (3) a TypeScript type-check build step added to CI and the publish pipeline. The barrel-file feature integrates cleanly into the existing exportsData path by querying reexports edges, and the interface/implementor work correctly extends bfsTransitiveCallers, contextData, and the graph edge builder.

Key issues found:

  • --no-implementations flag is silently ignored for diff-impact: diffImpactData accepts opts.includeImplementors (set by the CLI) but never passes it through buildFunctionImpactResultsbfsTransitiveCallers. The BFS always includes implementors regardless of the flag.
  • BFS depth collapse for interfaces: Because implementors are pushed onto frontier before the main d=1 loop, callers of those implementors land in levels[1] at the same depth as the implementors themselves. This makes --depth 1 effectively behave like --depth 2 when the start node is an interface.
  • totalUnused / totalExported are misleading for barrel files: Both counts reflect only the file's own symbols. Pure barrel files always show totalExported: 0, totalUnused: 0, even when all re-exported symbols are unused.
  • reexportedSymbols is not paginated: The field is returned in full regardless of limit/offset, which could be a problem for large barrel files (e.g., a root index.ts aggregating hundreds of modules).

Confidence Score: 3/5

  • Safe to merge with known limitations; one P1 logic bug makes a new CLI flag a no-op, and two P2 issues affect output correctness for barrel files.
  • The core barrel-file feature and the implementations/interfaces commands work correctly and are well-tested. However, the --no-implementations flag for diff-impact is accepted but silently ignored (P1), the BFS depth model for interfaces conflates depth-1 and depth-2 nodes, and the barrel-file statistics (totalUnused, totalExported) are misleading. These reduce confidence from a 5 but don't block functionality for the primary use case.
  • src/domain/analysis/impact.js (BFS depth + missing includeImplementors propagation to diffImpactData) and src/domain/analysis/exports.js (statistics accuracy + pagination for reexportedSymbols).

Important Files Changed

Filename Overview
src/domain/analysis/exports.js Core barrel-file feature: adds reexportedSymbols by querying reexports edges. Two issues: totalUnused/totalExported don't account for re-exported symbols, and reexportedSymbols is not paginated.
src/domain/analysis/impact.js BFS extended with implementor seeding. Two issues: diffImpactData never reads opts.includeImplementors (making --no-implementations on diff-impact a no-op), and implementors + their callers collapse to the same BFS depth level.
src/domain/analysis/implementations.js New module for implementationsData / interfacesData. Clean structure, proper db.close() in finally blocks, pagination handled correctly.
src/domain/analysis/context.js Adds buildImplementationInfo to enrich context results with implementors (for interfaces) or implemented interfaces (for classes). Logic is clean and correctly guarded by kind sets.
src/db/repository/edges.js Adds findImplementors and findInterfaces with correct SQL and WeakMap-cached prepared statements. Queries are correct (source→target direction for implements edges).
src/cli/commands/diff-impact.js Adds --no-implementations flag and passes includeImplementors in opts, but diffImpactData never reads this option — the flag is effectively a no-op.
src/cli/commands/implementations.js New CLI command for listing implementors. Follows established command pattern correctly.
src/cli/commands/interfaces.js New CLI command for listing implemented interfaces. Follows established command pattern correctly.
src/domain/graph/builder/stages/build-edges.js Extends class hierarchy edge building to support structs, records, enums, and traits via Sets instead of hardcoded === 'class' checks. Clean refactor.
tests/integration/implementations.test.js Thorough integration tests covering implementationsData, interfacesData, contextData enrichment, and BFS blast radius with implementors. Tests confirm expected behavior but don't validate BFS depth correctness for implementors.
tsconfig.json New TypeScript configuration for type-checking and building. Reasonable strict settings with allowJs: true, checkJs: false for incremental JS→TS migration. $schema points to json-schema.org meta-schema rather than SchemaStore, which may reduce IDE tooling support.
package.json Switches entrypoints from src/ to dist/, adds build/typecheck/clean/prepack scripts, adds typescript devDependency. Breaking change for any consumers pointing directly to src/index.js, but dist/ is now included in files.
.github/workflows/ci.yml Adds typecheck job to CI pipeline, correctly added to ci-pipeline needs array. Retry logic for npm install is consistent with existing test job pattern.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI (codegraph exports)
    participant exportsData
    participant exportsFileImpl
    participant DB as SQLite DB

    CLI->>exportsData: exportsData(file, dbPath, opts)
    exportsData->>exportsFileImpl: exportsFileImpl(db, target, noTests, getFileLines, unused)
    exportsFileImpl->>DB: findFileNodes (LIKE %target%)
    DB-->>exportsFileImpl: fileNodes[]

    loop for each fileNode (fn)
        exportsFileImpl->>DB: SELECT own exports (exported=1)
        DB-->>exportsFileImpl: exported[]
        exportsFileImpl->>DB: SELECT reexports edges (source=fn.id, kind='reexports')
        DB-->>exportsFileImpl: reexportTargets[]

        loop for each reexportTarget
            exportsFileImpl->>DB: SELECT exported symbols from target.file
            DB-->>exportsFileImpl: targetExported[]
            loop for each symbol s in targetExported
                exportsFileImpl->>DB: SELECT callers (calls edges to s.id)
                DB-->>exportsFileImpl: consumers[]
                Note over exportsFileImpl: push { ...symbolResult, originFile } to reexportedSymbols
            end
        end

        Note over exportsFileImpl: apply unused filter to both results & reexportedSymbols
    end

    exportsFileImpl-->>exportsData: { file, results, reexports, reexportedSymbols, totalExported, totalUnused }
    exportsData-->>CLI: paginateResult (paginates results only, NOT reexportedSymbols)
Loading

Comments Outside Diff (1)

  1. src/domain/analysis/impact.js, line 330-340 (link)

    P1 --no-implementations flag silently ignored in diffImpactData

    diffImpactData passes opts.includeImplementors to neither buildFunctionImpactResults nor the inner bfsTransitiveCallers call, so the --no-implementations CLI flag for diff-impact is accepted but has no effect. The BFS will always include implementors in diff-impact results.

    // in buildFunctionImpactResults — add includeImplementors parameter
    function buildFunctionImpactResults(db, affectedFunctions, noTests, maxDepth, includeImplementors) {
      ...
      const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, {
        noTests,
        maxDepth,
        includeImplementors,
        onVisit(c, parentId) { ... },
      });

    And in diffImpactData, propagate the option:

    const includeImplementors = opts.includeImplementors !== false;
    const { functionResults, allAffected } = buildFunctionImpactResults(
      db,
      affectedFunctions,
      noTests,
      maxDepth,
      includeImplementors,
    );

Last reviewed commit: "feat: show re-export..."

Comment on lines +47 to +68
// Seed: if start node is an interface/trait, include its implementors at depth 1
if (includeImplementors) {
const startNode = findNodeById(db, startId);
if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
const impls = findImplementors(db, startId);
for (const impl of impls) {
if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {
visited.add(impl.id);
frontier.push(impl.id);
if (!levels[1]) levels[1] = [];
levels[1].push({
name: impl.name,
kind: impl.kind,
file: impl.file,
line: impl.line,
viaImplements: true,
});
if (onVisit) onVisit({ ...impl, viaImplements: true }, startId, 1);
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Implementors and their direct callers collapse to the same depth level

Implementors are added to frontier before the loop starts. When the loop runs at d=1, it processes both the original start node AND the implementors — meaning callers of implementors land in levels[1] alongside the implementors themselves.

Concretely: Serializable → UserService (impl, levels[1]) → handleUser (caller of impl, also levels[1]). So --depth 1 effectively exposes depth-2 nodes for interfaces, making the depth limit unreliable when seeding from an interface/trait.

Consider pushing implementors into levels[1] but NOT into frontier — then add them to the frontier at the start of the d=1 loop (or handle them via a dedicated pre-pass that adds them to nextFrontier after processing d=1 callers). This would correctly place their callers at levels[2].

Comment on lines 156 to 170
let filteredResults = results;
let filteredReexported = reexportedSymbols;
if (unused) {
filteredResults = results.filter((r) => r.consumerCount === 0);
filteredReexported = reexportedSymbols.filter((r) => r.consumerCount === 0);
}

return {
file: fn.file,
results: filteredResults,
reexports,
reexportedSymbols: filteredReexported,
totalExported: exported.length,
totalInternal: internalCount,
totalUnused,
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 totalUnused / totalExported don't reflect re-exported symbols

For a pure barrel file (no own exports, only reexportedSymbols), totalExported and totalUnused are always 0 — even if every re-exported symbol has no consumers. The returned statistics paint an inaccurate picture for barrel files and may confuse consumers of the JSON output (e.g., MCP tools that use these counts for summarization).

Consider adding a totalReexported and totalReexportedUnused field alongside the existing counters so callers can accurately surface "this barrel has N re-exported symbols, M of which are unused".

Comment on lines +138 to +153
const reexportedSymbols = [];
for (const target of reexportTargets) {
const targetExported = hasExportedCol
? db
.prepare(
"SELECT * FROM nodes WHERE file = ? AND kind != 'file' AND exported = 1 ORDER BY line",
)
.all(target.file)
: [];
for (const s of targetExported) {
const fileLines = getFileLines(target.file);
reexportedSymbols.push({
...buildSymbolResult(s, fileLines),
originFile: target.file,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 reexportedSymbols bypasses pagination

paginateResult(base, 'results', ...) only paginates results. reexportedSymbols is always returned in full. A deep barrel file (e.g., src/index.ts re-exporting hundreds of modules) could return thousands of symbol entries in a single response, both for the CLI and especially for the MCP tool that passes through exportsData directly.

Consider either paginating reexportedSymbols as a secondary key, adding a per-call size cap, or at minimum documenting that this field is unbounded so callers know to handle it.

@carlos-alm
Copy link
Contributor Author

Scope Creep Detected

This PR is titled "feat: show re-exported symbols for barrel files" but carries two unrelated commits that are not on main:

  1. 40c8630feat: interface and trait implementation tracking — an entirely different feature (implementations/interfaces commands, BFS blast radius changes, class hierarchy edge builder refactor). This should be its own PR.
  2. ad3181bfeat: add TypeScript project setup for incremental migration — the TS setup was already merged to main as d799c10 (feat: TypeScript project setup for incremental migration #508), but this branch carries its own pre-merge copy, which is causing merge conflicts.

The barrel-exports feature itself (8fa4f2a) is the only commit scoped to this PR's stated purpose.

Recommendation: Create a new branch from origin/main, cherry-pick only 8fa4f2a (barrel exports), and open the interface tracking feature as a separate PR. This will also resolve the merge conflicts (which are caused by the stale TS setup commit colliding with the merged version on main).

Pausing review of Greptile's feedback until the PR is scoped correctly — some of the comments (e.g., the --no-implementations flag issue, BFS depth collapse for interfaces) are about the interface tracking feature, not barrel exports.

@carlos-alm
Copy link
Contributor Author

Closing — this PR had scope creep (interface tracking + TS setup + barrel exports in one PR).

@carlos-alm carlos-alm closed this Mar 19, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Mar 19, 2026
@carlos-alm carlos-alm deleted the feat/barrel-exports branch March 19, 2026 06:08
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