diff --git a/USAGE.md b/USAGE.md index 1c68a95..c94f0fe 100644 --- a/USAGE.md +++ b/USAGE.md @@ -99,7 +99,9 @@ fall back to a raw byte-compare, which does require CI to use the same - **"codeshot: 'codegraph' not found on PATH"** — Install CodeGraph and make sure it's on your PATH, then try again. - **"codeshot: 'dot' not found on PATH"** — Install Graphviz (`brew install graphviz` on Mac, `apt install graphviz` on Ubuntu/WSL), then try again. -- **The command runs but the diagram is empty or missing edges** — The repo probably hasn't been indexed yet, or the index is stale. Run `codegraph init` (or re-run indexing) in the target repo first. +- **The command runs but the diagram is empty or missing edges** — The repo probably hasn't been indexed yet, or the index is stale. Run `codegraph init` (or re-run indexing) in the target repo first. Codeshot no longer draws a blank picture silently: it warns on stderr in the two cases below. +- **"codeshot: '...' has no callers or callees in codegraph's index"** — The symbol exists but nothing calls it and it calls nothing, so the diagram is just that one box. It may be genuinely unused (dead code) or a top-level entry point — or codegraph's index is incomplete for its file (see the sparse-diagram note below). The image is still written; the warning just explains why it's a lone box. +- **"codeshot: --architecture found no cross-file call edges — the diagram is blank"** — codegraph reported no resolved calls *between files* in this repo, so there's nothing for the file-level graph to draw. Expected for a small or single-file repo; otherwise the index is likely missing or stale — run `codegraph init `, then `codegraph status` to confirm it built. A blank image is still written so the `--out` path exists. - **"codeshot: symbol '...' not found in codegraph's index"** — Double-check the exact spelling/casing of the symbol name, and confirm `--path` points at the repo that actually contains it. - **The diagram is real but looks sparse — CodeGraph's index has known gaps.** Tested against several real codebases: same-named methods on unrelated classes are sometimes merged or one silently dropped, aliased imports (`import x as y`) can return zero callers for a genuinely well-used function, and dependency-injection patterns (e.g. FastAPI's `Depends()`) often don't resolve to real caller functions at all. Codeshot only draws what CodeGraph reports — if a diagram looks thinner than you expect for a symbol you know is heavily used, that's more likely a CodeGraph indexing gap than a Codeshot bug. A `dotted gray "file"` edge (see above) is one visible symptom of this; a *missing* edge is the invisible version and harder to catch — spot-check against the real source if it matters. - **`--out diagram.svg` produced a PNG (or vice versa)** — Codeshot only ever writes what `--format` says; it never infers format from `--out`'s extension. If you see this, you forgot `--format svg` (or whichever format matches the extension you wanted) — Codeshot now warns about this mismatch on stderr before it happens, so check for that warning first. diff --git a/render/callgraph.js b/render/callgraph.js index 0e08d8c..d75cfbc 100755 --- a/render/callgraph.js +++ b/render/callgraph.js @@ -158,6 +158,18 @@ function truncationWarning(kind, results, limit) { return `codeshot: showing ${results.length} ${kind} — codegraph's --limit (${limit}) may have cut off more; rerun with --limit to see additional ${kind}.`; } +// A symbol with no callers AND no callees renders as a lone box — a valid but +// uninformative picture. Warn so the empty result reads as a finding (the symbol +// is unused, an entry point, or codegraph's index is incomplete for its file) +// rather than looking like a tool glitch — the same "warn, don't hand back a +// silent blank" stance as indexHealthWarning/duplicateNameWarning. +function emptyGraphWarning(symbol, callers, callees) { + const nCallers = Array.isArray(callers) ? callers.length : 0; + const nCallees = Array.isArray(callees) ? callees.length : 0; + if (nCallers > 0 || nCallees > 0) return null; + return `codeshot: '${symbol}' has no callers or callees in codegraph's index — the diagram is just the symbol itself. It may be unused (dead code) or an entry point, or codegraph's index may be incomplete for its file.`; +} + function dedupeNodes(nodes) { const seen = new Set(); const result = []; @@ -414,6 +426,16 @@ function symbolBudgetWarning(truncated, budget) { return `codeshot: --architecture stopped enumerating after ${budget} symbols (--max-symbols) — the graph is incomplete; rerun with a larger --max-symbols to cover the rest of the repo.`; } +// With zero cross-file edges, buildArchitectureDot emits a graph with no nodes — +// a blank image. Node count alone can't tell that apart from a legitimately +// edge-free repo, so warn with the likely cause (small/single-file repo, or an +// index that was never built) instead of writing a silent blank picture. Pure: +// takes the aggregated file edges, returns the warning string or null. +function emptyArchitectureWarning(fileEdges) { + if (Array.isArray(fileEdges) && fileEdges.length > 0) return null; + return `codeshot: --architecture found no cross-file call edges — the diagram is blank. codegraph's index has no resolved calls between files in this repo (it may be small or single-file, or the index may be missing — run 'codegraph init ' to build it, then 'codegraph status' to confirm).`; +} + // codegraph's callers/callees take a bare name with no --file disambiguation // (unlike `codegraph node -f`), so two same-named symbols in different files // are genuinely ambiguous to a `codegraph callees ` probe — a real risk @@ -538,6 +560,9 @@ async function runArchitectureMode(repoPath, { limit, maxSymbols, maxRender }) { const symbolEdges = await probeFileEdges(symbols, repoPath, limit); const fileEdges = aggregateFileEdges(symbolEdges); + const emptyWarning = emptyArchitectureWarning(fileEdges); + if (emptyWarning) console.error(emptyWarning); + const totalFiles = new Set(fileEdges.flatMap(e => [e.from, e.to])).size; const note = renderTruncationNote('files', totalFiles, maxRender); if (note) console.error(note); @@ -894,6 +919,9 @@ async function main() { if (warning) console.error(warning); } + const emptyWarning = emptyGraphWarning(resolvedSymbol, callers, callees); + if (emptyWarning) console.error(emptyWarning); + let transitiveEdges = []; if (depth > 1) { const discovered = new Set(dedupeNodes([...(callers || []), ...(callees || [])]).map(n => `${n.name} ${n.filePath}`)); @@ -945,4 +973,5 @@ module.exports = { topFilesByWeight, buildArchitectureDot, architectureOutputBaseName, applyEmbed, embedMarkers, embedRelLink, parseUnresolvedRefs, svgStructure, decodeXmlEntities, + emptyGraphWarning, emptyArchitectureWarning, }; diff --git a/test/run.js b/test/run.js index 23b3b7d..f7b0386 100644 --- a/test/run.js +++ b/test/run.js @@ -9,6 +9,7 @@ const { topFilesByWeight, buildArchitectureDot, architectureOutputBaseName, applyEmbed, embedMarkers, embedRelLink, parseUnresolvedRefs, svgStructure, decodeXmlEntities, + emptyGraphWarning, emptyArchitectureWarning, } = require('../render/callgraph.js'); let passed = 0; @@ -286,6 +287,20 @@ test('truncationWarning is null when under the limit', () => { assert.strictEqual(truncationWarning('callers', results, 20), null); }); +test('emptyGraphWarning fires only when a symbol has neither callers nor callees', () => { + assert.match(emptyGraphWarning('Foo', [], []), /'Foo' has no callers or callees/); + assert.strictEqual(emptyGraphWarning('Foo', [{ name: 'a' }], []), null); + assert.strictEqual(emptyGraphWarning('Foo', [], [{ name: 'b' }]), null); + // undefined arrays (codegraph returned nothing) count as empty, not a crash + assert.match(emptyGraphWarning('Foo', undefined, undefined), /has no callers or callees/); +}); + +test('emptyArchitectureWarning fires only when there are zero cross-file edges', () => { + assert.match(emptyArchitectureWarning([]), /no cross-file call edges — the diagram is blank/); + assert.strictEqual(emptyArchitectureWarning([{ from: 'a', to: 'b', weight: 1 }]), null); + assert.match(emptyArchitectureWarning(undefined), /no cross-file call edges/); +}); + test('--limit rejects non-positive-integer values before reaching codegraph', () => { const { execFileSync } = require('child_process'); for (const bad of ['abc', '0', '-5', '3.5', 'NaN']) {