feat: show re-exported symbols for barrel files#513
Conversation
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 SummaryThis PR delivers three related features: (1) barrel/re-export file support in Key issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
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)
|
| // 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); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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].
| 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, |
There was a problem hiding this comment.
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".
| 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, | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
Scope Creep DetectedThis PR is titled "feat: show re-exported symbols for barrel files" but carries two unrelated commits that are not on
The barrel-exports feature itself ( Recommendation: Create a new branch from Pausing review of Greptile's feedback until the PR is scoped correctly — some of the comments (e.g., the |
|
Closing — this PR had scope creep (interface tracking + TS setup + barrel exports in one PR).
|
Summary
codegraph exportson 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)reexportsedges to gather exported symbols from target modules, displaying them grouped by origin file with consumer info--unusedfiltering, JSON/MCP output via newreexportedSymbolsfield, and renamed "Re-exported by" label for clarityTest plan
--unusedfilters them, non-barrel files have empty arrayreexportedSymbolswithoriginFileannotationreexportedSymbolsautomatically (passes throughexportsData)