From 84ba73b8eb6064279e173934e5728d020fc4cb36 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sat, 18 Apr 2026 11:58:26 -0400 Subject: [PATCH] Document each built-in rule in its own folder --- AGENTS.md | 2 +- README.md | 23 ++++---- src/rules/async-noise/README.md | 48 ++++++++++++++++ .../{async-noise.ts => async-noise/index.ts} | 8 +-- src/rules/barrel-density/README.md | 41 ++++++++++++++ .../index.ts} | 6 +- src/rules/directory-fanout-hotspot/README.md | 53 ++++++++++++++++++ .../index.ts} | 10 ++-- .../duplicate-function-signatures/README.md | 55 ++++++++++++++++++ .../index.ts} | 6 +- src/rules/duplicate-mock-setup/README.md | 44 +++++++++++++++ .../index.ts} | 6 +- src/rules/empty-catch/README.md | 50 +++++++++++++++++ .../{empty-catch.ts => empty-catch/index.ts} | 8 +-- src/rules/error-obscuring/README.md | 56 +++++++++++++++++++ .../index.ts} | 8 +-- src/rules/error-swallowing/README.md | 44 +++++++++++++++ .../index.ts} | 8 +-- src/rules/over-fragmentation/README.md | 53 ++++++++++++++++++ .../index.ts} | 10 ++-- src/rules/pass-through-wrappers/README.md | 45 +++++++++++++++ .../index.ts} | 8 +-- src/rules/placeholder-comments/README.md | 52 +++++++++++++++++ .../index.ts} | 6 +- src/rules/{ => shared}/helpers.ts | 0 .../{ => shared}/try-catch-rule-helpers.ts | 2 +- 26 files changed, 597 insertions(+), 55 deletions(-) create mode 100644 src/rules/async-noise/README.md rename src/rules/{async-noise.ts => async-noise/index.ts} (93%) create mode 100644 src/rules/barrel-density/README.md rename src/rules/{barrel-density.ts => barrel-density/index.ts} (91%) create mode 100644 src/rules/directory-fanout-hotspot/README.md rename src/rules/{directory-fanout-hotspot.ts => directory-fanout-hotspot/index.ts} (93%) create mode 100644 src/rules/duplicate-function-signatures/README.md rename src/rules/{duplicate-function-signatures.ts => duplicate-function-signatures/index.ts} (95%) create mode 100644 src/rules/duplicate-mock-setup/README.md rename src/rules/{duplicate-mock-setup.ts => duplicate-mock-setup/index.ts} (95%) create mode 100644 src/rules/empty-catch/README.md rename src/rules/{empty-catch.ts => empty-catch/index.ts} (91%) create mode 100644 src/rules/error-obscuring/README.md rename src/rules/{error-obscuring.ts => error-obscuring/index.ts} (92%) create mode 100644 src/rules/error-swallowing/README.md rename src/rules/{error-swallowing.ts => error-swallowing/index.ts} (91%) create mode 100644 src/rules/over-fragmentation/README.md rename src/rules/{over-fragmentation.ts => over-fragmentation/index.ts} (93%) create mode 100644 src/rules/pass-through-wrappers/README.md rename src/rules/{pass-through-wrappers.ts => pass-through-wrappers/index.ts} (93%) create mode 100644 src/rules/placeholder-comments/README.md rename src/rules/{placeholder-comments.ts => placeholder-comments/index.ts} (93%) rename src/rules/{ => shared}/helpers.ts (100%) rename src/rules/{ => shared}/try-catch-rule-helpers.ts (96%) diff --git a/AGENTS.md b/AGENTS.md index ce851e5..11bae53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ - Public plugin helpers and plugin object shape: `src/plugin.ts` - Discovery / ignore handling: `src/discovery/walk.ts` - Reusable signals: `src/facts/*` -- Findings logic: `src/rules/*` (flat files; grouping is by rule `id` / `family`, not folders) +- Findings logic: `src/rules//index.ts` with per-rule docs in `src/rules//README.md` and shared helpers in `src/rules/shared/*` - Output formats: `src/reporters/*` - Current language scope: `src/languages/javascript-like.ts` diff --git a/README.md b/README.md index 7962bcf..5e0aee5 100644 --- a/README.md +++ b/README.md @@ -125,17 +125,17 @@ slop-scan delta --base ../main --fail-on added,worsened Current checks focus on patterns that often show up in unreviewed generated code: -- log-and-continue catch blocks -- error-obscuring catch blocks (default-return or generic replacement error) -- empty catch blocks -- async wrapper / `return await` noise -- pass-through wrappers -- barrel density -- duplicate helper/function signatures across source files -- over-fragmentation -- directory fan-out hotspots -- placeholder comments -- duplicated test mock/setup patterns +- [log-and-continue catch blocks](src/rules/error-swallowing/README.md) +- [error-obscuring catch blocks](src/rules/error-obscuring/README.md) (default-return or generic replacement error) +- [empty catch blocks](src/rules/empty-catch/README.md) +- [async wrapper / `return await` noise](src/rules/async-noise/README.md) +- [pass-through wrappers](src/rules/pass-through-wrappers/README.md) +- [barrel density](src/rules/barrel-density/README.md) +- [duplicate helper/function signatures across source files](src/rules/duplicate-function-signatures/README.md) +- [over-fragmentation](src/rules/over-fragmentation/README.md) +- [directory fan-out hotspots](src/rules/directory-fanout-hotspot/README.md) +- [placeholder comments](src/rules/placeholder-comments/README.md) +- [duplicated test mock/setup patterns](src/rules/duplicate-mock-setup/README.md) ## What you get back @@ -283,6 +283,7 @@ That keeps the analyzer deterministic and extensible without turning it into one ## Docs - plugin guide: [`docs/plugins.md`](docs/plugins.md) +- built-in rule docs: browse [`src/rules/`](src/rules) - benchmark guide: [`benchmarks/README.md`](benchmarks/README.md) - pinned benchmark report: [`reports/known-ai-vs-solid-oss-benchmark.md`](reports/known-ai-vs-solid-oss-benchmark.md) - exploratory note on non-JS/TS candidates: [`reports/exploratory-vite-astro-openclaw-beads.md`](reports/exploratory-vite-astro-openclaw-beads.md) diff --git a/src/rules/async-noise/README.md b/src/rules/async-noise/README.md new file mode 100644 index 0000000..097399b --- /dev/null +++ b/src/rules/async-noise/README.md @@ -0,0 +1,48 @@ +# defensive.async-noise + +Flags async ceremony that adds little value. + +- **Family:** `defensive` +- **Severity:** `medium` +- **Scope:** `file` +- **Requires:** `file.functionSummaries` + +## How it works + +The rule reports two patterns: + +- redundant `return await` around a direct call +- trivial async pass-through wrappers with no internal `await` + +Boundary wrappers are exempted for common edge-facing targets such as `fetch`, `axios.*`, `prisma.*`, `redis.*`, and similar APIs, because those wrappers are often intentional integration boundaries. + +## Flagged examples + +```ts +async function loadUser(id: string) { + return await fetchUser(id); +} + +async function getUser(id: string) { + return fetchUser(id); +} +``` + +## Usually ignored + +```ts +async function loadUser(id: string) { + const user = await fetchUser(id); + return normalizeUser(user); +} + +async function getJson(url: string) { + return fetch(url); +} +``` + +## Scoring + +Redundant `return await` sites add `1.5` each. +Plain async pass-through wrappers add `0.75` each. +The total file contribution is capped at `4`. diff --git a/src/rules/async-noise.ts b/src/rules/async-noise/index.ts similarity index 93% rename from src/rules/async-noise.ts rename to src/rules/async-noise/index.ts index 264cbb2..e384f98 100644 --- a/src/rules/async-noise.ts +++ b/src/rules/async-noise/index.ts @@ -1,7 +1,7 @@ -import type { RulePlugin } from "../core/types"; -import type { FunctionSummary } from "../facts/types"; -import { delta } from "../rule-delta"; -import { BOUNDARY_WRAPPER_TARGET_PREFIXES } from "./helpers"; +import type { RulePlugin } from "../../core/types"; +import type { FunctionSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; +import { BOUNDARY_WRAPPER_TARGET_PREFIXES } from "../shared/helpers"; type AsyncNoiseMatch = { summary: FunctionSummary; diff --git a/src/rules/barrel-density/README.md b/src/rules/barrel-density/README.md new file mode 100644 index 0000000..70d0123 --- /dev/null +++ b/src/rules/barrel-density/README.md @@ -0,0 +1,41 @@ +# structure.barrel-density + +Flags files that are effectively nothing but re-export barrels. + +- **Family:** `structure` +- **Severity:** `medium` +- **Scope:** `file` +- **Requires:** `file.exportSummary` + +## How it works + +A file is reported when both of these are true: + +- every top-level statement is a re-export +- there are at least 2 re-export statements + +That keeps the rule focused on pure barrels instead of legitimate modules that happen to re-export one helper or type. + +## Flagged example + +```ts +export * from "./client"; +export * from "./types"; +export { createStore } from "./store"; +``` + +## Usually ignored + +```ts +import { createStoreImpl } from "./store"; + +export function createStore() { + return createStoreImpl(); +} + +export { type Store } from "./types"; +``` + +## Scoring + +The score starts at `1` and adds `0.5` per re-export statement, capped at `3`. diff --git a/src/rules/barrel-density.ts b/src/rules/barrel-density/index.ts similarity index 91% rename from src/rules/barrel-density.ts rename to src/rules/barrel-density/index.ts index 8324c65..e4f577a 100644 --- a/src/rules/barrel-density.ts +++ b/src/rules/barrel-density/index.ts @@ -1,6 +1,6 @@ -import type { RulePlugin } from "../core/types"; -import type { ExportSummary } from "../facts/types"; -import { delta } from "../rule-delta"; +import type { RulePlugin } from "../../core/types"; +import type { ExportSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; /** * Flags files that are mostly re-export barrels. diff --git a/src/rules/directory-fanout-hotspot/README.md b/src/rules/directory-fanout-hotspot/README.md new file mode 100644 index 0000000..b3cf0d8 --- /dev/null +++ b/src/rules/directory-fanout-hotspot/README.md @@ -0,0 +1,53 @@ +# structure.directory-fanout-hotspot + +Flags directories whose file count is unusually large relative to nearby siblings or, when sibling context is weak, the repo-wide average. + +- **Family:** `structure` +- **Severity:** `medium` +- **Scope:** `directory` +- **Requires:** `directory.metrics` + +## How it works + +The rule prefers a sibling-directory baseline when there is enough local context. +Otherwise it falls back to a repo-wide average. +A directory is reported only when its file count clears a dynamic threshold: + +- sibling baseline: `ceil(medianSiblingCount * 2.25)` +- repo-wide fallback: `ceil(globalAverage * 2.5)` +- absolute minimum threshold: `6` files + +It skips directories that are mostly tests and asset-like directories such as `icons/` or `assets/`. + +## Flagged example + +```text +src/ +├── api/ # 3 files +├── auth/ # 4 files +├── billing/ # 3 files +└── generated-actions/ # 16 files + ├── action-01.ts + ├── action-02.ts + ├── action-03.ts + └── ... +``` + +With siblings clustered around 3–4 files, `generated-actions/` becomes a local fan-out hotspot. + +## Usually ignored + +```text +src/icons/ +├── add.tsx +├── remove.tsx +├── search.tsx +└── ... +``` + +Asset-like buckets and test-matrix directories are intentionally suppressed because wide directory shapes are expected there. + +## Scoring + +The rule starts at `2` and adds a bounded amount based on how far the directory is above the computed threshold. +The total directory contribution stays capped at `6`. diff --git a/src/rules/directory-fanout-hotspot.ts b/src/rules/directory-fanout-hotspot/index.ts similarity index 93% rename from src/rules/directory-fanout-hotspot.ts rename to src/rules/directory-fanout-hotspot/index.ts index 673e94f..9b56b3b 100644 --- a/src/rules/directory-fanout-hotspot.ts +++ b/src/rules/directory-fanout-hotspot/index.ts @@ -1,9 +1,9 @@ import path from "node:path"; -import type { RulePlugin } from "../core/types"; -import { isTestFile } from "../facts/ts-helpers"; -import type { DirectoryMetrics } from "../facts/types"; -import { delta } from "../rule-delta"; -import { average, countMatching, isAssetLikeDirectoryPath, median, ratio } from "./helpers"; +import type { RulePlugin } from "../../core/types"; +import { isTestFile } from "../../facts/ts-helpers"; +import type { DirectoryMetrics } from "../../facts/types"; +import { delta } from "../../rule-delta"; +import { average, countMatching, isAssetLikeDirectoryPath, median, ratio } from "../shared/helpers"; /** * Flags directories whose file count is unusually large relative to nearby diff --git a/src/rules/duplicate-function-signatures/README.md b/src/rules/duplicate-function-signatures/README.md new file mode 100644 index 0000000..9edc7a6 --- /dev/null +++ b/src/rules/duplicate-function-signatures/README.md @@ -0,0 +1,55 @@ +# structure.duplicate-function-signatures + +Flags repeated non-test helper shapes that show up across several source files. + +- **Family:** `structure` +- **Severity:** `medium` +- **Scope:** `file` +- **Requires:** `repo.duplicateFunctionSignatures` + +## How it works + +A repo-level fact builds structural fingerprints for function bodies, normalizing local names so copy-pasted helpers still match after superficial renaming. +The rule then projects those duplicate clusters back onto each affected file. + +A cluster only counts when it appears in **3 or more files**. +Tiny functions and pass-through wrappers are excluded before clustering, and test files are skipped entirely. + +## Flagged example + +```ts +// src/users/normalize.ts +export function normalizeUser(input: ApiUser) { + const name = input.name?.trim() ?? ""; + const email = input.email?.toLowerCase() ?? ""; + return { name, email, active: Boolean(input.active) }; +} + +// src/teams/normalize.ts +export function normalizeTeamMember(member: ApiMember) { + const name = member.name?.trim() ?? ""; + const email = member.email?.toLowerCase() ?? ""; + return { name, email, active: Boolean(member.active) }; +} + +// src/accounts/normalize.ts +export function normalizeAccountOwner(owner: ApiOwner) { + const name = owner.name?.trim() ?? ""; + const email = owner.email?.toLowerCase() ?? ""; + return { name, email, active: Boolean(owner.active) }; +} +``` + +## Usually ignored + +```ts +export function getUser(id: string) { + return loadUser(id); +} +``` + +Pass-through wrappers are excluded, and a duplicate that only appears in 2 files is below the reporting threshold. + +## Scoring + +Each duplicate cluster adds `1.25 + 0.5 * (fileCount - 3)` for the current file, capped at `6`. diff --git a/src/rules/duplicate-function-signatures.ts b/src/rules/duplicate-function-signatures/index.ts similarity index 95% rename from src/rules/duplicate-function-signatures.ts rename to src/rules/duplicate-function-signatures/index.ts index 203244e..f792443 100644 --- a/src/rules/duplicate-function-signatures.ts +++ b/src/rules/duplicate-function-signatures/index.ts @@ -1,6 +1,6 @@ -import type { RulePlugin } from "../core/types"; -import type { DuplicateFunctionIndex } from "../facts/types"; -import { isTestFile } from "../facts/ts-helpers"; +import type { RulePlugin } from "../../core/types"; +import type { DuplicateFunctionIndex } from "../../facts/types"; +import { isTestFile } from "../../facts/ts-helpers"; function findUniqueDuplicateFunctionClusters( duplication: DuplicateFunctionIndex | undefined, diff --git a/src/rules/duplicate-mock-setup/README.md b/src/rules/duplicate-mock-setup/README.md new file mode 100644 index 0000000..dfb0a0a --- /dev/null +++ b/src/rules/duplicate-mock-setup/README.md @@ -0,0 +1,44 @@ +# tests.duplicate-mock-setup + +Flags repeated test mock/setup shapes across several test files. + +- **Family:** `tests` +- **Severity:** `medium` +- **Scope:** `file` +- **Requires:** `repo.testMockDuplication` + +## How it works + +A repo-level fact fingerprints statement-level mock/setup shapes inside test files. +The rule reports a file when one of those shapes appears in **3 or more test files**. + +Generic labels such as `vi.mock`, `jest.mock`, `vi.spyOn`, `jest.spyOn`, `sinon.stub`, and `sinon.spy` are filtered out so routine framework setup does not dominate the signal. +Cleanup-only statements like `mockReset` and `mockClear` are also ignored. + +## Flagged example + +```ts +// users.test.ts +vi.mocked(api.fetchUser).mockResolvedValue({ id: 1, name: "Ada" }); + +// teams.test.ts +vi.mocked(api.fetchUser).mockResolvedValue({ id: 2, name: "Lin" }); + +// accounts.test.ts +vi.mocked(api.fetchUser).mockResolvedValue({ id: 3, name: "Max" }); +``` + +Once that same setup shape appears in 3 files, each participating file gets a finding. + +## Usually ignored + +```ts +vi.mock("./client"); +vi.clearAllMocks(); +``` + +Generic mock declarations and cleanup-only statements do not contribute to this rule. + +## Scoring + +Each duplicate setup cluster adds `1 + 0.5 * (fileCount - 2)` for the current file, capped at `5`. diff --git a/src/rules/duplicate-mock-setup.ts b/src/rules/duplicate-mock-setup/index.ts similarity index 95% rename from src/rules/duplicate-mock-setup.ts rename to src/rules/duplicate-mock-setup/index.ts index 49276c7..7446032 100644 --- a/src/rules/duplicate-mock-setup.ts +++ b/src/rules/duplicate-mock-setup/index.ts @@ -1,6 +1,6 @@ -import type { RulePlugin } from "../core/types"; -import type { DuplicateTestSetupIndex } from "../facts/types"; -import { isTestFile } from "../facts/ts-helpers"; +import type { RulePlugin } from "../../core/types"; +import type { DuplicateTestSetupIndex } from "../../facts/types"; +import { isTestFile } from "../../facts/ts-helpers"; function findUniqueDuplicateMockSetupClusters( duplication: DuplicateTestSetupIndex | undefined, diff --git a/src/rules/empty-catch/README.md b/src/rules/empty-catch/README.md new file mode 100644 index 0000000..0211a87 --- /dev/null +++ b/src/rules/empty-catch/README.md @@ -0,0 +1,50 @@ +# defensive.empty-catch + +Flags empty catch blocks that silently suppress failures. + +- **Family:** `defensive` +- **Severity:** `strong` +- **Scope:** `file` +- **Requires:** `file.tryCatchSummaries` + +## How it works + +The rule reports small try/catch blocks when the catch body is empty. +It intentionally skips: + +- common filesystem-existence probes +- documented local fallbacks where the try block only resolves local values and the catch explains that execution should fall through to another source +- larger try blocks where this structural approximation is less trustworthy + +## Flagged example + +```ts +export function parseConfig(raw: string) { + try { + return JSON.parse(raw); + } catch {} + + return null; +} +``` + +## Usually ignored + +```ts +export function loadTheme() { + let stored: string | null = null; + + try { + stored = localStorage.getItem("theme"); + } catch { + // fall through to the default theme + } + + return stored ?? "light"; +} +``` + +## Scoring + +Each flagged catch uses the shared try/catch scoring helper, then the file total is capped at `8`. +Boundary-oriented catches are downweighted instead of fully ignored. diff --git a/src/rules/empty-catch.ts b/src/rules/empty-catch/index.ts similarity index 91% rename from src/rules/empty-catch.ts rename to src/rules/empty-catch/index.ts index 8692b6b..a3ca913 100644 --- a/src/rules/empty-catch.ts +++ b/src/rules/empty-catch/index.ts @@ -1,11 +1,11 @@ -import type { RulePlugin } from "../core/types"; -import type { TryCatchSummary } from "../facts/types"; -import { delta } from "../rule-delta"; +import type { RulePlugin } from "../../core/types"; +import type { TryCatchSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; import { formatTryCatchBoundary, isValidTryCatchTarget, scoreTryCatch, -} from "./try-catch-rule-helpers"; +} from "../shared/try-catch-rule-helpers"; function findEmptyCatchSummaries(summaries: TryCatchSummary[]): TryCatchSummary[] { return summaries.filter( diff --git a/src/rules/error-obscuring/README.md b/src/rules/error-obscuring/README.md new file mode 100644 index 0000000..666d7af --- /dev/null +++ b/src/rules/error-obscuring/README.md @@ -0,0 +1,56 @@ +# defensive.error-obscuring + +Flags catch blocks that replace the original failure with a default value or generic error. + +- **Family:** `defensive` +- **Severity:** `strong` +- **Scope:** `file` +- **Requires:** `file.tryCatchSummaries` + +## How it works + +The rule reports small try/catch blocks when the catch clause does one of these things: + +- returns a default literal +- throws a generic replacement error +- logs and then returns a default + +Those patterns make downstream diagnosis harder because the original failure is flattened or hidden. + +## Flagged examples + +```ts +export function readConfig(raw: string) { + try { + return JSON.parse(raw); + } catch { + return {}; + } +} + +export function loadProfile(id: string) { + try { + return fetchProfile(id); + } catch { + throw new Error("failed to load profile"); + } +} +``` + +## Usually ignored + +```ts +export function readConfig(raw: string) { + try { + return JSON.parse(raw); + } catch (error) { + logger.error({ error }); + throw error; + } +} +``` + +## Scoring + +Each flagged catch uses the shared try/catch scoring helper, then the file total is capped at `8`. +Generic rethrows are still noisy, but scored slightly lower than silent default-return patterns. diff --git a/src/rules/error-obscuring.ts b/src/rules/error-obscuring/index.ts similarity index 92% rename from src/rules/error-obscuring.ts rename to src/rules/error-obscuring/index.ts index 635de82..19df23f 100644 --- a/src/rules/error-obscuring.ts +++ b/src/rules/error-obscuring/index.ts @@ -1,11 +1,11 @@ -import type { RulePlugin } from "../core/types"; -import type { TryCatchSummary } from "../facts/types"; -import { delta } from "../rule-delta"; +import type { RulePlugin } from "../../core/types"; +import type { TryCatchSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; import { formatTryCatchBoundary, isValidTryCatchTarget, scoreTryCatch, -} from "./try-catch-rule-helpers"; +} from "../shared/try-catch-rule-helpers"; /** * Keeps evidence strings aligned on the same catch-transformation categories the rule reports. diff --git a/src/rules/error-swallowing/README.md b/src/rules/error-swallowing/README.md new file mode 100644 index 0000000..38e782c --- /dev/null +++ b/src/rules/error-swallowing/README.md @@ -0,0 +1,44 @@ +# defensive.error-swallowing + +Flags log-and-continue catch blocks. + +- **Family:** `defensive` +- **Severity:** `strong` +- **Scope:** `file` +- **Requires:** `file.tryCatchSummaries` + +## How it works + +The rule looks for small try/catch blocks where the catch clause only logs and then continues. +That pattern records the failure but still suppresses it from callers. + +Filesystem-existence probes are ignored, and boundary-heavy catches are downweighted rather than removed entirely. + +## Flagged example + +```ts +export async function syncUser(id: string) { + try { + await pushUser(id); + } catch (error) { + logger.warn(error); + } +} +``` + +## Usually ignored + +```ts +export async function syncUser(id: string) { + try { + await pushUser(id); + } catch (error) { + logger.error({ error, id }); + throw error; + } +} +``` + +## Scoring + +Each flagged catch uses the shared try/catch scoring helper, then the file total is capped at `8`. diff --git a/src/rules/error-swallowing.ts b/src/rules/error-swallowing/index.ts similarity index 91% rename from src/rules/error-swallowing.ts rename to src/rules/error-swallowing/index.ts index ac64a48..85e219a 100644 --- a/src/rules/error-swallowing.ts +++ b/src/rules/error-swallowing/index.ts @@ -1,11 +1,11 @@ -import type { RulePlugin } from "../core/types"; -import type { TryCatchSummary } from "../facts/types"; -import { delta } from "../rule-delta"; +import type { RulePlugin } from "../../core/types"; +import type { TryCatchSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; import { formatTryCatchBoundary, isValidTryCatchTarget, scoreTryCatch, -} from "./try-catch-rule-helpers"; +} from "../shared/try-catch-rule-helpers"; function findErrorSwallowingSummaries(summaries: TryCatchSummary[]): TryCatchSummary[] { return summaries.filter( diff --git a/src/rules/over-fragmentation/README.md b/src/rules/over-fragmentation/README.md new file mode 100644 index 0000000..57cf0a5 --- /dev/null +++ b/src/rules/over-fragmentation/README.md @@ -0,0 +1,53 @@ +# structure.over-fragmentation + +Flags directories dominated by tiny files and structural ceremony. + +- **Family:** `structure` +- **Severity:** `strong` +- **Scope:** `directory` +- **Requires:** `directory.metrics` + +## How it works + +A directory is considered suspicious when all of these are true: + +- it has at least `6` files +- at least `60%` of its files are tiny (`<= 25` lines) +- it is not mostly tests +- it is not an asset-like directory such as `icons/` or `assets/` + +The rule also looks at ceremony density by counting wrapper files and pure barrel files inside the directory. +If the directory is small-file-heavy but those files still contain substantial implementation, the rule backs off. + +## Flagged example + +```text +src/payments/ +├── index.ts +├── create-payment.ts +├── update-payment.ts +├── delete-payment.ts +├── get-payment.ts +├── payment-types.ts +├── payment-errors.ts +└── payment-client.ts +``` + +If most of those files are tiny wrappers or barrels, the directory is likely over-fragmented rather than intentionally modular. + +## Usually ignored + +```text +src/icons/ +├── add.tsx +├── remove.tsx +├── search.tsx +└── ... +``` + +Asset buckets and test-heavy directories are suppressed, and a directory full of small but substantial implementation files can also avoid a finding. + +## Scoring + +The score is `4 + tinyRatio * 3 + ceremonyRatio * 2`. +That weights tiny-file prevalence most heavily and adds extra pressure when wrappers and barrels make up a large share of the directory. diff --git a/src/rules/over-fragmentation.ts b/src/rules/over-fragmentation/index.ts similarity index 93% rename from src/rules/over-fragmentation.ts rename to src/rules/over-fragmentation/index.ts index 9896b51..5e090cf 100644 --- a/src/rules/over-fragmentation.ts +++ b/src/rules/over-fragmentation/index.ts @@ -1,8 +1,8 @@ -import type { RulePlugin } from "../core/types"; -import { isTestFile } from "../facts/ts-helpers"; -import type { DirectoryMetrics } from "../facts/types"; -import { delta } from "../rule-delta"; -import { countMatching, isAssetLikeDirectoryPath, ratio } from "./helpers"; +import type { RulePlugin } from "../../core/types"; +import { isTestFile } from "../../facts/ts-helpers"; +import type { DirectoryMetrics } from "../../facts/types"; +import { delta } from "../../rule-delta"; +import { countMatching, isAssetLikeDirectoryPath, ratio } from "../shared/helpers"; /** * Flags directories dominated by tiny files and structural ceremony. diff --git a/src/rules/pass-through-wrappers/README.md b/src/rules/pass-through-wrappers/README.md new file mode 100644 index 0000000..9e5bd49 --- /dev/null +++ b/src/rules/pass-through-wrappers/README.md @@ -0,0 +1,45 @@ +# structure.pass-through-wrappers + +Flags trivial wrappers that mostly just rename or forward another call. + +- **Family:** `structure` +- **Severity:** `strong` +- **Scope:** `file` +- **Requires:** `file.functionSummaries`, `file.comments` + +## How it works + +The rule looks for functions whose body is essentially a direct pass-through call. +It skips two common intentional cases: + +- nearby alias / compatibility comments such as `alias` or `backward compatibility` +- boundary wrappers around targets like `fetch`, `axios.*`, `prisma.*`, `redis.*`, and similar APIs + +## Flagged example + +```ts +export function getUser(id: string) { + return loadUser(id); +} + +export function saveUser(input: UserInput) { + return persistUser(input); +} +``` + +## Usually ignored + +```ts +// backward compatibility alias +export function fetchUserRecord(id: string) { + return getUser(id); +} + +export function getJson(url: string) { + return fetch(url); +} +``` + +## Scoring + +Each wrapper adds `2` points, capped at `5` for the file. diff --git a/src/rules/pass-through-wrappers.ts b/src/rules/pass-through-wrappers/index.ts similarity index 93% rename from src/rules/pass-through-wrappers.ts rename to src/rules/pass-through-wrappers/index.ts index eb15fa5..c95429e 100644 --- a/src/rules/pass-through-wrappers.ts +++ b/src/rules/pass-through-wrappers/index.ts @@ -1,7 +1,7 @@ -import type { RulePlugin } from "../core/types"; -import type { CommentSummary, FunctionSummary } from "../facts/types"; -import { delta } from "../rule-delta"; -import { BOUNDARY_WRAPPER_TARGET_PREFIXES } from "./helpers"; +import type { RulePlugin } from "../../core/types"; +import type { CommentSummary, FunctionSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; +import { BOUNDARY_WRAPPER_TARGET_PREFIXES } from "../shared/helpers"; // Nearby wording like "alias" or "backward compatibility" usually means the // wrapper exists to preserve an API name rather than because the author lazily diff --git a/src/rules/placeholder-comments/README.md b/src/rules/placeholder-comments/README.md new file mode 100644 index 0000000..2a29ca2 --- /dev/null +++ b/src/rules/placeholder-comments/README.md @@ -0,0 +1,52 @@ +# comments.placeholder-comments + +Flags filler comments that gesture at future work without explaining current behavior. + +- **Family:** `comments` +- **Severity:** `weak` +- **Scope:** `file` +- **Requires:** `file.comments` + +## How it works + +The rule scans parsed comments in a file and looks for intentionally strong placeholder-style phrasing, including patterns like: + +- `add more validation` +- `handle more cases` +- `extend this logic` +- `customize this behavior` +- `implement ... here` + +The patterns are conservative on purpose so routine TODOs and descriptive maintenance notes do not create noise. + +## Flagged example + +```ts +// Add more validation if needed +export function normalizeName(input: string) { + return input.trim(); +} + +// Handle additional cases here later +export function parseMode(value: string) { + return value === "fast" ? "fast" : "safe"; +} +``` + +## Usually ignored + +```ts +// Keep in sync with the upstream API contract. +export function normalizeName(input: string) { + return input.trim(); +} + +// TODO(ben): remove after the v2 rollout. +export function legacyMode() { + return "safe"; +} +``` + +## Scoring + +Each matching comment adds `0.75` to the file score, capped at `1.5`. diff --git a/src/rules/placeholder-comments.ts b/src/rules/placeholder-comments/index.ts similarity index 93% rename from src/rules/placeholder-comments.ts rename to src/rules/placeholder-comments/index.ts index 0a13638..c6ae32d 100644 --- a/src/rules/placeholder-comments.ts +++ b/src/rules/placeholder-comments/index.ts @@ -1,6 +1,6 @@ -import type { RulePlugin } from "../core/types"; -import type { CommentSummary } from "../facts/types"; -import { delta } from "../rule-delta"; +import type { RulePlugin } from "../../core/types"; +import type { CommentSummary } from "../../facts/types"; +import { delta } from "../../rule-delta"; /** * Flags filler comments that gesture at future work without explaining current diff --git a/src/rules/helpers.ts b/src/rules/shared/helpers.ts similarity index 100% rename from src/rules/helpers.ts rename to src/rules/shared/helpers.ts diff --git a/src/rules/try-catch-rule-helpers.ts b/src/rules/shared/try-catch-rule-helpers.ts similarity index 96% rename from src/rules/try-catch-rule-helpers.ts rename to src/rules/shared/try-catch-rule-helpers.ts index cea1a76..3ab4e8f 100644 --- a/src/rules/try-catch-rule-helpers.ts +++ b/src/rules/shared/try-catch-rule-helpers.ts @@ -1,4 +1,4 @@ -import type { TryCatchSummary } from "../facts/types"; +import type { TryCatchSummary } from "../../facts/types"; /** * Screens out probe-style catches that would dominate results without being meaningfully slop-like.