diff --git a/src/presentation/audit.ts b/src/presentation/audit.ts index 9a350a6e..2670b769 100644 --- a/src/presentation/audit.ts +++ b/src/presentation/audit.ts @@ -2,6 +2,7 @@ import { kindIcon } from '../domain/queries.js'; import { auditData } from '../features/audit.js'; import { outputResult } from '../infrastructure/result-formatter.js'; import type { AuditFunctionEntry, AuditResult, CodegraphConfig } from '../types.js'; +import { renderImpactLevels } from './impact-levels.js'; interface AuditOpts { json?: boolean; @@ -62,12 +63,12 @@ function renderThresholdBreaches(fn: AuditFunctionEntry): void { } } -/** Render the transitive-dependent impact summary, one line per BFS level. */ +/** Render the transitive-dependent impact summary, one block per BFS level. */ function renderImpactSection(fn: AuditFunctionEntry): void { console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`); - for (const [level, nodes] of Object.entries(fn.impact.levels)) { - console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`); - } + // No "0 found" message here -- the count above already conveys it, matching this + // file's other sections (e.g. renderCallRefs), which print nothing when empty. + renderImpactLevels(fn.impact.levels, { emptyMessage: null }); } /** Render a labeled list of call references (used for both "Calls" and "Called by"). */ diff --git a/src/presentation/impact-levels.ts b/src/presentation/impact-levels.ts new file mode 100644 index 00000000..6ed044f2 --- /dev/null +++ b/src/presentation/impact-levels.ts @@ -0,0 +1,60 @@ +/** + * Shared renderer for transitive-caller "impact levels" — the `{ level -> entries[] }` + * map produced by `bfsTransitiveCallers` (see domain/analysis/fn-impact.js). + * + * Both `codegraph audit` (presentation/audit.js) and `codegraph fn-impact` + * (presentation/queries-cli/impact.js) render this same shape and must stay + * visually identical — this module is the single source of truth for that format. + */ + +import { kindIcon } from '../domain/queries.js'; + +/** A single transitive-caller entry within one impact-level bucket. */ +export interface ImpactLevelRef { + name: string; + kind: string; + file: string; + line: number; +} + +export interface RenderImpactLevelsOpts { + /** + * Message printed when there are no levels at all. Pass `null` to print nothing + * (e.g. when the caller already reports a "0" count immediately above). + * Default: " No callers found." + */ + emptyMessage?: string | null; + /** Max entries shown per level before truncating with "... and N more". Default: 20. */ + limit?: number; +} + +/** + * Render a transitive-caller level map as indented, icon-prefixed bullet lists — + * one block per BFS level, with deeper levels indented further: + * + * -- Level 1 (2 functions): + * ^ f caller src/a.ts:10 + * ^ m Caller.method src/b.ts:20 + * ---- Level 2 (1 functions): + * ^ f transitiveCaller src/c.ts:5 + */ +export function renderImpactLevels( + levels: Record, + opts: RenderImpactLevelsOpts = {}, +): void { + const { emptyMessage = ' No callers found.', limit = 20 } = opts; + + if (Object.keys(levels).length === 0) { + if (emptyMessage !== null) console.log(emptyMessage); + return; + } + + for (const [level, fns] of Object.entries(levels).sort((a, b) => Number(a[0]) - Number(b[0]))) { + const l = parseInt(level, 10); + console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`); + for (const f of fns.slice(0, limit)) { + console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`); + } + if (fns.length > limit) console.log(` ... and ${fns.length - limit} more`); + } +} diff --git a/src/presentation/queries-cli/impact.ts b/src/presentation/queries-cli/impact.ts index 94b1fa64..16cc0fe7 100644 --- a/src/presentation/queries-cli/impact.ts +++ b/src/presentation/queries-cli/impact.ts @@ -8,6 +8,7 @@ import { kindIcon, } from '../../domain/queries.js'; import { outputResult } from '../../infrastructure/result-formatter.js'; +import { renderImpactLevels } from '../impact-levels.js'; interface SymbolRef { kind: string; @@ -228,20 +229,6 @@ export function impactAnalysis(file: string, customDbPath: string, opts: OutputO console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`); } -function printFnImpactLevels(levels: Record): void { - if (Object.keys(levels).length === 0) { - console.log(` No callers found.`); - return; - } - for (const [level, fns] of Object.entries(levels).sort((a, b) => Number(a[0]) - Number(b[0]))) { - const l = parseInt(level, 10); - console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`); - for (const f of fns.slice(0, 20)) - console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`); - if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`); - } -} - export function fnImpact(name: string, customDbPath: string, opts: OutputOpts = {}): void { const data = fnImpactData(name, customDbPath, opts) as unknown as FnImpactData; if (outputResult(data as unknown as Record, 'results', opts)) return; @@ -253,7 +240,7 @@ export function fnImpact(name: string, customDbPath: string, opts: OutputOpts = for (const r of data.results) { console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`); - printFnImpactLevels(r.levels); + renderImpactLevels(r.levels); console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`); } }