Skip to content

Commit 39cc012

Browse files
authored
Document each built-in rule in its own folder (#12)
1 parent e4c6b92 commit 39cc012

26 files changed

Lines changed: 597 additions & 55 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
- Public plugin helpers and plugin object shape: `src/plugin.ts`
3333
- Discovery / ignore handling: `src/discovery/walk.ts`
3434
- Reusable signals: `src/facts/*`
35-
- Findings logic: `src/rules/*` (flat files; grouping is by rule `id` / `family`, not folders)
35+
- Findings logic: `src/rules/<rule>/index.ts` with per-rule docs in `src/rules/<rule>/README.md` and shared helpers in `src/rules/shared/*`
3636
- Output formats: `src/reporters/*`
3737
- Current language scope: `src/languages/javascript-like.ts`
3838

README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,17 @@ slop-scan delta --base ../main --fail-on added,worsened
125125

126126
Current checks focus on patterns that often show up in unreviewed generated code:
127127

128-
- log-and-continue catch blocks
129-
- error-obscuring catch blocks (default-return or generic replacement error)
130-
- empty catch blocks
131-
- async wrapper / `return await` noise
132-
- pass-through wrappers
133-
- barrel density
134-
- duplicate helper/function signatures across source files
135-
- over-fragmentation
136-
- directory fan-out hotspots
137-
- placeholder comments
138-
- duplicated test mock/setup patterns
128+
- [log-and-continue catch blocks](src/rules/error-swallowing/README.md)
129+
- [error-obscuring catch blocks](src/rules/error-obscuring/README.md) (default-return or generic replacement error)
130+
- [empty catch blocks](src/rules/empty-catch/README.md)
131+
- [async wrapper / `return await` noise](src/rules/async-noise/README.md)
132+
- [pass-through wrappers](src/rules/pass-through-wrappers/README.md)
133+
- [barrel density](src/rules/barrel-density/README.md)
134+
- [duplicate helper/function signatures across source files](src/rules/duplicate-function-signatures/README.md)
135+
- [over-fragmentation](src/rules/over-fragmentation/README.md)
136+
- [directory fan-out hotspots](src/rules/directory-fanout-hotspot/README.md)
137+
- [placeholder comments](src/rules/placeholder-comments/README.md)
138+
- [duplicated test mock/setup patterns](src/rules/duplicate-mock-setup/README.md)
139139

140140
## What you get back
141141

@@ -283,6 +283,7 @@ That keeps the analyzer deterministic and extensible without turning it into one
283283
## Docs
284284

285285
- plugin guide: [`docs/plugins.md`](docs/plugins.md)
286+
- built-in rule docs: browse [`src/rules/`](src/rules)
286287
- benchmark guide: [`benchmarks/README.md`](benchmarks/README.md)
287288
- pinned benchmark report: [`reports/known-ai-vs-solid-oss-benchmark.md`](reports/known-ai-vs-solid-oss-benchmark.md)
288289
- exploratory note on non-JS/TS candidates: [`reports/exploratory-vite-astro-openclaw-beads.md`](reports/exploratory-vite-astro-openclaw-beads.md)

src/rules/async-noise/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# defensive.async-noise
2+
3+
Flags async ceremony that adds little value.
4+
5+
- **Family:** `defensive`
6+
- **Severity:** `medium`
7+
- **Scope:** `file`
8+
- **Requires:** `file.functionSummaries`
9+
10+
## How it works
11+
12+
The rule reports two patterns:
13+
14+
- redundant `return await` around a direct call
15+
- trivial async pass-through wrappers with no internal `await`
16+
17+
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.
18+
19+
## Flagged examples
20+
21+
```ts
22+
async function loadUser(id: string) {
23+
return await fetchUser(id);
24+
}
25+
26+
async function getUser(id: string) {
27+
return fetchUser(id);
28+
}
29+
```
30+
31+
## Usually ignored
32+
33+
```ts
34+
async function loadUser(id: string) {
35+
const user = await fetchUser(id);
36+
return normalizeUser(user);
37+
}
38+
39+
async function getJson(url: string) {
40+
return fetch(url);
41+
}
42+
```
43+
44+
## Scoring
45+
46+
Redundant `return await` sites add `1.5` each.
47+
Plain async pass-through wrappers add `0.75` each.
48+
The total file contribution is capped at `4`.
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { RulePlugin } from "../core/types";
2-
import type { FunctionSummary } from "../facts/types";
3-
import { delta } from "../rule-delta";
4-
import { BOUNDARY_WRAPPER_TARGET_PREFIXES } from "./helpers";
1+
import type { RulePlugin } from "../../core/types";
2+
import type { FunctionSummary } from "../../facts/types";
3+
import { delta } from "../../rule-delta";
4+
import { BOUNDARY_WRAPPER_TARGET_PREFIXES } from "../shared/helpers";
55

66
type AsyncNoiseMatch = {
77
summary: FunctionSummary;

src/rules/barrel-density/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# structure.barrel-density
2+
3+
Flags files that are effectively nothing but re-export barrels.
4+
5+
- **Family:** `structure`
6+
- **Severity:** `medium`
7+
- **Scope:** `file`
8+
- **Requires:** `file.exportSummary`
9+
10+
## How it works
11+
12+
A file is reported when both of these are true:
13+
14+
- every top-level statement is a re-export
15+
- there are at least 2 re-export statements
16+
17+
That keeps the rule focused on pure barrels instead of legitimate modules that happen to re-export one helper or type.
18+
19+
## Flagged example
20+
21+
```ts
22+
export * from "./client";
23+
export * from "./types";
24+
export { createStore } from "./store";
25+
```
26+
27+
## Usually ignored
28+
29+
```ts
30+
import { createStoreImpl } from "./store";
31+
32+
export function createStore() {
33+
return createStoreImpl();
34+
}
35+
36+
export { type Store } from "./types";
37+
```
38+
39+
## Scoring
40+
41+
The score starts at `1` and adds `0.5` per re-export statement, capped at `3`.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { RulePlugin } from "../core/types";
2-
import type { ExportSummary } from "../facts/types";
3-
import { delta } from "../rule-delta";
1+
import type { RulePlugin } from "../../core/types";
2+
import type { ExportSummary } from "../../facts/types";
3+
import { delta } from "../../rule-delta";
44

55
/**
66
* Flags files that are mostly re-export barrels.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# structure.directory-fanout-hotspot
2+
3+
Flags directories whose file count is unusually large relative to nearby siblings or, when sibling context is weak, the repo-wide average.
4+
5+
- **Family:** `structure`
6+
- **Severity:** `medium`
7+
- **Scope:** `directory`
8+
- **Requires:** `directory.metrics`
9+
10+
## How it works
11+
12+
The rule prefers a sibling-directory baseline when there is enough local context.
13+
Otherwise it falls back to a repo-wide average.
14+
A directory is reported only when its file count clears a dynamic threshold:
15+
16+
- sibling baseline: `ceil(medianSiblingCount * 2.25)`
17+
- repo-wide fallback: `ceil(globalAverage * 2.5)`
18+
- absolute minimum threshold: `6` files
19+
20+
It skips directories that are mostly tests and asset-like directories such as `icons/` or `assets/`.
21+
22+
## Flagged example
23+
24+
```text
25+
src/
26+
├── api/ # 3 files
27+
├── auth/ # 4 files
28+
├── billing/ # 3 files
29+
└── generated-actions/ # 16 files
30+
├── action-01.ts
31+
├── action-02.ts
32+
├── action-03.ts
33+
└── ...
34+
```
35+
36+
With siblings clustered around 3–4 files, `generated-actions/` becomes a local fan-out hotspot.
37+
38+
## Usually ignored
39+
40+
```text
41+
src/icons/
42+
├── add.tsx
43+
├── remove.tsx
44+
├── search.tsx
45+
└── ...
46+
```
47+
48+
Asset-like buckets and test-matrix directories are intentionally suppressed because wide directory shapes are expected there.
49+
50+
## Scoring
51+
52+
The rule starts at `2` and adds a bounded amount based on how far the directory is above the computed threshold.
53+
The total directory contribution stays capped at `6`.

src/rules/directory-fanout-hotspot.ts renamed to src/rules/directory-fanout-hotspot/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import path from "node:path";
2-
import type { RulePlugin } from "../core/types";
3-
import { isTestFile } from "../facts/ts-helpers";
4-
import type { DirectoryMetrics } from "../facts/types";
5-
import { delta } from "../rule-delta";
6-
import { average, countMatching, isAssetLikeDirectoryPath, median, ratio } from "./helpers";
2+
import type { RulePlugin } from "../../core/types";
3+
import { isTestFile } from "../../facts/ts-helpers";
4+
import type { DirectoryMetrics } from "../../facts/types";
5+
import { delta } from "../../rule-delta";
6+
import { average, countMatching, isAssetLikeDirectoryPath, median, ratio } from "../shared/helpers";
77

88
/**
99
* Flags directories whose file count is unusually large relative to nearby
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# structure.duplicate-function-signatures
2+
3+
Flags repeated non-test helper shapes that show up across several source files.
4+
5+
- **Family:** `structure`
6+
- **Severity:** `medium`
7+
- **Scope:** `file`
8+
- **Requires:** `repo.duplicateFunctionSignatures`
9+
10+
## How it works
11+
12+
A repo-level fact builds structural fingerprints for function bodies, normalizing local names so copy-pasted helpers still match after superficial renaming.
13+
The rule then projects those duplicate clusters back onto each affected file.
14+
15+
A cluster only counts when it appears in **3 or more files**.
16+
Tiny functions and pass-through wrappers are excluded before clustering, and test files are skipped entirely.
17+
18+
## Flagged example
19+
20+
```ts
21+
// src/users/normalize.ts
22+
export function normalizeUser(input: ApiUser) {
23+
const name = input.name?.trim() ?? "";
24+
const email = input.email?.toLowerCase() ?? "";
25+
return { name, email, active: Boolean(input.active) };
26+
}
27+
28+
// src/teams/normalize.ts
29+
export function normalizeTeamMember(member: ApiMember) {
30+
const name = member.name?.trim() ?? "";
31+
const email = member.email?.toLowerCase() ?? "";
32+
return { name, email, active: Boolean(member.active) };
33+
}
34+
35+
// src/accounts/normalize.ts
36+
export function normalizeAccountOwner(owner: ApiOwner) {
37+
const name = owner.name?.trim() ?? "";
38+
const email = owner.email?.toLowerCase() ?? "";
39+
return { name, email, active: Boolean(owner.active) };
40+
}
41+
```
42+
43+
## Usually ignored
44+
45+
```ts
46+
export function getUser(id: string) {
47+
return loadUser(id);
48+
}
49+
```
50+
51+
Pass-through wrappers are excluded, and a duplicate that only appears in 2 files is below the reporting threshold.
52+
53+
## Scoring
54+
55+
Each duplicate cluster adds `1.25 + 0.5 * (fileCount - 3)` for the current file, capped at `6`.

src/rules/duplicate-function-signatures.ts renamed to src/rules/duplicate-function-signatures/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { RulePlugin } from "../core/types";
2-
import type { DuplicateFunctionIndex } from "../facts/types";
3-
import { isTestFile } from "../facts/ts-helpers";
1+
import type { RulePlugin } from "../../core/types";
2+
import type { DuplicateFunctionIndex } from "../../facts/types";
3+
import { isTestFile } from "../../facts/ts-helpers";
44

55
function findUniqueDuplicateFunctionClusters(
66
duplication: DuplicateFunctionIndex | undefined,

0 commit comments

Comments
 (0)