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
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,25 +68,26 @@ GITTENSORY_REVIEW_ENRICHMENT=false
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,sizeSmell,floatingPromise,commitLint
# conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting,commitLint
#
# Profile defaults:
# fast: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild
# testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker
# debugLeftover,sizeSmell,floatingPromise
# debugLeftover,sizeSmell,floatingPromise,deepNesting
# balanced (default): dependency,dependencyDiff,lockfileDrift,secret,license,installScript
# heavyDependency,hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight
# typosquat,commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication
# churnHotspot,blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch
# commitHygiene,pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology
# todoMarker,magicNumber,conflictMarker,debugLeftover,sizeSmell,floatingPromise,commitLint
# todoMarker,magicNumber,conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting
# commitLint
# deep: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,sizeSmell,floatingPromise,commitLint
# conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting,commitLint
# END GENERATED REES ANALYZERS

# Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep
Expand Down
23 changes: 23 additions & 0 deletions apps/gittensory-ui/src/lib/rees-analyzers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,29 @@ export const REES_ANALYZERS = [
"Precision-first: bare expression statements only — assignments and non-promise callees are skipped. Structural heuristic, not a type checker.",
},
},
{
name: "deepNesting",
title: "Deep control-flow nesting",
category: "quality",
cost: "local",
defaultEnabled: true,
profiles: ["fast", "balanced", "deep"],
requires: ["files"],
limits: {
maxFindings: 25,
maxDepth: 4,
maxLineChars: 2000,
},
docs: {
summary:
"Flags newly-added control-flow blocks whose nesting depth exceeds a threshold inside a contiguous run of added lines.",
looksAt: "Added lines in changed non-test source files within each hunk.",
reports: "File, line, measured control-flow depth, and configured threshold.",
network: "Pure local analyzer. No external network call.",
notes:
"Counts braces opened by if/for/while/switch/try/catch/else/do/with, arrow bodies, and function bodies — not object-literal braces. Resets across context lines.",
},
},
{
name: "commitLint",
title: "Conventional-commit subjects",
Expand Down
27 changes: 27 additions & 0 deletions review-enrichment/analyzer-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,33 @@
"notes": "Precision-first: bare expression statements only — assignments and non-promise callees are skipped. Structural heuristic, not a type checker."
}
},
{
"name": "deepNesting",
"title": "Deep control-flow nesting",
"category": "quality",
"cost": "local",
"defaultEnabled": true,
"profiles": [
"fast",
"balanced",
"deep"
],
"requires": [
"files"
],
"limits": {
"maxFindings": 25,
"maxDepth": 4,
"maxLineChars": 2000
},
"docs": {
"summary": "Flags newly-added control-flow blocks whose nesting depth exceeds a threshold inside a contiguous run of added lines.",
"looksAt": "Added lines in changed non-test source files within each hunk.",
"reports": "File, line, measured control-flow depth, and configured threshold.",
"network": "Pure local analyzer. No external network call.",
"notes": "Counts braces opened by if/for/while/switch/try/catch/else/do/with, arrow bodies, and function bodies — not object-literal braces. Resets across context lines."
}
},
{
"name": "commitLint",
"title": "Conventional-commit subjects",
Expand Down
138 changes: 138 additions & 0 deletions review-enrichment/src/analyzers/deep-nesting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Deep-nesting / arrow-anti-pattern analyzer (#2030). Flags newly-added control flow whose
// control-flow brace depth exceeds a threshold inside a contiguous run of added lines — a readability
// smell distinct from cyclomatic complexity. Object-literal braces are tracked but do not increase depth.
// Pure compute over added diff lines, no network.
import type { DeepNestingFinding, EnrichRequest } from "../types.js";
import { codeOnly } from "./secret-log.js";
import { isTestPath } from "./test-ratio.js";

export const DEFAULT_MAX_DEPTH = 4;
const MAX_FINDINGS = 25;
const MAX_LINE_CHARS = 2000;

type BraceKind = "control" | "other";

/** True when `{` opens control-flow scope (if/for/try/=>/function), not an object literal. Pure. */
export function isControlFlowOpenBrace(code: string, braceIdx: number): boolean {
const before = code.slice(0, braceIdx).trimEnd();
if (/(?:=>|\belse|\btry|\bfinally|\bdo)\s*$/i.test(before)) return true;
if (/\b(?:if|for|while|switch|catch|with)\s*\([^)]*\)\s*$/i.test(before)) return true;
if (/\b(?:async\s+)?function(?:\s+\w+)?\s*\([^)]*\)\s*$/i.test(before)) return true;
return false;
}

/** Advance control-flow brace depth over one code fragment and return ending depth + peak. Pure. */
export function advanceControlFlowDepth(
code: string,
depth: number,
): { depth: number; peak: number } {
let peak = depth;
const stack: BraceKind[] = [];

for (let i = 0; i < code.length; i++) {
const ch = code[i]!;
if (ch === "{") {
const kind: BraceKind = isControlFlowOpenBrace(code, i) ? "control" : "other";
stack.push(kind);
if (kind === "control") {
depth++;
peak = Math.max(peak, depth);
}
continue;
}
if (ch === "}") {
const kind = stack.pop();
if (kind === "control") {
depth = Math.max(0, depth - 1);
}
}
}

return { depth, peak };
}

type ScanLimits = {
maxDepth?: number;
maxFindings?: number;
signal?: AbortSignal;
};

type RunState = {
depth: number;
flagged: boolean;
};

function resetRun(state: RunState): void {
state.depth = 0;
state.flagged = false;
}

