Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>`, 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.
Expand Down
29 changes: 29 additions & 0 deletions render/callgraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n> 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 = [];
Expand Down Expand Up @@ -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 <path>' 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 <name>` probe — a real risk
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}`));
Expand Down Expand Up @@ -945,4 +973,5 @@ module.exports = {
topFilesByWeight, buildArchitectureDot, architectureOutputBaseName,
applyEmbed, embedMarkers, embedRelLink, parseUnresolvedRefs,
svgStructure, decodeXmlEntities,
emptyGraphWarning, emptyArchitectureWarning,
};
15 changes: 15 additions & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
topFilesByWeight, buildArchitectureDot, architectureOutputBaseName,
applyEmbed, embedMarkers, embedRelLink, parseUnresolvedRefs,
svgStructure, decodeXmlEntities,
emptyGraphWarning, emptyArchitectureWarning,
} = require('../render/callgraph.js');

let passed = 0;
Expand Down Expand Up @@ -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']) {
Expand Down
Loading