@rdjperron/lint 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +329 -64
- package/dist/config-CD2iCM6L.mjs +346 -0
- package/dist/config-CD2iCM6L.mjs.map +1 -0
- package/dist/config.d.mts +109 -11
- package/dist/config.mjs +2 -218
- package/dist/index.mjs +44 -10
- package/dist/index.mjs.map +1 -1
- package/dist/repo.d.mts +71 -0
- package/dist/repo.mjs +357 -0
- package/dist/repo.mjs.map +1 -0
- package/dist/verify.d.mts +1 -92
- package/dist/verify.mjs +124 -256
- package/dist/verify.mjs.map +1 -1
- package/fixtures/README.md +17 -24
- package/package.json +30 -17
- package/dist/config.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
# @rdjperron/lint
|
|
2
2
|
|
|
3
3
|
One Oxlint JS plugin, `rdjperron`, with twenty rules against low-evidence code, the config data a
|
|
4
|
-
project spreads into its own `vite.config.ts`,
|
|
4
|
+
project spreads into its own `vite.config.ts`, the check that proves every rule still fires, and
|
|
5
|
+
the repository checks that hold a repo's documents to their own conventions.
|
|
5
6
|
|
|
6
|
-
| Entry
|
|
7
|
-
|
|
|
8
|
-
| `@rdjperron/lint`
|
|
9
|
-
| `@rdjperron/lint/config`
|
|
10
|
-
| `@rdjperron/lint/
|
|
11
|
-
|
|
|
7
|
+
| Entry | What it gives you |
|
|
8
|
+
| ------------------------ | ---------------------------------------------------------------------------------- |
|
|
9
|
+
| `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty rules. |
|
|
10
|
+
| `@rdjperron/lint/config` | `recommended`, the override builders, and the `fmt`, `ignore` and `staged` blocks. |
|
|
11
|
+
| `@rdjperron/lint/repo` | `checkRepo`, five checks on a repo's links, decision register and research names. |
|
|
12
|
+
| `rdjperron-lint-verify` | The command that proves every rule still fires. No config, no arguments. |
|
|
12
13
|
|
|
13
14
|
Seventeen of the rules are a copy of [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop),
|
|
14
|
-
owned and edited here (`src/anti-slop/VENDORED.md` in
|
|
15
|
+
owned and edited here (`src/anti-slop/VENDORED.md` in this repository records the commit). The
|
|
15
16
|
other three are written here.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Measured on vite-plus 0.3.0, 0.3.1, 0.3.2 and 0.3.3, which is oxlint 1.79.0 through 1.83.0
|
|
19
|
+
(2026-09-19). All twenty rules fired on all four.
|
|
18
20
|
|
|
19
21
|
## Install
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
npm install --save-dev --save-exact @rdjperron/lint
|
|
24
|
+
npm install --save-dev --save-exact @rdjperron/lint
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
and not the bundled oxlint version:
|
|
27
|
+
That is the whole installation. `@oxlint/plugins`, which the rules import `defineRule` and the AST
|
|
28
|
+
types from, is an ordinary dependency here and arrives with the package; there is no version of it
|
|
29
|
+
for you to pin or to move.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Pin `@oxlint/plugins` exactly to that number. The peer range here is a caret, so an exact pin
|
|
36
|
-
anywhere in the same major satisfies it. Move the pin every time `vp` moves; `rdjperron-lint-verify`
|
|
37
|
-
fails when the two drift apart.
|
|
31
|
+
`vite-plus` is an optional peer, `>=0.3.0`, so a project on plain oxlint can still load the plugin.
|
|
32
|
+
The floor is `react()`, not the rules: oxlint 1.77.0, which vite-plus 0.2.9 declares, has no
|
|
33
|
+
`react/immutability`, `react/purity` or `react/refs`, and refuses to build a config that names them.
|
|
38
34
|
|
|
39
35
|
## Use it
|
|
40
36
|
|
|
41
37
|
```ts
|
|
42
38
|
// vite.config.ts
|
|
43
|
-
import {
|
|
39
|
+
import { fmt, ignore, react, recommended, staged, tests } from "@rdjperron/lint/config";
|
|
44
40
|
import { defineConfig } from "vite-plus";
|
|
45
41
|
|
|
46
42
|
export default defineConfig({
|
|
@@ -48,22 +44,21 @@ export default defineConfig({
|
|
|
48
44
|
fmt: { ...fmt, ignorePatterns: ignore },
|
|
49
45
|
lint: {
|
|
50
46
|
extends: [recommended],
|
|
51
|
-
plugins: ["typescript", "unicorn", "oxc", "import", "promise", "node"],
|
|
52
|
-
categories: { correctness: "error", suspicious: "warn", perf: "warn" },
|
|
53
47
|
env: { builtin: true, node: true },
|
|
54
48
|
overrides: [
|
|
55
49
|
...react(["app/**"], { useEffectAllowedIn: "hooks/use-mount-effect.ts" }),
|
|
56
50
|
...tests(["**/*.test.ts", "**/*.test.tsx"]),
|
|
57
|
-
...fixtures(),
|
|
58
51
|
],
|
|
59
|
-
options: {
|
|
52
|
+
options: { typeCheck: true },
|
|
60
53
|
ignorePatterns: ignore,
|
|
61
54
|
},
|
|
62
55
|
});
|
|
63
56
|
```
|
|
64
57
|
|
|
65
|
-
`jsPlugins`
|
|
66
|
-
|
|
58
|
+
`jsPlugins`, `plugins`, `categories` and `options.typeAware` need no entry of their own:
|
|
59
|
+
`recommended` carries all four, and they come through `extends`. `options` merges key by key, so
|
|
60
|
+
writing `options: { typeCheck: false }` keeps the inherited `typeAware`. Registering the plugin by
|
|
61
|
+
hand works too, in either form, because it declares its own `meta.name`:
|
|
67
62
|
|
|
68
63
|
```ts
|
|
69
64
|
jsPlugins: ["@rdjperron/lint"];
|
|
@@ -80,57 +75,257 @@ Measured on vite-plus 0.3.1 and oxlint 1.81.0 (2026-09-17), with a base config o
|
|
|
80
75
|
| `jsPlugins`, `rules`, `overrides`, `plugins`, `categories`, `options` | yes, and the consumer's own entries win |
|
|
81
76
|
| `env`, `settings`, `ignorePatterns` | no, the consumer writes its own |
|
|
82
77
|
|
|
83
|
-
So `recommended` carries
|
|
84
|
-
|
|
85
|
-
`ignorePatterns`, not a field you inherit.
|
|
78
|
+
So `recommended` carries everything on the inherited side: the plugin entry, the rules, the plugin
|
|
79
|
+
set, the categories and `options.typeAware`. What is left to you is `env` and `ignorePatterns`;
|
|
80
|
+
`ignore` is an array you spread into `ignorePatterns`, not a field you inherit.
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
`options` is the one field that could read either way, so it was measured on its own (2026-09-19):
|
|
83
|
+
it merges key by key. A base carrying `{ typeAware: true }` and a consumer writing
|
|
84
|
+
`{ typeCheck: false }` leaves `typeAware` on. Only naming `typeAware` yourself overrides it.
|
|
85
|
+
|
|
86
|
+
Two more things oxlint does quietly. An override that names a rule of a plugin the config does not
|
|
87
|
+
enable has to add that plugin in its own `plugins`, or the entry is dropped with no error; a plugin
|
|
88
|
+
already in the top-level set needs no repeating, and an override's `plugins` adds to that set rather
|
|
89
|
+
than replacing it (measured on vite-plus 0.3.1 and oxlint 1.81.0, 2026-09-19). And a relative plugin
|
|
90
|
+
specifier inside an `extends` object is refused; a package name is what works.
|
|
91
91
|
|
|
92
92
|
### The config exports
|
|
93
93
|
|
|
94
|
-
| Export
|
|
95
|
-
|
|
|
96
|
-
| `plugin`
|
|
97
|
-
| `RULE_IDS`, `RuleId`
|
|
98
|
-
| `recommended`
|
|
99
|
-
| `react(files, options?)`
|
|
100
|
-
| `i18n(files, options?)`
|
|
101
|
-
| `expo(files)`
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
94
|
+
| Export | What it is |
|
|
95
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `plugin` | `{ name: "rdjperron", specifier: "@rdjperron/lint" }`. |
|
|
97
|
+
| `RULE_IDS`, `RuleId` | The twenty prefixed ids, as a readonly tuple and as a type. |
|
|
98
|
+
| `recommended` | The config object for `lint.extends`: the plugin entry, the rules below, the built-in choices, the plugin set, the categories and `options.typeAware`. |
|
|
99
|
+
| `react(files, options?)` | The hooks rules, the compiler's rules and the globals. `options.env` is the environment those files run in, `{ browser: true }` by default, so a React Native consumer names its own. `options.useEffectAllowedIn` names the one file allowed to call `useEffect`, puts its name in every finding, and switches both `no-use-effect` and `react/exhaustive-deps` off for it. |
|
|
100
|
+
| `i18n(files, options?)` | `rdjperron/no-hardcoded-jsx-text` at error. `options.pattern` sets the word pattern. |
|
|
101
|
+
| `expo(files, options?)` | The React Native facts: `static-public-env` at error, `typescript/no-require-imports` relaxed for asset extensions, `unicorn/no-array-sort` off. `options.assetExts` adds your `metro.config.js` extensions, `options.reactFiles` adds the two `react/*` rules an Expo app needs. |
|
|
102
|
+
| `restrictedImports(…)` | One `no-restricted-imports` override per file set, expanded from one declaration. See below. |
|
|
103
|
+
| `tests(files)` | `any`, `!`, `no-runtime-typeof` and the safety-comment rule off, with the reason on each. |
|
|
104
|
+
| `decoders(files, options?)` | `no-runtime-typeof` and `no-unknown-parameters` at error everywhere, each hinting at the files in `files`, and `no-runtime-typeof` off inside them. `options.hint` replaces the generated sentence. |
|
|
105
|
+
| `fmt` | `printWidth` 100, double quotes, semicolons, two spaces. |
|
|
106
|
+
| `ignore` | Dependencies, build output, the AI agent directories and the Vitest JSON report. |
|
|
107
|
+
| `staged` | `vp check --fix` on JS and TS sources. |
|
|
107
108
|
|
|
108
109
|
`recommended` puts every rule at `error` except `no-hardcoded-jsx-text` and `static-public-env`,
|
|
109
110
|
which belong to a translated app and to an Expo app and are left to `i18n()` and `expo()`. The
|
|
110
111
|
measurements behind those severities are in the comments beside them.
|
|
111
112
|
|
|
113
|
+
### `restrictedImports`, one declaration for every file set
|
|
114
|
+
|
|
115
|
+
Two overrides matching one file do not merge. The later one replaces the rule's whole entry, so
|
|
116
|
+
every file set needs `no-restricted-imports` written out in full. A repo with ten file sets
|
|
117
|
+
restates the same list ten times, and a new ban has to be added to all ten. Forget one and that
|
|
118
|
+
file set silently keeps importing what the others may not.
|
|
119
|
+
|
|
120
|
+
Here each entry is named once and each scope says which scope it starts `from`, what it `add`s and
|
|
121
|
+
what it `drop`s. Nothing is matched by glob and nothing is guessed: one override comes out per
|
|
122
|
+
scope, in the order you declared them, so you declare broad to narrow.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const RN_TEXT = { name: "react-native", importNames: ["Text"], message: "Text goes through <T role>." };
|
|
126
|
+
const RN_ALERT = { name: "react-native", importNames: ["Alert"], message: "lib/alert.ts answers on both targets." };
|
|
127
|
+
const SCHEMA = { group: ["**/convex/schema"], message: "convex/validators.ts is what the client reads." };
|
|
128
|
+
|
|
129
|
+
...restrictedImports({
|
|
130
|
+
paths: { RN_TEXT, RN_ALERT },
|
|
131
|
+
patterns: { SCHEMA },
|
|
132
|
+
scopes: {
|
|
133
|
+
all: { files: ["**"], use: ["RN_TEXT", "RN_ALERT", "SCHEMA"] },
|
|
134
|
+
kit: { files: ["components/ui/**"], from: "all", drop: ["RN_TEXT"] },
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
That returns two overrides. The first bans all three everywhere, the second bans `RN_ALERT` and
|
|
140
|
+
`SCHEMA` under `components/ui/`, which is where `Text` is wrapped. A ban added to `paths` and to
|
|
141
|
+
`all` reaches every scope that descends from it, and `components/ui/` keeps its one exemption.
|
|
142
|
+
|
|
143
|
+
The broadest scope is an override on `files: ["**"]` rather than a top-level rule. Measured on
|
|
144
|
+
vite-plus 0.3.1 and oxlint 1.81.0, 2026-09-19: the two behave the same, files matched by no other
|
|
145
|
+
override included, so the whole list reads in one place.
|
|
146
|
+
|
|
147
|
+
Severity is `error`, always. Every message is yours.
|
|
148
|
+
|
|
149
|
+
TypeScript rejects four mistakes on the spot:
|
|
150
|
+
|
|
151
|
+
- a name in `use`, `add` or `drop` that is no key of `paths` or `patterns`,
|
|
152
|
+
- a `from` naming a scope that does not exist,
|
|
153
|
+
- `use` and `from` on the same scope,
|
|
154
|
+
- a scope with neither.
|
|
155
|
+
|
|
156
|
+
Three more depend on the order scopes resolve in, so they throw when the config loads, naming the
|
|
157
|
+
scope and the entry:
|
|
158
|
+
|
|
159
|
+
- a `from` naming a scope declared after this one,
|
|
160
|
+
- a `drop` of a name the parent does not restrict,
|
|
161
|
+
- an `add` of a name the parent already restricts.
|
|
162
|
+
|
|
163
|
+
The last two exist because both are a claim about the parent that is false, and a config that
|
|
164
|
+
states something false about its parent is one rename away from lifting a ban.
|
|
165
|
+
|
|
112
166
|
## Prove the rules still fire
|
|
113
167
|
|
|
114
168
|
```bash
|
|
115
169
|
npx rdjperron-lint-verify
|
|
116
170
|
```
|
|
117
171
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
every rule fired on the file named after it
|
|
121
|
-
|
|
122
|
-
|
|
172
|
+
No configuration and no arguments. A rule can go silently dead after a linter upgrade, and "reports
|
|
173
|
+
nothing" looks exactly like "no violations", so nothing else in a repo notices. This asserts a
|
|
174
|
+
positive: every rule fired, each on the file named after it. It prints a table and exits non-zero on
|
|
175
|
+
the first rule that stayed quiet.
|
|
176
|
+
|
|
177
|
+
The fixtures ship in the package, one deliberately violating `fixtures/<rule>.fixture.ts(x)` per
|
|
178
|
+
rule. The command copies them into a throwaway project under your system temp directory, beside a
|
|
179
|
+
generated config that puts every rule at error, with your `node_modules` symlinked in, and removes
|
|
180
|
+
the whole directory afterwards whether the run passed or failed. Your own config, your ignore
|
|
181
|
+
patterns and your severities take no part.
|
|
182
|
+
|
|
183
|
+
The linter it runs is the oxlint your local `vite-plus` depends on, so the run that matters is the
|
|
184
|
+
one right after you move `vite-plus`, and in CI.
|
|
185
|
+
|
|
186
|
+
One thing not to do: do not delete the fixtures. They are the only thing that catches a rule going
|
|
187
|
+
dead.
|
|
188
|
+
|
|
189
|
+
## Repository checks
|
|
190
|
+
|
|
191
|
+
`@rdjperron/lint/repo` is for a repo that keeps a decision register in `docs/decisions.md` and
|
|
192
|
+
dated research in `_research/`. Prose can state those conventions and cannot enforce them. `checkRepo`
|
|
193
|
+
reads the tree and returns what breaks them, so a test fails on a dead link or on a citation of a
|
|
194
|
+
retired decision.
|
|
195
|
+
|
|
196
|
+
It is plain Node and imports no test runner. You call it from a test of your own:
|
|
123
197
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
198
|
+
```ts
|
|
199
|
+
// lib/repo.test.ts
|
|
200
|
+
import { fileURLToPath } from "node:url";
|
|
201
|
+
|
|
202
|
+
import { CHECKS, checkRepo, HINTS, TITLES } from "@rdjperron/lint/repo";
|
|
203
|
+
import { describe, expect, it } from "vitest";
|
|
204
|
+
|
|
205
|
+
const faults = checkRepo({
|
|
206
|
+
root: fileURLToPath(new URL("..", import.meta.url)),
|
|
207
|
+
codeRoots: ["app", "components", "lib", "convex", "e2e"],
|
|
208
|
+
linked: ["e2e/README.md"],
|
|
209
|
+
});
|
|
128
210
|
|
|
129
|
-
|
|
130
|
-
|
|
211
|
+
describe("the tree", () => {
|
|
212
|
+
for (const check of CHECKS) {
|
|
213
|
+
it(TITLES[check], () => {
|
|
214
|
+
expect(faults[check], HINTS[check]).toEqual([]);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
```
|
|
131
219
|
|
|
132
|
-
|
|
133
|
-
|
|
220
|
+
Loop over `CHECKS` rather than listing the checks by hand. A check added in a later version then
|
|
221
|
+
runs in your repo the day you move the pin, and nobody has to remember to assert it.
|
|
222
|
+
|
|
223
|
+
`checkRepo` returns one array of faults per check, empty where the check holds. A fault is one
|
|
224
|
+
line that opens with a path relative to the root, with the line number when the fault has one:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
README.md:63 points at docs/not-there.md, which is not there
|
|
228
|
+
src/main.tsx:43 cites D16, which is retired
|
|
229
|
+
_research/notes.md is not named YYYYMMDD.HHMM.topic.md or YYYYMMDD.topic/
|
|
230
|
+
src, named by `codeRoots`, is not there
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`TITLES` holds what each check promises, for the test's name. `HINTS` holds one sentence per check
|
|
234
|
+
saying how to fix it, which the example passes to `expect` as the failure message. `CHECKS` is the
|
|
235
|
+
five names: `scans`, `links`, `register`, `citations`, `research`.
|
|
236
|
+
|
|
237
|
+
`checkRepo` does not throw on a tree or a config it cannot read. A code root that is a file, a
|
|
238
|
+
register that is a directory, a symbolic link that leads nowhere and a root that does not exist
|
|
239
|
+
are each a fault in `scans`, and a link whose percent-escapes do not decode is a fault in `links`.
|
|
240
|
+
|
|
241
|
+
### What each check holds
|
|
242
|
+
|
|
243
|
+
| Check | What it holds |
|
|
244
|
+
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| `links` | Every relative Markdown link in the living documents and in each research folder's `README.md` points at a path that exists. A URL with a scheme, a `//host` and a bare `#anchor` are skipped, and an anchor after a path is dropped before the path is tested. |
|
|
246
|
+
| `register` | Every row is one line, `- **Dn** Sentence. (YYYY-MM-DD)` or `- **Dn** Sentence. (YYYY-MM-DD, source)`. Each number is given once and is not also in the retired list. No number is missing up to the highest ever given. No other line names a number in bold. A `## Retired` section exists, and it may name no number yet. |
|
|
247
|
+
| `citations` | Every `D<n>` in the scanned files, and in the register's own rows after their number, is a live row. A retired number fails as retired and a number never given fails as unknown. |
|
|
248
|
+
| `research` | Each entry of `_research/` is a file `YYYYMMDD.HHMM.topic.md` or a folder `YYYYMMDD.topic/` that holds a `README.md`. The topic is lowercase letters, digits and hyphens. |
|
|
249
|
+
| `scans` | Every path the config names is there and is the right kind, a file or a directory, defaults included. Every code root yields a file to scan. The register has a row. No symbolic link a walk meets leads nowhere. |
|
|
250
|
+
|
|
251
|
+
`scans` exists because a check that reads nothing passes. Rename `lib/` to `src/` without touching
|
|
252
|
+
the config and the citation scan would cover one directory fewer and stay green. With `scans` the
|
|
253
|
+
missing root is a fault.
|
|
254
|
+
|
|
255
|
+
`scans` holds paths and files, not matches. A repo with no research directory, an empty one, no
|
|
256
|
+
relative link or no citation yet is clean, so a repository can turn the test on from its first
|
|
257
|
+
commit. The one path that may be absent is the research directory.
|
|
258
|
+
|
|
259
|
+
The living documents are `README.md`, `AGENTS.md` and `BACKLOG.md` at the root, every `.md` file
|
|
260
|
+
under `docs/`, and the documents `linked` names. Their links are held and they are scanned for
|
|
261
|
+
citations, the register excepted. The citation scan also reads every file under a code root with a
|
|
262
|
+
scanned extension, and every file at the root with one. A lockfile is never scanned, meaning a
|
|
263
|
+
name that ends in `-lock.json`, `-lock.yaml` or the like.
|
|
264
|
+
|
|
265
|
+
The research is frozen once written, so the checks read little of it. The body of a research
|
|
266
|
+
entry is checked for neither links nor citations, since it names paths and numbers as they stood on
|
|
267
|
+
its date. Only a research folder's `README.md` has its links held, and nothing under the research
|
|
268
|
+
directory is scanned for citations, even if you name it as a code root or keep it under `docs/`.
|
|
269
|
+
The retired numbers are every `D<n>` in the `## Retired` section, up to the next `##` heading. The
|
|
270
|
+
prose around the register's rows may name any number.
|
|
271
|
+
|
|
272
|
+
When the register cannot be read, `scans` says so once, and `register` and `citations` stay empty
|
|
273
|
+
rather than failing every citation in the repo.
|
|
274
|
+
|
|
275
|
+
### The config
|
|
276
|
+
|
|
277
|
+
Every path is relative to the root, and `lib`, `./lib` and `lib/` name the same one.
|
|
278
|
+
|
|
279
|
+
| Field | Default | What it is |
|
|
280
|
+
| ------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `root` | required | The repository root. A relative one is resolved against the working directory. |
|
|
282
|
+
| `codeRoots` | required | The directories scanned for citations. `["."]` scans the whole root. |
|
|
283
|
+
| `extensions` | `ts` `tsx` `mts` `cts` `js` `jsx` `mjs` `cjs` `md` `json` `jsonc` `yml` `yaml` `html` | The extensions scanned for citations. Yours replace the default list. |
|
|
284
|
+
| `skip` | `node_modules` `_generated` `dist` | Names no walk enters, wherever they sit. Yours are added to the default list. |
|
|
285
|
+
| `leftOut` | none | Paths no walk enters, as `{ path: reason }`. The reason is for the reader. |
|
|
286
|
+
| `documents` | `README.md` `AGENTS.md` `BACKLOG.md` | The living documents at the root. Yours replace the default list, so a repo with no backlog names the two it has. |
|
|
287
|
+
| `docs` | `docs` | The directory whose every `.md` file is a living document. |
|
|
288
|
+
| `linked` | none | More living documents, such as `e2e/README.md`. |
|
|
289
|
+
| `register` | `decisions.md` in the `docs` directory | The decision register. It follows `docs`: with `docs: "handbook"` it is `handbook/decisions.md`. |
|
|
290
|
+
| `research` | `_research` | The research directory. It may be absent. |
|
|
291
|
+
|
|
292
|
+
`skip` matches a name at any depth, so `skip: ["fixtures"]` leaves out every `fixtures` directory.
|
|
293
|
+
`leftOut` matches one path, for a directory whose name is legitimate elsewhere:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
leftOut: { "site/.well-known": "certificate fingerprints read like decision numbers" },
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Both act on what a walk finds under `docs`, under a code root and at the root. Neither removes a
|
|
300
|
+
document you name in `documents` or `linked`. To stop holding one of those, take it off its list.
|
|
301
|
+
|
|
302
|
+
A citation is any `D` followed by digits and standing as a word of its own, so recorded data full
|
|
303
|
+
of ids such as `D12` is the usual reason to leave a path out. Leave it out with `skip` or
|
|
304
|
+
`leftOut`, or keep its extension off the list, as with `csv`.
|
|
305
|
+
|
|
306
|
+
`DEFAULT_DOCUMENTS`, `DEFAULT_EXTENSIONS` and `DEFAULT_SKIP` are exported, for a config that adds
|
|
307
|
+
to a list the field replaces.
|
|
308
|
+
|
|
309
|
+
### What the checks do not do
|
|
310
|
+
|
|
311
|
+
They hold form, not truth. A link that resolves may point at the wrong document. A row may be well
|
|
312
|
+
formed and state a rule the code broke last week. A citation of a live number may cite the wrong
|
|
313
|
+
rule. A research entry with a correct name may be empty.
|
|
314
|
+
|
|
315
|
+
They also leave these alone:
|
|
316
|
+
|
|
317
|
+
- An anchor is not checked against the headings of its target.
|
|
318
|
+
- A link written as bare text, as `<path>`, inside an HTML tag, with a title in single quotes or
|
|
319
|
+
with parentheses in its path is not read as a link, or is read wrong.
|
|
320
|
+
- A link that opens with `/` is tested against the filesystem root, not the repository's.
|
|
321
|
+
- The date in a row or in a research name is not checked to be a real date.
|
|
322
|
+
- `**D11.**` and `**D12a**` are neither a row nor row-like, so the register check does not see them.
|
|
323
|
+
- `#D12` and `-D16` count as citations, since the `D` still opens a word.
|
|
324
|
+
- Any file directly under the research directory that is not a dated note is a fault, a
|
|
325
|
+
`README.md` or a `.DS_Store` included.
|
|
326
|
+
- A linked directory is not entered, since it may lead back to one of its own parents.
|
|
327
|
+
|
|
328
|
+
Reading the rows against the code is still a review's job.
|
|
134
329
|
|
|
135
330
|
## The rules
|
|
136
331
|
|
|
@@ -167,12 +362,82 @@ scope it off; the messages, the options and the cases are in the rule and its te
|
|
|
167
362
|
- `rdjperron/no-hardcoded-jsx-text` takes `{ pattern?: string }`, a regex source compiled with `u`. The
|
|
168
363
|
default `\p{L}{3,}` says three letters in a row is a word; digits, separators and bullets are
|
|
169
364
|
not. It looks at JSX text and at string literals placed as children, never at attribute values.
|
|
170
|
-
- `rdjperron/no-runtime-typeof` takes `{ allowInTypeGuards?: boolean }`. `recommended`
|
|
171
|
-
`typeof` inside a type predicate is the decode the rule asks for, named and
|
|
365
|
+
- `rdjperron/no-runtime-typeof` takes `{ allowInTypeGuards?: boolean, hint?: string }`. `recommended`
|
|
366
|
+
turns the first on: a `typeof` inside a type predicate is the decode the rule asks for, named and
|
|
367
|
+
reusable. The hint is appended to every finding; `decoders(files)` sets it.
|
|
368
|
+
- `rdjperron/no-unknown-parameters` takes `{ hint?: string }`, appended to every finding, so a project
|
|
369
|
+
can name the modules where the parse belongs. `decoders(files)` sets it.
|
|
172
370
|
- `rdjperron/static-public-env` has no options.
|
|
173
371
|
|
|
372
|
+
## Name the file that holds the fix
|
|
373
|
+
|
|
374
|
+
A rule tells a reader what is wrong. When the fix is the same every time, the finding can tell them
|
|
375
|
+
where to go as well, which is what `useEffectAllowedIn` and `decoders(files)` are for: they put a
|
|
376
|
+
sentence naming the sanctioned file in every finding of the rule that will send someone there.
|
|
377
|
+
`decoders(["lib/api-error.ts", "convex/lib/json.ts"])` reads:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
Parameter `body` leaves input unparsed. Accept a named domain type; run the expected schema or
|
|
381
|
+
parser at the I/O boundary before calling this function. Decode the value in lib/api-error.ts or
|
|
382
|
+
convex/lib/json.ts, then call that decoder from here.
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The same sentence lands on `no-runtime-typeof`, and inside those files that rule is off, because a
|
|
386
|
+
`typeof` there is the parse rather than a narrowing in place of one.
|
|
387
|
+
|
|
174
388
|
## Severities are measurements
|
|
175
389
|
|
|
176
390
|
Land a new rule at `warn`, fix or scope what fires, then promote it. When a rule fires, fix the
|
|
177
391
|
code or add a narrowly scoped override with the measurement that justifies it. Never weaken the
|
|
178
392
|
rule, add a cast, or rename a symbol to dodge the check.
|
|
393
|
+
|
|
394
|
+
## Moving from 0.5
|
|
395
|
+
|
|
396
|
+
Nothing is removed and nothing changes. 0.6.0 adds `@rdjperron/lint/repo`. A repo that carries the
|
|
397
|
+
checks as a test file of its own replaces that file with the example under "Repository checks",
|
|
398
|
+
and keeps only its config.
|
|
399
|
+
|
|
400
|
+
## Moving from 0.4
|
|
401
|
+
|
|
402
|
+
Nothing is removed. One builder changed what it does, and three are new.
|
|
403
|
+
|
|
404
|
+
`expo(files)` used to set `static-public-env` and nothing else. It now also puts
|
|
405
|
+
`typescript/no-require-imports` at error with the asset extensions allowed, and turns
|
|
406
|
+
`unicorn/no-array-sort` off. If your config carries either by hand, delete it, and pass
|
|
407
|
+
`assetExts` for what your `metro.config.js` adds. An asset `require()` is now allowed only in the
|
|
408
|
+
files you hand to `expo()`. If you allowed it everywhere before, a `require()` of an asset outside
|
|
409
|
+
those files reports.
|
|
410
|
+
|
|
411
|
+
| You write by hand today | 0.5.0 gives you |
|
|
412
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------- |
|
|
413
|
+
| The same `no-restricted-imports` list in every override, minus one entry | `restrictedImports({ paths, patterns, scopes })` |
|
|
414
|
+
| `no-runtime-typeof` off for your decoder modules | `decoders(files)`, which also names them in the fix |
|
|
415
|
+
| `react/capitalized-calls` off, `react/style-prop-object` for `StatusBar` | `expo(files, { reactFiles })` |
|
|
416
|
+
| `react/exhaustive-deps` off for the `useEffectAllowedIn` file | `react()` does it |
|
|
417
|
+
|
|
418
|
+
Measured on rummy, 2026-09-19: `vite.config.ts` went from 447 lines to 366. 132 planted findings
|
|
419
|
+
across ten import scopes came out identical, position for position, before and after. The one
|
|
420
|
+
difference is the asset rule, which moved from warn to error and stopped allowing assets outside
|
|
421
|
+
the phone's files.
|
|
422
|
+
|
|
423
|
+
## Moving from 0.3
|
|
424
|
+
|
|
425
|
+
0.4.0 moved the rules' health out of the consumer. Everything below is something you delete, and
|
|
426
|
+
nothing replaces it.
|
|
427
|
+
|
|
428
|
+
| Delete | Why it is gone |
|
|
429
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
430
|
+
| `@oxlint/plugins` from your `package.json` | An ordinary dependency here now. Nothing left to pin, and nothing to move on a `vp` upgrade. |
|
|
431
|
+
| `plugins: [...]` from `lint` | `recommended` carries the set its rules need. Keep yours only if you add a plugin of your own. |
|
|
432
|
+
| `categories: {...}` from `lint` | `recommended` carries `correctness: "error"`, `suspicious: "warn"`, `perf: "warn"`. |
|
|
433
|
+
| `typeAware: true` from `lint.options` | `recommended` carries it, and `options` merges key by key, so your `typeCheck` still applies. |
|
|
434
|
+
| `fixtures()` from your imports and your overrides | The verify command writes its own config now. The builder no longer exists. |
|
|
435
|
+
| Any comment about keeping `scripts/.lint-fixtures` un-ignored | The check runs under your system temp directory, where nothing is ignored. |
|
|
436
|
+
| Your `verify:lint-fixtures` script, if it passed arguments | `rdjperron-lint-verify` takes none. |
|
|
437
|
+
|
|
438
|
+
Two exports also went: `@rdjperron/lint/verify` (`verifySuites`, `packageSuite`,
|
|
439
|
+
`verifyPluginApiPin`) and `@rdjperron/lint/fixtures/*`. The fixtures still ship, the bin reads them
|
|
440
|
+
itself, and a repo with a second plugin to check now runs that plugin's own command beside this one.
|
|
441
|
+
|
|
442
|
+
New in 0.4.0: `oxc/no-accumulating-spread` at error, measured at zero hits on three repos. If it
|
|
443
|
+
fires on yours, that reducer is quadratic.
|