Skip to content

Commit 0f525cb

Browse files
authored
Add fix guidance to rule READMEs (#23)
1 parent 39725b2 commit 0f525cb

15 files changed

Lines changed: 313 additions & 0 deletions

File tree

src/rules/async-noise/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ async function getJson(url: string) {
4141
}
4242
```
4343

44+
## How to fix / do this better
45+
46+
Prefer one of these instead:
47+
48+
- remove `async` entirely when the function is just forwarding a promise
49+
- remove redundant `await` when you are immediately returning the awaited value
50+
- keep the wrapper only if it adds real behavior such as validation, normalization, retries, metrics, or error context
51+
52+
```ts
53+
function getUser(id: string) {
54+
return fetchUser(id);
55+
}
56+
57+
async function loadUser(id: string) {
58+
const user = await fetchUser(id);
59+
return normalizeUser(user);
60+
}
61+
```
62+
63+
The goal is not "never use async". It is to avoid wrapper ceremony that makes the call graph larger without making behavior clearer.
64+
4465
## Scoring
4566

4667
Redundant `return await` sites add `1.5` each.

src/rules/barrel-density/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ export function createStore() {
3636
export { type Store } from "./types";
3737
```
3838

39+
## How to fix / do this better
40+
41+
Prefer barrels only when they improve discoverability without hiding module boundaries.
42+
43+
Better options:
44+
45+
- keep a barrel small and intentional
46+
- export a stable public surface from one place, but avoid creating layers of barrel-to-barrel indirection
47+
- import directly from the implementation module when a barrel adds little value
48+
49+
```ts
50+
export { createStore } from "./store";
51+
export { type Store } from "./types";
52+
```
53+
54+
If a file is just a wide list of re-exports, ask whether it is actually helping API design or only adding another place to chase symbols through.
55+
3956
## Scoring
4057

4158
The score starts at `1` and adds `0.5` per re-export statement, capped at `3`.

src/rules/directory-fanout-hotspot/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ src/icons/
4747

4848
Asset-like buckets and test-matrix directories are intentionally suppressed because wide directory shapes are expected there.
4949

50+
## How to fix / do this better
51+
52+
A wide directory is usually a sign that one of these is missing:
53+
54+
- a stronger domain split
55+
- a deeper subdirectory boundary
56+
- a more cohesive module with fewer one-file-per-concept fragments
57+
58+
Better patterns:
59+
60+
- group related files into subdomains once a folder becomes a grab bag
61+
- merge ultra-thin files when the split adds naming overhead but not conceptual clarity
62+
- separate generated output from hand-written source when possible
63+
64+
```text
65+
src/
66+
└── billing/
67+
├── invoices/
68+
├── subscriptions/
69+
└── shared/
70+
```
71+
72+
The goal is not tiny directories everywhere. It is to avoid a single hotspot folder becoming the dumping ground for too many loosely related files.
73+
5074
## Scoring
5175

5276
The rule starts at `2` and adds a bounded amount based on how far the directory is above the computed threshold.

src/rules/duplicate-function-signatures/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ export function getUser(id: string) {
5050

5151
Pass-through wrappers are excluded, and a duplicate that only appears in 2 files is below the reporting threshold.
5252

53+
## How to fix / do this better
54+
55+
When the same helper shape appears across multiple files, prefer one of these:
56+
57+
- extract the shared logic into a single reusable helper
58+
- create a small configurable normalizer instead of copy-pasting near-identical functions
59+
- keep duplication only when the domain concepts are truly diverging and deserve separate behavior
60+
61+
```ts
62+
function normalizePersonLike(input: { name?: string; email?: string; active?: boolean }) {
63+
return {
64+
name: input.name?.trim() ?? "",
65+
email: input.email?.toLowerCase() ?? "",
66+
active: Boolean(input.active),
67+
};
68+
}
69+
```
70+
71+
The point is not to eliminate all repetition. It is to avoid silent copy-paste drift when several files are maintaining the same logic independently.
72+
5373
## Scoring
5474

5575
Each duplicate cluster adds `1.25 + 0.5 * (fileCount - 3)` for the current file, capped at `6`.

src/rules/duplicate-mock-setup/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ vi.clearAllMocks();
3939

4040
Generic mock declarations and cleanup-only statements do not contribute to this rule.
4141

42+
## How to fix / do this better
43+
44+
When the same mock setup keeps reappearing, prefer shared test helpers over repeating the setup inline.
45+
46+
Better options:
47+
48+
- move repeated mock wiring into a factory or fixture helper
49+
- centralize common setup in `beforeEach` when it is truly shared
50+
- expose small scenario builders so tests vary only the interesting values
51+
52+
```ts
53+
function mockUserFetch(overrides: Partial<User> = {}) {
54+
vi.mocked(api.fetchUser).mockResolvedValue({ id: 1, name: "Ada", ...overrides });
55+
}
56+
```
57+
58+
That keeps test intent focused on what changes per case instead of duplicating the same mock plumbing in every file.
59+
4260
## Scoring
4361

4462
Each duplicate setup cluster adds `1 + 0.5 * (fileCount - 2)` for the current file, capped at `5`.

src/rules/empty-catch/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ export function loadTheme() {
4444
}
4545
```
4646

47+
## How to fix / do this better
48+
49+
An empty catch should usually become one of these instead:
50+
51+
- rethrow the error
52+
- return a deliberate typed fallback with a comment explaining the boundary behavior
53+
- log meaningful context and then rethrow
54+
- validate earlier so the exceptional path is narrower and more intentional
55+
56+
```ts
57+
export function parseConfig(raw: string) {
58+
try {
59+
return JSON.parse(raw);
60+
} catch (error) {
61+
throw new Error("Invalid config JSON", { cause: error });
62+
}
63+
}
64+
```
65+
66+
If swallowing the error is truly intentional, document why the fallback is safe and keep the scope local.
67+
4768
## Scoring
4869

4970
Each flagged catch uses the shared try/catch scoring helper, then the file total is capped at `8`.

src/rules/error-obscuring/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,28 @@ export function readConfig(raw: string) {
5050
}
5151
```
5252

53+
## How to fix / do this better
54+
55+
Prefer preserving failure meaning instead of replacing it with a cheap fallback.
56+
57+
Better patterns:
58+
59+
- rethrow the original error
60+
- wrap with context while preserving `cause`
61+
- return a deliberate result type that makes the failure explicit instead of pretending the operation succeeded
62+
63+
```ts
64+
export function loadProfile(id: string) {
65+
try {
66+
return fetchProfile(id);
67+
} catch (error) {
68+
throw new Error(`Failed to load profile ${id}`, { cause: error });
69+
}
70+
}
71+
```
72+
73+
If you truly need a fallback value, keep it narrow, document why it is safe, and avoid erasing the original failure in code paths that still need diagnosis.
74+
5375
## Scoring
5476

5577
Each flagged catch uses the shared try/catch scoring helper, then the file total is capped at `8`.

src/rules/error-swallowing/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ export async function syncUser(id: string) {
3939
}
4040
```
4141

42+
## How to fix / do this better
43+
44+
Logging is not a substitute for control flow.
45+
If the caller still needs to know the operation failed, prefer one of these:
46+
47+
- log and rethrow
48+
- return an explicit result type such as `{ ok: false, error }`
49+
- handle the failure completely at this layer only when you can prove continuing is safe
50+
51+
```ts
52+
export async function syncUser(id: string) {
53+
try {
54+
await pushUser(id);
55+
} catch (error) {
56+
logger.error({ error, id }, "failed to sync user");
57+
throw error;
58+
}
59+
}
60+
```
61+
62+
The key is to make failure visible in the API contract instead of only visible in logs.
63+
4264
## Scoring
4365

4466
Each flagged catch uses the shared try/catch scoring helper, then the file total is capped at `8`.

src/rules/generic-record-casts/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,32 @@ const token = value as { token: string };
4040
const metadata = input as Map<string, string>;
4141
```
4242

43+
## How to fix / do this better
44+
45+
Treat unknown input as unknown for longer, then validate or narrow it at the boundary.
46+
47+
Better options:
48+
49+
- parse into a real domain type with a schema or decoder
50+
- keep the value as `unknown` until you prove the fields you need
51+
- use a very local cast only when you immediately narrow and contain it
52+
53+
```ts
54+
const input: unknown = JSON.parse(raw);
55+
const parsed = UserConfigSchema.parse(input);
56+
```
57+
58+
Or, without a schema library:
59+
60+
```ts
61+
const input: unknown = JSON.parse(raw);
62+
if (!isUserConfig(input)) {
63+
throw new Error("Invalid user config");
64+
}
65+
```
66+
67+
The goal is to avoid turning uncertain external data into a roaming generic object bag that downstream code has to keep guessing about.
68+
4369
## Scoring
4470

4571
Each generic record cast adds `2` points.

src/rules/generic-status-envelopes/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ return { success: true, user };
3535
return { error: "missing" };
3636
```
3737

38+
## How to fix / do this better
39+
40+
Prefer API shapes that express the actual domain outcome instead of wrapping everything in a shallow boolean envelope.
41+
42+
Better options:
43+
44+
- return the domain object directly on success
45+
- use typed result variants when callers really need success/failure branching
46+
- model specific failure cases instead of pushing everything into generic `message` / `error` strings
47+
48+
```ts
49+
type CreateRepoResult =
50+
| { kind: "created"; repository: Repository }
51+
| { kind: "forbidden" }
52+
| { kind: "conflict"; reason: string };
53+
```
54+
55+
A small `{ ok, data }` wrapper is sometimes fine, but if it becomes the default shape for every operation it usually means the API is describing transport status rather than domain meaning.
56+
3857
## Scoring
3958

4059
Each generic status envelope adds `2` points.

0 commit comments

Comments
 (0)