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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Options:
--cwd <path> working directory
--no-color disable terminal color
-q, --quiet terminal only: suppress per-finding detail
--profile print per-rule timing to stderr without changing findings
```

Examples:
Expand Down
14 changes: 14 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ program.command("scan")
.option("--cwd <path>", "working directory", process.cwd())
.option("--no-color", "disable ANSI color in terminal output")
.option("-q, --quiet", "print only the summary line, suppress individual findings")
.option("--profile", "print per-rule timing without changing findings")
.action(async (target: string, rawOptions: Record<string, unknown>) => {
try {
const format = parseFormat(String(rawOptions.format ?? "terminal"));
Expand Down Expand Up @@ -94,6 +95,7 @@ program.command("scan")
respectGitignore: rawOptions.respectGitignore === true ? true : undefined,
changedFiles,
fileContents,
profile: rawOptions.profile === true,
});

if (rawOptions.writeBaseline && rawOptions.baseline) {
Expand All @@ -115,6 +117,10 @@ program.command("scan")
}
}

if (rawOptions.profile === true && result.summary.profile) {
process.stderr.write(formatProfileReport(result.summary.profile.ruleTimingsMs));
}

if (rawOptions.writeBaseline) {
const baselinePath = rawOptions.writeBaseline === true
? DEFAULT_BASELINE_FILENAME
Expand Down Expand Up @@ -433,3 +439,11 @@ async function resolveReportedIssues(
});
return applyBaseline(result, createBaseline(baseResult.issues));
}

function formatProfileReport(ruleTimingsMs: Record<string, number>): string {
const lines = ["DebtLens profile (per-rule ms):"];
for (const [ruleId, elapsedMs] of Object.entries(ruleTimingsMs).sort((left, right) => right[1] - left[1])) {
lines.push(` ${ruleId}: ${elapsedMs}ms`);
}
return `${lines.join("\n")}\n`;
}
1 change: 1 addition & 0 deletions src/config/mergeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ export function mergeConfig(target: string, fileConfig: DebtLensConfig, cliOptio
: undefined,
changedFiles: cliOptions.changedFiles,
fileContents: cliOptions.fileContents,
profile: cliOptions.profile,
};
}
6 changes: 6 additions & 0 deletions src/core/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
let issues: DebtIssue[] = [];
const warnings: string[] = [];
let filteredByMinSeverity = 0;
const ruleTimingsMs: Record<string, number> = {};

for (const detector of detectors) {
const detectorStartedAt = options.profile ? Date.now() : 0;
const detectorIssues = await detector.detect({
project,
files,
Expand All @@ -59,6 +61,9 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
filteredByMinSeverity += 1;
}
}
if (options.profile) {
ruleTimingsMs[detector.id] = Date.now() - detectorStartedAt;
}
}

issues.sort((a, b) => {
Expand Down Expand Up @@ -98,6 +103,7 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
elapsedMs: Date.now() - startedAt,
...(warnings.length ? { warnings } : {}),
...(Object.keys(filterStats).length > 0 ? { filterStats } : {}),
...(options.profile ? { profile: { ruleTimingsMs } } : {}),
};

return {
Expand Down
8 changes: 8 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export interface ScanOptions {
todoCommentReplaceDefaults?: boolean;
todoCommentDisableDefaults?: string[];
todoCommentMarkers?: Array<{ regex: RegExp; severity: Severity; label: string }>;
/** When true, collect per-rule timing in `summary.profile`. */
profile?: boolean;
}

export interface CliOptions {
Expand All @@ -115,6 +117,7 @@ export interface CliOptions {
noColor?: boolean;
changedFiles?: string[];
fileContents?: Record<string, string>;
profile?: boolean;
}

export interface DetectorContext {
Expand All @@ -140,6 +143,10 @@ export interface ScanFilterStats {
suppressedByInline?: number;
}

export interface ScanProfile {
ruleTimingsMs: Record<string, number>;
}

export interface ScanSummary {
totalIssues: number;
bySeverity: Record<Severity, number>;
Expand All @@ -149,6 +156,7 @@ export interface ScanSummary {
elapsedMs: number;
warnings?: string[];
filterStats?: ScanFilterStats;
profile?: ScanProfile;
}

export interface ScanResult {
Expand Down
13 changes: 13 additions & 0 deletions tests/cli/scan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ describe("debtlens scan diff-base", () => {
});
});

describe("debtlens scan profile", () => {
it("prints per-rule timing to stderr without changing findings", () => {
const result = runScan(["examples/react", "--rules", "todo-comment", "--profile", "--format", "json"]);
const parsed = JSON.parse(result.stdout);

assert.equal(result.status, 0);
assert.match(result.stderr, /DebtLens profile \(per-rule ms\):/);
assert.match(result.stderr, /todo-comment: \d+ms/);
assert.ok(parsed.summary.profile?.ruleTimingsMs["todo-comment"] !== undefined);
assert.equal(parsed.summary.totalIssues, parsed.issues.length);
});
});

describe("debtlens scan git modes", () => {
it("rejects --changed and --staged together", () => {
const result = runScan(["examples/react", "--changed", "--staged"]);
Expand Down