From aad1d2c85cdad3fcd8dd2d4104c718e6fcc735e9 Mon Sep 17 00:00:00 2001 From: jaso0n0818 Date: Sun, 5 Jul 2026 04:44:12 +0000 Subject: [PATCH] feat(enrichment): add error-swallow catch analyzer Closes #2014 --- .env.example | 7 +- apps/gittensory-ui/src/lib/rees-analyzers.ts | 23 +++ review-enrichment/analyzer-metadata.json | 26 ++++ .../src/analyzers/error-swallow.ts | 140 ++++++++++++++++++ review-enrichment/src/analyzers/registry.ts | 40 +++++ review-enrichment/src/render.ts | 1 + review-enrichment/src/types.ts | 9 ++ .../test/analyzer-registry.test.ts | 1 + review-enrichment/test/error-swallow.test.ts | 114 ++++++++++++++ src/review/enrichment-analyzer-names.ts | 1 + test/unit/enrichment-wire.test.ts | 3 +- 11 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 review-enrichment/src/analyzers/error-swallow.ts create mode 100644 review-enrichment/test/error-swallow.test.ts diff --git a/.env.example b/.env.example index 24836986fa..1f5a7515a1 100644 --- a/.env.example +++ b/.env.example @@ -67,23 +67,24 @@ GITTENSORY_REVIEW_ENRICHMENT=false # 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,commitLint +# looseRange,terminology,todoMarker,magicNumber,conflictMarker,commitLint,errorSwallow # # Profile defaults: # fast: dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol # redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild,testRatio,migrationSafety -# looseRange,terminology,todoMarker,magicNumber,conflictMarker +# looseRange,terminology,todoMarker,magicNumber,conflictMarker,errorSwallow # balanced (default): dependency,lockfileDrift,secret,license,installScript,heavyDependency # 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,commitLint +# conflictMarker,commitLint,errorSwallow # deep: dependency,lockfileDrift,secret,license,installScript,heavyDependency,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,commitLint +# errorSwallow # END GENERATED REES ANALYZERS # Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep diff --git a/apps/gittensory-ui/src/lib/rees-analyzers.ts b/apps/gittensory-ui/src/lib/rees-analyzers.ts index e2f514346d..2de759007a 100644 --- a/apps/gittensory-ui/src/lib/rees-analyzers.ts +++ b/apps/gittensory-ui/src/lib/rees-analyzers.ts @@ -909,6 +909,29 @@ export const REES_ANALYZERS = [ "Structured-fields-only: reads commit.message subjects, linted independently, never cross-line state. Fail-safe on missing token/fetch error.", }, }, + { + name: "errorSwallow", + title: "Swallowed errors", + category: "quality", + cost: "local", + defaultEnabled: true, + profiles: ["fast", "balanced", "deep"], + requires: ["files"], + limits: { + maxFindings: 25, + maxLineChars: 2000, + }, + docs: { + summary: + "Flags newly-added catch blocks that swallow the error: an empty body, a body that just returns null/undefined, or one that neither rethrows, logs, nor references the caught binding.", + looksAt: + "Added lines in JS/TS source (a single-line `catch` block) and Python (`except …: pass`).", + reports: "File, line, and the swallow kind — never line content.", + network: "Pure local analyzer. No external network call.", + notes: + "Single-line by design: a catch body spread across lines is not tracked (the safe, false-negative direction). String literals and comments are blanked first.", + }, + }, ] as const satisfies readonly ReesAnalyzerDoc[]; export const REES_ANALYZER_NAMES = REES_ANALYZERS.map((analyzer) => analyzer.name); diff --git a/review-enrichment/analyzer-metadata.json b/review-enrichment/analyzer-metadata.json index df8b580a6c..a1dc631562 100644 --- a/review-enrichment/analyzer-metadata.json +++ b/review-enrichment/analyzer-metadata.json @@ -1023,6 +1023,32 @@ "network": "Calls the GitHub PR-commits API once, bounded to one page.", "notes": "Structured-fields-only: reads commit.message subjects, linted independently, never cross-line state. Fail-safe on missing token/fetch error." } + }, + { + "name": "errorSwallow", + "title": "Swallowed errors", + "category": "quality", + "cost": "local", + "defaultEnabled": true, + "profiles": [ + "fast", + "balanced", + "deep" + ], + "requires": [ + "files" + ], + "limits": { + "maxFindings": 25, + "maxLineChars": 2000 + }, + "docs": { + "summary": "Flags newly-added catch blocks that swallow the error: an empty body, a body that just returns null/undefined, or one that neither rethrows, logs, nor references the caught binding.", + "looksAt": "Added lines in JS/TS source (a single-line `catch` block) and Python (`except …: pass`).", + "reports": "File, line, and the swallow kind — never line content.", + "network": "Pure local analyzer. No external network call.", + "notes": "Single-line by design: a catch body spread across lines is not tracked (the safe, false-negative direction). String literals and comments are blanked first." + } } ] } diff --git a/review-enrichment/src/analyzers/error-swallow.ts b/review-enrichment/src/analyzers/error-swallow.ts new file mode 100644 index 0000000000..4b588e6b46 --- /dev/null +++ b/review-enrichment/src/analyzers/error-swallow.ts @@ -0,0 +1,140 @@ +// Error-swallow analyzer (#2014). Flags newly-added catch blocks that swallow the error — an empty body, a body +// that just returns null/undefined, or a body that neither rethrows, logs, nor references the caught binding — a +// top source of silent failures. Pure compute over added diff lines, no network. Scoped to JS/TS (a `catch` +// block) and Python (a bare `except … : pass`). Detection is SINGLE-LINE by design (the catch/except and its +// body on one added line, the compact form the pattern targets): a body spread across multiple lines is not +// tracked — missing it is the safe (false-negative) direction, and there is no cross-line state. String literals +// and comments are blanked first (a `catch {}` in a string, and a comment-only body which is itself a swallow). +// Follows the actions-pin.ts added-line hunk-walk pattern. Line-cited via hunk headers. +import type { EnrichRequest, ErrorSwallowFinding } from "../types.js"; +import { codeOnly } from "./secret-log.js"; + +const MAX_FINDINGS = 25; +const MAX_LINE_CHARS = 2000; + +const JS_EXTS = new Set(["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"]); +const PY_EXTS = new Set(["py"]); + +// A JS/TS `catch` with an OPTIONAL binding and a single-line body captured up to the first `}`. +// group 1 = the binding name (when `catch (e)`); group 2 = the body between the braces. +const JS_CATCH_RE = /\bcatch\s*(?:\(\s*([A-Za-z_$][\w$]*)[^)]*\))?\s*\{([^}]*)\}/; +// A Python bare `except …: pass` — the canonical error-swallow. +const PY_EXCEPT_PASS_RE = /^\s*except\b[^:]*:\s*pass\s*$/; + +// Body signals that mean the error is HANDLED, not swallowed: a rethrow, or a logging call. +const RETHROW_RE = /\bthrow\b/; +const LOG_RE = /\b(?:console|logger|log)\s*\.\s*\w+\s*\(|\blog\s*\(|\bwarn\s*\(|\berror\s*\(/i; +// A body that is EXACTLY `return null`/`return undefined` — a swallow via a null return. Anchored to the whole +// (already-trimmed) body so a `return null` that is only ONE branch of a body that also rethrows +// (`if (x) return null; throw e;`) is NOT mistaken for a pure null-return swallow. +const RETURN_NULL_RE = /^return\s+(?:null|undefined)\s*;?$/; + +function extOf(path: string): string | null { + const base = path.split("/").pop() ?? path; + const dot = base.lastIndexOf("."); + return dot > 0 ? base.slice(dot + 1).toLowerCase() : null; +} + +/** Classify a single added source line for an error-swallow, given its file's language. Returns the kind, or + * null. Strings/comments are blanked first so a `catch {}` in a string is not matched. Pure. */ +export function detectErrorSwallow( + line: string, + lang: "js" | "py", +): ErrorSwallowFinding["kind"] | null { + if (lang === "py") { + return PY_EXCEPT_PASS_RE.test(line) ? "empty-catch" : null; + } + const code = codeOnly(line).replace(/\/\*.*?\*\//g, " ").replace(/\/\/.*$/, ""); + const match = JS_CATCH_RE.exec(code); + if (!match) return null; + const binding = match[1]; + const body = (match[2] ?? "").trim(); + if (!body) return "empty-catch"; + if (RETURN_NULL_RE.test(body)) return "return-null"; + // A body that neither rethrows, logs, nor references the caught binding swallows the error. Only meaningful + // when there IS a binding to ignore — a bindingless `catch { doStuff() }` is not an "unused binding". The + // binding is a JS identifier that may contain `$`, so it is regex-escaped before use, and referenced-ness is + // tested with identifier-char boundaries (not `\b`, which mishandles a leading `$`/`_`) so `report($err)` + // correctly counts as a reference to `$err`. + if (binding && !RETHROW_RE.test(body) && !LOG_RE.test(body)) { + const escaped = binding.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const referencesBinding = new RegExp(`(?= maxFindings) return findings; + } + } + newLine++; + } else if (!line.startsWith("-") && !line.startsWith("\\")) { + // A `\ No newline at end of file` marker is not a new-file line — do not advance the cursor + // (same class as the actions-pin / iac-misconfig line-number fix). + newLine++; + } + } + return findings; +} + +/** Analyzer entrypoint: scan every changed JS/TS/Python file's added lines for error-swallow catch blocks. */ +export async function scanErrorSwallow( + req: EnrichRequest, + signal?: AbortSignal, +): Promise { + const findings: ErrorSwallowFinding[] = []; + for (const file of req.files ?? []) { + if (signal?.aborted) throw new Error("analyzer_aborted"); + if (!file.patch) continue; + for (const finding of scanPatchForErrorSwallow(file.path, file.patch, { + maxFindings: MAX_FINDINGS - findings.length, + signal, + })) { + findings.push(finding); + if (findings.length >= MAX_FINDINGS) return findings; + } + } + return findings; +} diff --git a/review-enrichment/src/analyzers/registry.ts b/review-enrichment/src/analyzers/registry.ts index 76da147c3f..ee020c9a2d 100644 --- a/review-enrichment/src/analyzers/registry.ts +++ b/review-enrichment/src/analyzers/registry.ts @@ -30,6 +30,7 @@ import { scanLooseRanges } from "./loose-range.js"; import { scanMagicNumbers } from "./magic-number.js"; import { scanConflictMarkers } from "./conflict-marker.js"; import { scanCommitLint } from "./commit-lint.js"; +import { scanErrorSwallow } from "./error-swallow.js"; import { scanTerminology } from "./terminology.js"; import { scanTodoMarker } from "./todo-marker.js"; import { scanTyposquat } from "./typosquat.js"; @@ -959,6 +960,45 @@ export const ANALYZER_DESCRIPTORS = [ run: (req, { signal, analysis, diagnostics }) => scanCommitLint(req, fetch, { signal, analysis, diagnostics }), }), + descriptor({ + name: "errorSwallow", + title: "Swallowed errors", + category: "quality", + cost: "local", + defaultEnabled: true, + requires: ["files"], + limits: { maxFindings: 25, maxLineChars: 2000 }, + docs: { + summary: + "Flags newly-added catch blocks that swallow the error: an empty body, a body that just returns null/undefined, or one that neither rethrows, logs, nor references the caught binding.", + looksAt: "Added lines in JS/TS source (a single-line `catch` block) and Python (`except …: pass`).", + reports: "File, line, and the swallow kind — never line content.", + network: "Pure local analyzer. No external network call.", + notes: + "Single-line by design: a catch body spread across lines is not tracked (the safe, false-negative direction). String literals and comments are blanked first.", + }, + render: (findings, helpers) => { + if (!findings.length) return []; + const explain = (kind: (typeof findings)[number]["kind"]): string => { + switch (kind) { + case "empty-catch": + return "an empty catch body silently discards the error"; + case "return-null": + return "the catch returns null/undefined, swallowing the error"; + case "unused-binding": + return "the catch never rethrows, logs, or references the caught error"; + } + }; + const lines = ["### Swallowed errors (silent failure risk)"]; + for (const item of findings) { + lines.push( + `- ${helpers.safeCodeSpan(`${item.file}:${item.line}`)} — ${explain(item.kind)}`, + ); + } + return lines; + }, + run: (req, { signal }) => scanErrorSwallow(req, signal), + }), ] as const satisfies readonly AnyAnalyzerDescriptor[]; export const ANALYZER_NAMES = ANALYZER_DESCRIPTORS.map( diff --git a/review-enrichment/src/render.ts b/review-enrichment/src/render.ts index 38ed8483aa..5cdb96620d 100644 --- a/review-enrichment/src/render.ts +++ b/review-enrichment/src/render.ts @@ -461,6 +461,7 @@ export function renderBrief( lines.push(...renderDescriptorSection("magicNumber", findings.magicNumber)); lines.push(...renderDescriptorSection("conflictMarker", findings.conflictMarker)); lines.push(...renderDescriptorSection("commitLint", findings.commitLint)); + lines.push(...renderDescriptorSection("errorSwallow", findings.errorSwallow)); if (!lines.length) return { promptSection: "", systemSuffix: "" }; diff --git a/review-enrichment/src/types.ts b/review-enrichment/src/types.ts index aa2b7da70d..b15fc4ba1e 100644 --- a/review-enrichment/src/types.ts +++ b/review-enrichment/src/types.ts @@ -463,6 +463,14 @@ export interface CommitLintFinding { reason: "bad-type" | "missing-colon" | "too-long" | "empty"; } +/** A newly-added catch block that swallows the error — an empty body, a null/undefined return, or a body that + * never rethrows, logs, or references the caught binding (#2014, part of #1499). Reports location + kind. */ +export interface ErrorSwallowFinding { + file: string; + line: number; + kind: "empty-catch" | "unused-binding" | "return-null"; +} + /** Structured analyzer output. Each analyzer fills its own key; more land as analyzers ship (#1477/#1478). */ export interface BriefFindings { dependency?: DependencyFinding[]; @@ -501,6 +509,7 @@ export interface BriefFindings { magicNumber?: MagicNumberFinding[]; conflictMarker?: ConflictMarkerFinding[]; commitLint?: CommitLintFinding[]; + errorSwallow?: ErrorSwallowFinding[]; } /** A JSDoc/TSDoc block whose `@param` tags name parameters the adjacent function no longer declares — a diff --git a/review-enrichment/test/analyzer-registry.test.ts b/review-enrichment/test/analyzer-registry.test.ts index edd5354d4d..f5b64edc09 100644 --- a/review-enrichment/test/analyzer-registry.test.ts +++ b/review-enrichment/test/analyzer-registry.test.ts @@ -46,6 +46,7 @@ const EXPECTED_ANALYZERS = [ "magicNumber", "conflictMarker", "commitLint", + "errorSwallow", ]; test("analyzer descriptors cover the runtime registry in stable order", () => { diff --git a/review-enrichment/test/error-swallow.test.ts b/review-enrichment/test/error-swallow.test.ts new file mode 100644 index 0000000000..e7e3639cea --- /dev/null +++ b/review-enrichment/test/error-swallow.test.ts @@ -0,0 +1,114 @@ +// Units for the error-swallow analyzer (#2014). Own file (not enrichment.test.ts) so concurrent analyzer PRs +// don't collide. No network — pure, single-line detection. Runs against the compiled dist/. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + detectErrorSwallow, + scanPatchForErrorSwallow, + scanErrorSwallow, +} from "../dist/analyzers/error-swallow.js"; + +const patchOf = (lines) => `@@ -1,0 +1,${lines.length} @@\n${lines.map((l) => `+${l}`).join("\n")}`; + +test("detectErrorSwallow: an empty catch body is flagged (with and without a binding)", () => { + assert.equal(detectErrorSwallow("try { risky(); } catch (e) {}", "js"), "empty-catch"); + assert.equal(detectErrorSwallow("} catch {}", "js"), "empty-catch"); + assert.equal(detectErrorSwallow("} catch (err) { }", "js"), "empty-catch"); + // A comment-only body is effectively empty — it too swallows the error. + assert.equal(detectErrorSwallow("} catch (e) { /* ignore */ }", "js"), "empty-catch"); +}); + +test("detectErrorSwallow: a catch that just returns null/undefined is return-null", () => { + assert.equal(detectErrorSwallow("} catch (e) { return null; }", "js"), "return-null"); + assert.equal(detectErrorSwallow("} catch (e) { return undefined }", "js"), "return-null"); +}); + +test("detectErrorSwallow: a catch whose body ignores the binding (no rethrow/log/reference) is unused-binding", () => { + assert.equal(detectErrorSwallow("} catch (e) { cleanup(); }", "js"), "unused-binding"); + assert.equal(detectErrorSwallow("} catch (err) { doStuff(); return false; }", "js"), "unused-binding"); +}); + +test("detectErrorSwallow: a catch that rethrows, logs, or references the binding is NOT flagged", () => { + assert.equal(detectErrorSwallow("} catch (e) { throw e; }", "js"), null); + assert.equal(detectErrorSwallow("} catch (e) { console.error(e); }", "js"), null); + assert.equal(detectErrorSwallow("} catch (e) { logger.warn(e.message); }", "js"), null); + assert.equal(detectErrorSwallow("} catch (e) { return e; }", "js"), null); + assert.equal(detectErrorSwallow("} catch (e) { reportError(e); }", "js"), null); // references e +}); + +test("detectErrorSwallow: a `return null` that is one branch of a body that ALSO rethrows is not return-null", () => { + // return-null requires the WHOLE body to be the null return; a guarded null-return that then rethrows is + // real error handling, not a swallow. + assert.equal(detectErrorSwallow("} catch (e) { if (recoverable) return null; throw e; }", "js"), null); + assert.equal(detectErrorSwallow("} catch (e) { log(e); return null; }", "js"), null); // logs, then returns +}); + +test("detectErrorSwallow: a binding containing `$`/`_` is matched safely (regex-escaped, identifier boundary)", () => { + // A `$`-prefixed binding must not break the reference regex, and a real reference to it must be recognized. + assert.equal(detectErrorSwallow("} catch ($err) { report($err); }", "js"), null); // references $err + assert.equal(detectErrorSwallow("} catch (_e) { use(_e.stack); }", "js"), null); // references _e + // …but a body that ignores the `$`-binding is still an unused-binding. + assert.equal(detectErrorSwallow("} catch ($err) { cleanup(); }", "js"), "unused-binding"); +}); + +test("detectErrorSwallow: a bindingless catch that does real work is not an unused-binding", () => { + // No binding to ignore, non-empty body — not flagged (only empty-catch/return-null apply without a binding). + assert.equal(detectErrorSwallow("} catch { cleanup(); }", "js"), null); +}); + +test("detectErrorSwallow: `catch` inside a string or comment is not matched", () => { + assert.equal(detectErrorSwallow('const s = "} catch (e) {}";', "js"), null); + assert.equal(detectErrorSwallow("// } catch (e) {} left as a note", "js"), null); +}); + +test("detectErrorSwallow: Python `except …: pass` is an empty-catch; a handling except is not", () => { + assert.equal(detectErrorSwallow(" except ValueError: pass", "py"), "empty-catch"); + assert.equal(detectErrorSwallow(" except: pass", "py"), "empty-catch"); + assert.equal(detectErrorSwallow(" except ValueError: raise", "py"), null); + assert.equal(detectErrorSwallow(" except ValueError as e: log(e)", "py"), null); +}); + +test("scanPatchForErrorSwallow: flags kinds on added lines with correct locations; non-JS/Py skipped", () => { + const findings = scanPatchForErrorSwallow( + "src/svc.ts", + patchOf(["function f() {", " try { a(); } catch (e) {}", " return 1;", "}"]), + ); + assert.deepEqual(findings, [{ file: "src/svc.ts", line: 2, kind: "empty-catch" }]); + assert.deepEqual(scanPatchForErrorSwallow("docs/x.md", patchOf(["} catch (e) {}"])), []); +}); + +test("scanPatchForErrorSwallow: only ADDED lines are scanned; new-file line numbers stay correct", () => { + const patch = [ + "@@ -10,2 +10,2 @@", + " function f() {", // context line 10 + "- } catch (e) {}", // removed, does not advance + "+ } catch (e) {}", // new-file line 11 + ].join("\n"); + assert.deepEqual(scanPatchForErrorSwallow("src/a.ts", patch), [ + { file: "src/a.ts", line: 11, kind: "empty-catch" }, + ]); +}); + +test("scanPatchForErrorSwallow: enforces the maxFindings cap", () => { + const lines = Array.from({ length: 30 }, () => "} catch (e) {}"); + assert.equal(scanPatchForErrorSwallow("src/a.ts", patchOf(lines), { maxFindings: 5 }).length, 5); + assert.deepEqual(scanPatchForErrorSwallow("src/a.ts", patchOf(lines), { maxFindings: 0 }), []); +}); + +test("scanErrorSwallow: scans every changed file and honors the global cap", async () => { + const empties = Array.from({ length: 30 }, () => "} catch (e) {}"); + const findings = await scanErrorSwallow({ + repoFullName: "octo/repo", + prNumber: 1, + files: [ + { path: "src/a.ts", patch: patchOf(["const ok = true;"]) }, + { path: "src/b.ts", patch: patchOf(empties) }, + ], + }); + assert.equal(findings.length, 25); + assert.ok(findings.every((f) => f.file === "src/b.ts")); +}); + +test("scanErrorSwallow: no files yields no findings", async () => { + assert.deepEqual(await scanErrorSwallow({ repoFullName: "octo/repo", prNumber: 1 }), []); +}); diff --git a/src/review/enrichment-analyzer-names.ts b/src/review/enrichment-analyzer-names.ts index f1f7d44de3..c35f1666af 100644 --- a/src/review/enrichment-analyzer-names.ts +++ b/src/review/enrichment-analyzer-names.ts @@ -40,6 +40,7 @@ export const REES_ANALYZER_NAMES = [ "magicNumber", "conflictMarker", "commitLint", + "errorSwallow", ] as const; export type ReesAnalyzerName = (typeof REES_ANALYZER_NAMES)[number]; diff --git a/test/unit/enrichment-wire.test.ts b/test/unit/enrichment-wire.test.ts index 9f4f57c51a..1aff5d8c3f 100644 --- a/test/unit/enrichment-wire.test.ts +++ b/test/unit/enrichment-wire.test.ts @@ -626,7 +626,7 @@ describe("resolveReesAnalyzers", () => { resolveReesAnalyzers( env({ REES_ANALYZERS: - "dependency,lockfileDrift,secret,license,installScript,heavyDependency,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,conflictMarker,commitLint", + "dependency,lockfileDrift,secret,license,installScript,heavyDependency,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,conflictMarker,commitLint,errorSwallow", }), ), ).toEqual([ @@ -665,6 +665,7 @@ describe("resolveReesAnalyzers", () => { "todoMarker", "conflictMarker", "commitLint", + "errorSwallow", ]); });