Skip to content
Closed
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
9 changes: 5 additions & 4 deletions src/presentation/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"). */
Expand Down
60 changes: 60 additions & 0 deletions src/presentation/impact-levels.ts
Original file line number Diff line number Diff line change
@@ -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<string, ImpactLevelRef[]>,
opts: RenderImpactLevelsOpts = {},
): void {
const { emptyMessage = ' No callers found.', limit = 20 } = opts;

if (Object.keys(levels).length === 0) {
if (emptyMessage !== null) console.log(emptyMessage);
return;
}
Comment thread
carlos-alm marked this conversation as resolved.

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`);
}
}
17 changes: 2 additions & 15 deletions src/presentation/queries-cli/impact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, SymbolRef[]>): 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<string, unknown>, 'results', opts)) return;
Expand All @@ -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`);
}
}
Expand Down
Loading