/** Scan one file patch's added lines for deep nesting, line-cited via hunk headers. Pure. */
export function scanPatchForDeepNesting(
path: string,
patch: string,
limits: ScanLimits = {},
): DeepNestingFinding[] {
const configured = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
const maxDepth = configured > 0 ? configured : DEFAULT_MAX_DEPTH;
const maxFindings = limits.maxFindings ?? MAX_FINDINGS;
if (maxFindings <= 0 || isTestPath(path)) return [];
const findings: DeepNestingFinding[] = [];
const run: RunState = { depth: 0, flagged: false };
let newLine = 0;
let inHunk = false;

const maybeFlag = (line: number, depth: number) => {
if (run.flagged || depth <= maxDepth) return;
findings.push({ file: path, line, depth, threshold: maxDepth });
run.flagged = true;
};

for (const line of patch.split("\n")) {
if (limits.signal?.aborted) throw new Error("analyzer_aborted");
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
if (hunk) {
newLine = Number(hunk[1]);
inHunk = true;
resetRun(run);
continue;
}
if (!inHunk) continue;
if (line.startsWith("+")) {
const body = line.slice(1);
if (body.length <= MAX_LINE_CHARS) {
const next = advanceControlFlowDepth(codeOnly(body), run.depth);
run.depth = next.depth;
maybeFlag(newLine, next.peak);
if (findings.length >= maxFindings) return findings;
}
newLine++;
} else {
resetRun(run);
if (!line.startsWith("-") && !line.startsWith("\\")) {
newLine++;
}
}
}
return findings;
}

/** Analyzer entrypoint: scan every changed non-test file's added lines for deep nesting. */
export async function scanDeepNesting(
req: EnrichRequest,
signal?: AbortSignal,
): Promise<DeepNestingFinding[]> {
const findings: DeepNestingFinding[] = [];
for (const file of req.files ?? []) {
if (signal?.aborted) throw new Error("analyzer_aborted");
if (!file.patch) continue;
for (const finding of scanPatchForDeepNesting(file.path, file.patch, {
maxFindings: MAX_FINDINGS - findings.length,
signal,
})) {
findings.push(finding);
if (findings.length >= MAX_FINDINGS) return findings;
}
}
return findings;
}
30 changes: 30 additions & 0 deletions review-enrichment/src/analyzers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { scanLooseRanges } from "./loose-range.js";
import { scanMagicNumbers } from "./magic-number.js";
import { scanConflictMarkers } from "./conflict-marker.js";
import { scanDebugLeftover } from "./debug-leftover.js";
import { scanDeepNesting } from "./deep-nesting.js";
import { scanFloatingPromise } from "./floating-promise.js";
import { scanSizeSmell } from "./size-smell.js";
import { scanCommitLint } from "./commit-lint.js";
Expand Down Expand Up @@ -1076,6 +1077,35 @@ export const ANALYZER_DESCRIPTORS = [
},
run: (req, { signal }) => scanFloatingPromise(req, signal),
}),
descriptor({
name: "deepNesting",
title: "Deep control-flow nesting",
category: "quality",
cost: "local",
defaultEnabled: true,
requires: ["files"],
limits: { maxFindings: 25, maxDepth: 4, maxLineChars: 2000 },
docs: {
summary:
"Flags newly-added control-flow blocks whose nesting depth exceeds a threshold inside a contiguous run of added lines.",
looksAt: "Added lines in changed non-test source files within each hunk.",
reports: "File, line, measured control-flow depth, and configured threshold.",
network: "Pure local analyzer. No external network call.",
notes:
"Counts braces opened by if/for/while/switch/try/catch/else/do/with, arrow bodies, and function bodies — not object-literal braces. Resets across context lines.",
},
render: (findings, helpers) => {
if (!findings.length) return [];
const lines = ["### Deep nesting (control-flow depth added by this PR)"];
for (const item of findings) {
lines.push(
`- ${helpers.safeCodeSpan(`${item.file}:${item.line}`)} — depth ${item.depth} (threshold ${item.threshold})`,
);
}
return lines;
},
run: (req, { signal }) => scanDeepNesting(req, signal),
}),
descriptor({
name: "commitLint",
title: "Conventional-commit subjects",
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export function renderBrief(
lines.push(...renderDescriptorSection("debugLeftover", findings.debugLeftover));
lines.push(...renderDescriptorSection("sizeSmell", findings.sizeSmell));
lines.push(...renderDescriptorSection("floatingPromise", findings.floatingPromise));
lines.push(...renderDescriptorSection("deepNesting", findings.deepNesting));
lines.push(...renderDescriptorSection("hardcodedUrl", findings.hardcodedUrl));
lines.push(...renderDescriptorSection("commitLint", findings.commitLint));

Expand Down
10 changes: 10 additions & 0 deletions review-enrichment/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,15 @@ export interface SizeSmellFinding {
name?: string;
}

/** Deep control-flow nesting newly added in the diff (#2030, part of #1499).
* Reports file, line, measured depth, and threshold — never source content. */
export interface DeepNestingFinding {
file: string;
line: number;
depth: number;
threshold: number;
}

/** A promise-shaped call added without await/return/void or a same-line .then/.catch chain (#2023, part of #1499).
* Reports location and a truncated callee name — never full expressions. */
export interface FloatingPromiseFinding {
Expand Down Expand Up @@ -560,6 +569,7 @@ export interface BriefFindings {
debugLeftover?: DebugLeftoverFinding[];
sizeSmell?: SizeSmellFinding[];
floatingPromise?: FloatingPromiseFinding[];
deepNesting?: DeepNestingFinding[];
hardcodedUrl?: HardcodedUrlFinding[];
commitLint?: CommitLintFinding[];
}
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/test/analyzer-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const EXPECTED_ANALYZERS = [
"debugLeftover",
"sizeSmell",
"floatingPromise",
"deepNesting",
"commitLint",
];

Expand Down
Loading
Loading