diff --git a/src/presentation/audit.ts b/src/presentation/audit.ts index 2670b769..add3a738 100644 --- a/src/presentation/audit.ts +++ b/src/presentation/audit.ts @@ -2,6 +2,11 @@ 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 { + renderCallRefsSection, + renderNoCallEdgesFallback, + renderRelatedTestsSection, +} from './call-ref-sections.js'; import { renderImpactLevels } from './impact-levels.js'; interface AuditOpts { @@ -17,9 +22,6 @@ interface AuditOpts { config?: CodegraphConfig; } -/** A caller/callee reference as rendered under the "Calls"/"Called by" sections. */ -type CallRef = AuditFunctionEntry['callees'][number]; - /** Render health metrics for a single audit function. */ function renderHealthMetrics(fn: AuditFunctionEntry): void { if (fn.health.cognitive == null) return; @@ -66,38 +68,21 @@ function renderThresholdBreaches(fn: AuditFunctionEntry): void { /** 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)`); - // 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. + // No "0 found" message here -- the count above already conveys it, matching the + // shared call-ref-sections helpers below, 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"). */ -function renderCallRefs(label: string, refs: CallRef[]): void { - if (refs.length === 0) return; - console.log(`\n ${label} (${refs.length}):`); - for (const c of refs) { - console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`); - } -} - -/** Render the related-test-file list for an audited function. */ -function renderRelatedTests(fn: AuditFunctionEntry): void { - if (fn.relatedTests.length === 0) return; - console.log(`\n Tests (${fn.relatedTests.length}):`); - for (const t of fn.relatedTests) { - console.log(` ${t.file}`); - } -} - /** Render a single audited function with all its sections. */ function renderAuditFunction(fn: AuditFunctionEntry): void { renderFunctionHeader(fn); renderHealthMetrics(fn); renderThresholdBreaches(fn); renderImpactSection(fn); - renderCallRefs('Calls', fn.callees); - renderCallRefs('Called by', fn.callers); - renderRelatedTests(fn); + renderCallRefsSection('Calls', fn.callees); + renderCallRefsSection('Called by', fn.callers); + renderRelatedTestsSection(fn.relatedTests); + renderNoCallEdgesFallback(fn.callees.length, fn.callers.length); console.log(); } diff --git a/src/presentation/call-ref-sections.ts b/src/presentation/call-ref-sections.ts new file mode 100644 index 00000000..328df243 --- /dev/null +++ b/src/presentation/call-ref-sections.ts @@ -0,0 +1,103 @@ +/** + * Shared renderers for the "Calls" / "Called by" / "Tests" sections rendered by + * both `codegraph audit` (presentation/audit.js) and `codegraph explain`/`codegraph query` + * (presentation/queries-cli/inspect.js — `audit --quick` is an alias for `explain`). + * + * Both commands render these sections from the same underlying 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 call reference rendered under the "Calls"/"Called by" sections. */ +export interface CallRefLike { + name: string; + kind: string; + file: string; + line: number; +} + +/** A single related-test-file reference rendered under the "Tests" section. */ +export interface RelatedTestRefLike { + file: string; +} + +export interface RenderCallRefsSectionOpts { + /** + * Indent prefix applied to every printed line. Used by `explain`'s recursive + * dependency rendering; top-level callers (e.g. `codegraph audit`) omit this + * and get the sensible default of no indent. + * Default: ''. + */ + indent?: string; +} + +/** + * Render a single labeled call-reference list — used for both the "Calls" and + * "Called by" sections (same shape, different label): + * + * {indent} Calls (2): + * {indent} f parse src/p.ts:1 + * {indent} m Parser.run src/p.ts:20 + * + * Prints nothing when `refs` is empty (see `renderNoCallEdgesFallback` for the + * combined "no edges at all" message). + */ +export function renderCallRefsSection( + label: string, + refs: CallRefLike[], + opts: RenderCallRefsSectionOpts = {}, +): void { + if (refs.length === 0) return; + const { indent = '' } = opts; + console.log(`\n${indent} ${label} (${refs.length}):`); + for (const c of refs) { + console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`); + } +} + +export interface RenderNoCallEdgesFallbackOpts { + /** Indent prefix applied to the printed line. Default: ''. */ + indent?: string; +} + +/** + * Print the "no call edges" fallback line when a function has neither callers + * nor callees. Call this once, after rendering both the "Calls" and "Called by" + * sections (which print nothing individually when empty) — it no-ops unless + * both counts are zero. + */ +export function renderNoCallEdgesFallback( + calleeCount: number, + callerCount: number, + opts: RenderNoCallEdgesFallbackOpts = {}, +): void { + if (calleeCount > 0 || callerCount > 0) return; + const { indent = '' } = opts; + console.log(`${indent} (no call edges found -- may be invoked dynamically or via re-exports)`); +} + +export interface RenderRelatedTestsSectionOpts { + /** Indent prefix applied to every printed line. Default: ''. */ + indent?: string; +} + +/** + * Render the related-test-file list for an audited/explained function, with a + * singular/plural "file"/"files" label: + * + * {indent} Tests (1 file): + * {indent} tests/parse.test.ts + */ +export function renderRelatedTestsSection( + tests: RelatedTestRefLike[], + opts: RenderRelatedTestsSectionOpts = {}, +): void { + if (tests.length === 0) return; + const { indent = '' } = opts; + const label = tests.length === 1 ? 'file' : 'files'; + console.log(`\n${indent} Tests (${tests.length} ${label}):`); + for (const t of tests) { + console.log(`${indent} ${t.file}`); + } +} diff --git a/src/presentation/queries-cli/inspect.ts b/src/presentation/queries-cli/inspect.ts index e900289d..39fe7c36 100644 --- a/src/presentation/queries-cli/inspect.ts +++ b/src/presentation/queries-cli/inspect.ts @@ -9,6 +9,11 @@ import { whereData, } from '../../domain/queries.js'; import { outputResult } from '../../infrastructure/result-formatter.js'; +import { + renderCallRefsSection, + renderNoCallEdgesFallback, + renderRelatedTestsSection, +} from '../call-ref-sections.js'; interface SymbolRef { kind: string; @@ -477,31 +482,10 @@ function renderExplainComplexity( } function renderExplainEdges(r: FunctionExplainResult, indent: string): void { - if (r.callees.length > 0) { - console.log(`\n${indent} Calls (${r.callees.length}):`); - for (const c of r.callees) { - console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`); - } - } - - if (r.callers.length > 0) { - console.log(`\n${indent} Called by (${r.callers.length}):`); - for (const c of r.callers) { - console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`); - } - } - - if (r.relatedTests.length > 0) { - const label = r.relatedTests.length === 1 ? 'file' : 'files'; - console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`); - for (const t of r.relatedTests) { - console.log(`${indent} ${t.file}`); - } - } - - if (r.callees.length === 0 && r.callers.length === 0) { - console.log(`${indent} (no call edges found -- may be invoked dynamically or via re-exports)`); - } + renderCallRefsSection('Calls', r.callees, { indent }); + renderCallRefsSection('Called by', r.callers, { indent }); + renderRelatedTestsSection(r.relatedTests, { indent }); + renderNoCallEdgesFallback(r.callees.length, r.callers.length, { indent }); } function renderFunctionExplain(r: FunctionExplainResult, indent = ''): void {