@rdjperron/lint 0.2.0 → 0.5.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 +210 -93
- package/dist/config-CD2iCM6L.mjs +346 -0
- package/dist/config-CD2iCM6L.mjs.map +1 -0
- package/dist/config.d.mts +121 -14
- package/dist/config.mjs +2 -218
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +94 -26
- package/dist/index.mjs.map +1 -1
- package/dist/verify.d.mts +1 -92
- package/dist/verify.mjs +125 -257
- package/dist/verify.mjs.map +1 -1
- package/fixtures/README.md +18 -25
- package/fixtures/no-array-filter-map.fixture.ts +1 -1
- package/fixtures/no-chained-type-assertions.fixture.ts +1 -1
- package/fixtures/no-conditional-empty-object-spread.fixture.ts +1 -1
- package/fixtures/no-hardcoded-jsx-text.fixture.tsx +1 -1
- package/fixtures/no-known-value-widening.fixture.ts +1 -1
- package/fixtures/no-module-mocking.fixture.ts +1 -1
- package/fixtures/no-object-parameters.fixture.ts +1 -1
- package/fixtures/no-reduce-accumulator-copy.fixture.ts +1 -1
- package/fixtures/no-reflect-apply.fixture.ts +1 -1
- package/fixtures/no-reflect-get.fixture.ts +1 -1
- package/fixtures/no-runtime-typeof.fixture.ts +1 -1
- package/fixtures/no-shape-in-symbol-names.fixture.ts +1 -1
- package/fixtures/no-unknown-parameters.fixture.ts +1 -1
- package/fixtures/no-unknown-returns.fixture.ts +1 -1
- package/fixtures/no-unknown-type-aliases.fixture.ts +1 -1
- package/fixtures/no-unsafe-dictionary-type.fixture.ts +1 -1
- package/fixtures/no-use-effect.fixture.tsx +1 -1
- package/fixtures/no-widen-then-assert.fixture.ts +1 -1
- package/fixtures/require-safety-comment-for-type-assertion.fixture.ts +1 -1
- package/fixtures/static-public-env.fixture.ts +1 -1
- package/package.json +28 -20
- package/dist/config.mjs.map +0 -1
- package/docs/rules/README.md +0 -43
- package/docs/rules/no-array-filter-map.md +0 -60
- package/docs/rules/no-chained-type-assertions.md +0 -52
- package/docs/rules/no-conditional-empty-object-spread.md +0 -50
- package/docs/rules/no-hardcoded-jsx-text.md +0 -52
- package/docs/rules/no-known-value-widening.md +0 -62
- package/docs/rules/no-module-mocking.md +0 -53
- package/docs/rules/no-object-parameters.md +0 -55
- package/docs/rules/no-reduce-accumulator-copy.md +0 -57
- package/docs/rules/no-reflect-apply.md +0 -56
- package/docs/rules/no-reflect-get.md +0 -51
- package/docs/rules/no-runtime-typeof.md +0 -61
- package/docs/rules/no-shape-in-symbol-names.md +0 -53
- package/docs/rules/no-unknown-parameters.md +0 -55
- package/docs/rules/no-unknown-returns.md +0 -61
- package/docs/rules/no-unknown-type-aliases.md +0 -52
- package/docs/rules/no-unsafe-dictionary-type.md +0 -56
- package/docs/rules/no-use-effect.md +0 -55
- package/docs/rules/no-widen-then-assert.md +0 -53
- package/docs/rules/require-safety-comment-for-type-assertion.md +0 -59
- package/docs/rules/static-public-env.md +0 -55
package/README.md
CHANGED
|
@@ -1,46 +1,40 @@
|
|
|
1
1
|
# @rdjperron/lint
|
|
2
2
|
|
|
3
|
-
One Oxlint JS plugin, `
|
|
3
|
+
One Oxlint JS plugin, `rdjperron`, with twenty rules against low-evidence code, the config data a
|
|
4
4
|
project spreads into its own `vite.config.ts`, and the check that proves every rule still fires.
|
|
5
5
|
|
|
6
|
-
| Entry
|
|
7
|
-
|
|
|
8
|
-
| `@rdjperron/lint`
|
|
9
|
-
| `@rdjperron/lint/config`
|
|
10
|
-
|
|
|
11
|
-
| `@rdjperron/lint/fixtures/*` | One deliberately violating file per rule, the input the verify command lints. |
|
|
6
|
+
| Entry | What it gives you |
|
|
7
|
+
| ------------------------ | ---------------------------------------------------------------------------------- |
|
|
8
|
+
| `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty rules. |
|
|
9
|
+
| `@rdjperron/lint/config` | `recommended`, the override builders, and the `fmt`, `ignore` and `staged` blocks. |
|
|
10
|
+
| `rdjperron-lint-verify` | The command that proves every rule still fires. No config, no arguments. |
|
|
12
11
|
|
|
13
12
|
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
|
|
13
|
+
owned and edited here (`src/anti-slop/VENDORED.md` in this repository records the commit). The
|
|
15
14
|
other three are written here.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
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
|
|
17
|
+
(2026-09-19). All twenty rules fired on all four.
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install --save-dev --save-exact @rdjperron/lint
|
|
22
|
+
npm install --save-dev --save-exact @rdjperron/lint
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
and not the bundled oxlint version:
|
|
25
|
+
That is the whole installation. `@oxlint/plugins`, which the rules import `defineRule` and the AST
|
|
26
|
+
types from, is an ordinary dependency here and arrives with the package; there is no version of it
|
|
27
|
+
for you to pin or to move.
|
|
29
28
|
|
|
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; `rdj-lint-verify`
|
|
37
|
-
fails when the two drift apart.
|
|
29
|
+
`vite-plus` is an optional peer, `>=0.3.0`, so a project on plain oxlint can still load the plugin.
|
|
30
|
+
The floor is `react()`, not the rules: oxlint 1.77.0, which vite-plus 0.2.9 declares, has no
|
|
31
|
+
`react/immutability`, `react/purity` or `react/refs`, and refuses to build a config that names them.
|
|
38
32
|
|
|
39
33
|
## Use it
|
|
40
34
|
|
|
41
35
|
```ts
|
|
42
36
|
// vite.config.ts
|
|
43
|
-
import {
|
|
37
|
+
import { fmt, ignore, react, recommended, staged, tests } from "@rdjperron/lint/config";
|
|
44
38
|
import { defineConfig } from "vite-plus";
|
|
45
39
|
|
|
46
40
|
export default defineConfig({
|
|
@@ -48,26 +42,25 @@ export default defineConfig({
|
|
|
48
42
|
fmt: { ...fmt, ignorePatterns: ignore },
|
|
49
43
|
lint: {
|
|
50
44
|
extends: [recommended],
|
|
51
|
-
plugins: ["typescript", "unicorn", "oxc", "import", "promise", "node"],
|
|
52
|
-
categories: { correctness: "error", suspicious: "warn", perf: "warn" },
|
|
53
45
|
env: { builtin: true, node: true },
|
|
54
46
|
overrides: [
|
|
55
47
|
...react(["app/**"], { useEffectAllowedIn: "hooks/use-mount-effect.ts" }),
|
|
56
48
|
...tests(["**/*.test.ts", "**/*.test.tsx"]),
|
|
57
|
-
...fixtures(),
|
|
58
49
|
],
|
|
59
|
-
options: {
|
|
50
|
+
options: { typeCheck: true },
|
|
60
51
|
ignorePatterns: ignore,
|
|
61
52
|
},
|
|
62
53
|
});
|
|
63
54
|
```
|
|
64
55
|
|
|
65
|
-
`jsPlugins`
|
|
66
|
-
|
|
56
|
+
`jsPlugins`, `plugins`, `categories` and `options.typeAware` need no entry of their own:
|
|
57
|
+
`recommended` carries all four, and they come through `extends`. `options` merges key by key, so
|
|
58
|
+
writing `options: { typeCheck: false }` keeps the inherited `typeAware`. Registering the plugin by
|
|
59
|
+
hand works too, in either form, because it declares its own `meta.name`:
|
|
67
60
|
|
|
68
61
|
```ts
|
|
69
62
|
jsPlugins: ["@rdjperron/lint"];
|
|
70
|
-
jsPlugins: [{ name: "
|
|
63
|
+
jsPlugins: [{ name: "rdjperron", specifier: "@rdjperron/lint" }];
|
|
71
64
|
```
|
|
72
65
|
|
|
73
66
|
### What `extends` carries, and what it does not
|
|
@@ -80,98 +73,222 @@ Measured on vite-plus 0.3.1 and oxlint 1.81.0 (2026-09-17), with a base config o
|
|
|
80
73
|
| `jsPlugins`, `rules`, `overrides`, `plugins`, `categories`, `options` | yes, and the consumer's own entries win |
|
|
81
74
|
| `env`, `settings`, `ignorePatterns` | no, the consumer writes its own |
|
|
82
75
|
|
|
83
|
-
So `recommended` carries
|
|
84
|
-
|
|
85
|
-
`ignorePatterns`, not a field you inherit.
|
|
76
|
+
So `recommended` carries everything on the inherited side: the plugin entry, the rules, the plugin
|
|
77
|
+
set, the categories and `options.typeAware`. What is left to you is `env` and `ignorePatterns`;
|
|
78
|
+
`ignore` is an array you spread into `ignorePatterns`, not a field you inherit.
|
|
79
|
+
|
|
80
|
+
`options` is the one field that could read either way, so it was measured on its own (2026-09-19):
|
|
81
|
+
it merges key by key. A base carrying `{ typeAware: true }` and a consumer writing
|
|
82
|
+
`{ typeCheck: false }` leaves `typeAware` on. Only naming `typeAware` yourself overrides it.
|
|
86
83
|
|
|
87
|
-
Two more things oxlint does quietly. An override that
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
Two more things oxlint does quietly. An override that names a rule of a plugin the config does not
|
|
85
|
+
enable has to add that plugin in its own `plugins`, or the entry is dropped with no error; a plugin
|
|
86
|
+
already in the top-level set needs no repeating, and an override's `plugins` adds to that set rather
|
|
87
|
+
than replacing it (measured on vite-plus 0.3.1 and oxlint 1.81.0, 2026-09-19). And a relative plugin
|
|
88
|
+
specifier inside an `extends` object is refused; a package name is what works.
|
|
91
89
|
|
|
92
90
|
### The config exports
|
|
93
91
|
|
|
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
|
-
| `
|
|
92
|
+
| Export | What it is |
|
|
93
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| `plugin` | `{ name: "rdjperron", specifier: "@rdjperron/lint" }`. |
|
|
95
|
+
| `RULE_IDS`, `RuleId` | The twenty prefixed ids, as a readonly tuple and as a type. |
|
|
96
|
+
| `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`. |
|
|
97
|
+
| `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. |
|
|
98
|
+
| `i18n(files, options?)` | `rdjperron/no-hardcoded-jsx-text` at error. `options.pattern` sets the word pattern. |
|
|
99
|
+
| `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. |
|
|
100
|
+
| `restrictedImports(…)` | One `no-restricted-imports` override per file set, expanded from one declaration. See below. |
|
|
101
|
+
| `tests(files)` | `any`, `!`, `no-runtime-typeof` and the safety-comment rule off, with the reason on each. |
|
|
102
|
+
| `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. |
|
|
103
|
+
| `fmt` | `printWidth` 100, double quotes, semicolons, two spaces. |
|
|
104
|
+
| `ignore` | Dependencies, build output, the AI agent directories and the Vitest JSON report. |
|
|
105
|
+
| `staged` | `vp check --fix` on JS and TS sources. |
|
|
107
106
|
|
|
108
107
|
`recommended` puts every rule at `error` except `no-hardcoded-jsx-text` and `static-public-env`,
|
|
109
108
|
which belong to a translated app and to an Expo app and are left to `i18n()` and `expo()`. The
|
|
110
109
|
measurements behind those severities are in the comments beside them.
|
|
111
110
|
|
|
111
|
+
### `restrictedImports`, one declaration for every file set
|
|
112
|
+
|
|
113
|
+
Two overrides matching one file do not merge. The later one replaces the rule's whole entry, so
|
|
114
|
+
every file set needs `no-restricted-imports` written out in full. A repo with ten file sets
|
|
115
|
+
restates the same list ten times, and a new ban has to be added to all ten. Forget one and that
|
|
116
|
+
file set silently keeps importing what the others may not.
|
|
117
|
+
|
|
118
|
+
Here each entry is named once and each scope says which scope it starts `from`, what it `add`s and
|
|
119
|
+
what it `drop`s. Nothing is matched by glob and nothing is guessed: one override comes out per
|
|
120
|
+
scope, in the order you declared them, so you declare broad to narrow.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const RN_TEXT = { name: "react-native", importNames: ["Text"], message: "Text goes through <T role>." };
|
|
124
|
+
const RN_ALERT = { name: "react-native", importNames: ["Alert"], message: "lib/alert.ts answers on both targets." };
|
|
125
|
+
const SCHEMA = { group: ["**/convex/schema"], message: "convex/validators.ts is what the client reads." };
|
|
126
|
+
|
|
127
|
+
...restrictedImports({
|
|
128
|
+
paths: { RN_TEXT, RN_ALERT },
|
|
129
|
+
patterns: { SCHEMA },
|
|
130
|
+
scopes: {
|
|
131
|
+
all: { files: ["**"], use: ["RN_TEXT", "RN_ALERT", "SCHEMA"] },
|
|
132
|
+
kit: { files: ["components/ui/**"], from: "all", drop: ["RN_TEXT"] },
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
That returns two overrides. The first bans all three everywhere, the second bans `RN_ALERT` and
|
|
138
|
+
`SCHEMA` under `components/ui/`, which is where `Text` is wrapped. A ban added to `paths` and to
|
|
139
|
+
`all` reaches every scope that descends from it, and `components/ui/` keeps its one exemption.
|
|
140
|
+
|
|
141
|
+
The broadest scope is an override on `files: ["**"]` rather than a top-level rule. Measured on
|
|
142
|
+
vite-plus 0.3.1 and oxlint 1.81.0, 2026-09-19: the two behave the same, files matched by no other
|
|
143
|
+
override included, so the whole list reads in one place.
|
|
144
|
+
|
|
145
|
+
Severity is `error`, always. Every message is yours.
|
|
146
|
+
|
|
147
|
+
TypeScript rejects four mistakes on the spot:
|
|
148
|
+
|
|
149
|
+
- a name in `use`, `add` or `drop` that is no key of `paths` or `patterns`,
|
|
150
|
+
- a `from` naming a scope that does not exist,
|
|
151
|
+
- `use` and `from` on the same scope,
|
|
152
|
+
- a scope with neither.
|
|
153
|
+
|
|
154
|
+
Three more depend on the order scopes resolve in, so they throw when the config loads, naming the
|
|
155
|
+
scope and the entry:
|
|
156
|
+
|
|
157
|
+
- a `from` naming a scope declared after this one,
|
|
158
|
+
- a `drop` of a name the parent does not restrict,
|
|
159
|
+
- an `add` of a name the parent already restricts.
|
|
160
|
+
|
|
161
|
+
The last two exist because both are a claim about the parent that is false, and a config that
|
|
162
|
+
states something false about its parent is one rename away from lifting a ban.
|
|
163
|
+
|
|
112
164
|
## Prove the rules still fire
|
|
113
165
|
|
|
114
166
|
```bash
|
|
115
|
-
npx
|
|
167
|
+
npx rdjperron-lint-verify
|
|
116
168
|
```
|
|
117
169
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
every rule fired on the file named after it
|
|
121
|
-
|
|
122
|
-
like "no violations". Nothing else notices.
|
|
170
|
+
No configuration and no arguments. A rule can go silently dead after a linter upgrade, and "reports
|
|
171
|
+
nothing" looks exactly like "no violations", so nothing else in a repo notices. This asserts a
|
|
172
|
+
positive: every rule fired, each on the file named after it. It prints a table and exits non-zero on
|
|
173
|
+
the first rule that stayed quiet.
|
|
123
174
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
The fixtures ship in the package, one deliberately violating `fixtures/<rule>.fixture.ts(x)` per
|
|
176
|
+
rule. The command copies them into a throwaway project under your system temp directory, beside a
|
|
177
|
+
generated config that puts every rule at error, with your `node_modules` symlinked in, and removes
|
|
178
|
+
the whole directory afterwards whether the run passed or failed. Your own config, your ignore
|
|
179
|
+
patterns and your severities take no part.
|
|
128
180
|
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
The linter it runs is the oxlint your local `vite-plus` depends on, so the run that matters is the
|
|
182
|
+
one right after you move `vite-plus`, and in CI.
|
|
131
183
|
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
One thing not to do: do not delete the fixtures. They are the only thing that catches a rule going
|
|
185
|
+
dead.
|
|
134
186
|
|
|
135
187
|
## The rules
|
|
136
188
|
|
|
137
|
-
Each
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
189
|
+
Each rule file opens with the reason the rule exists and the case where it is legitimate to
|
|
190
|
+
scope it off; the messages, the options and the cases are in the rule and its tests.
|
|
191
|
+
|
|
192
|
+
| Rule | What it rejects |
|
|
193
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------- |
|
|
194
|
+
| `rdjperron/no-array-filter-map` | `filter().map()`, two passes and an intermediate array. |
|
|
195
|
+
| `rdjperron/no-chained-type-assertions` | `x as unknown as T`, an assertion laundered through a second one. |
|
|
196
|
+
| `rdjperron/no-conditional-empty-object-spread` | `...(cond ? { a } : {})`, omission hidden behind an empty object. |
|
|
197
|
+
| `rdjperron/no-hardcoded-jsx-text` | Words written straight into JSX children. |
|
|
198
|
+
| `rdjperron/no-known-value-widening` | An annotation that throws away what inference already knew. |
|
|
199
|
+
| `rdjperron/no-module-mocking` | `vi.mock` and friends, a test of the mock rather than the module. |
|
|
200
|
+
| `rdjperron/no-object-parameters` | The bare `object` type on a function input. |
|
|
201
|
+
| `rdjperron/no-reduce-accumulator-copy` | Spreading the accumulator inside `reduce`, quadratic by construction. |
|
|
202
|
+
| `rdjperron/no-reflect-apply` | `Reflect.apply`, a call the types cannot follow. |
|
|
203
|
+
| `rdjperron/no-reflect-get` | `Reflect.get`, a property read the types cannot follow. |
|
|
204
|
+
| `rdjperron/no-runtime-typeof` | `typeof` as a substitute for parsing input at its boundary. |
|
|
205
|
+
| `rdjperron/no-shape-in-symbol-names` | The word "shape" in a symbol name, structure in place of a role. |
|
|
206
|
+
| `rdjperron/no-unknown-parameters` | `unknown` on a function input. |
|
|
207
|
+
| `rdjperron/no-unknown-returns` | `unknown` on a function output. |
|
|
208
|
+
| `rdjperron/no-unknown-type-aliases` | A type alias that resolves to `unknown`. |
|
|
209
|
+
| `rdjperron/no-unsafe-dictionary-type` | A dictionary whose values are `unknown`, `any` or `object`. |
|
|
210
|
+
| `rdjperron/no-use-effect` | Every `useEffect` and `useLayoutEffect` call or import. |
|
|
211
|
+
| `rdjperron/no-widen-then-assert` | Widening a value and asserting it back. |
|
|
212
|
+
| `rdjperron/require-safety-comment-for-type-assertion` | An assertion with no `SAFETY:` line naming the invariant. |
|
|
213
|
+
| `rdjperron/static-public-env` | A `process.env` read Expo's bundler does not replace at build time. |
|
|
161
214
|
|
|
162
215
|
### Options
|
|
163
216
|
|
|
164
|
-
- `
|
|
217
|
+
- `rdjperron/no-use-effect` takes `{ hint?: string }`, appended to every finding, so a project can name
|
|
165
218
|
its one sanctioned wrapper. `react(files, { useEffectAllowedIn })` sets it and exempts that file.
|
|
166
|
-
- `
|
|
219
|
+
- `rdjperron/no-hardcoded-jsx-text` takes `{ pattern?: string }`, a regex source compiled with `u`. The
|
|
167
220
|
default `\p{L}{3,}` says three letters in a row is a word; digits, separators and bullets are
|
|
168
221
|
not. It looks at JSX text and at string literals placed as children, never at attribute values.
|
|
169
|
-
- `
|
|
170
|
-
`typeof` inside a type predicate is the decode the rule asks for, named and
|
|
171
|
-
|
|
222
|
+
- `rdjperron/no-runtime-typeof` takes `{ allowInTypeGuards?: boolean, hint?: string }`. `recommended`
|
|
223
|
+
turns the first on: a `typeof` inside a type predicate is the decode the rule asks for, named and
|
|
224
|
+
reusable. The hint is appended to every finding; `decoders(files)` sets it.
|
|
225
|
+
- `rdjperron/no-unknown-parameters` takes `{ hint?: string }`, appended to every finding, so a project
|
|
226
|
+
can name the modules where the parse belongs. `decoders(files)` sets it.
|
|
227
|
+
- `rdjperron/static-public-env` has no options.
|
|
228
|
+
|
|
229
|
+
## Name the file that holds the fix
|
|
230
|
+
|
|
231
|
+
A rule tells a reader what is wrong. When the fix is the same every time, the finding can tell them
|
|
232
|
+
where to go as well, which is what `useEffectAllowedIn` and `decoders(files)` are for: they put a
|
|
233
|
+
sentence naming the sanctioned file in every finding of the rule that will send someone there.
|
|
234
|
+
`decoders(["lib/api-error.ts", "convex/lib/json.ts"])` reads:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
Parameter `body` leaves input unparsed. Accept a named domain type; run the expected schema or
|
|
238
|
+
parser at the I/O boundary before calling this function. Decode the value in lib/api-error.ts or
|
|
239
|
+
convex/lib/json.ts, then call that decoder from here.
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The same sentence lands on `no-runtime-typeof`, and inside those files that rule is off, because a
|
|
243
|
+
`typeof` there is the parse rather than a narrowing in place of one.
|
|
172
244
|
|
|
173
245
|
## Severities are measurements
|
|
174
246
|
|
|
175
247
|
Land a new rule at `warn`, fix or scope what fires, then promote it. When a rule fires, fix the
|
|
176
248
|
code or add a narrowly scoped override with the measurement that justifies it. Never weaken the
|
|
177
249
|
rule, add a cast, or rename a symbol to dodge the check.
|
|
250
|
+
|
|
251
|
+
## Moving from 0.4
|
|
252
|
+
|
|
253
|
+
Nothing is removed. One builder changed what it does, and three are new.
|
|
254
|
+
|
|
255
|
+
`expo(files)` used to set `static-public-env` and nothing else. It now also puts
|
|
256
|
+
`typescript/no-require-imports` at error with the asset extensions allowed, and turns
|
|
257
|
+
`unicorn/no-array-sort` off. If your config carries either by hand, delete it, and pass
|
|
258
|
+
`assetExts` for what your `metro.config.js` adds. An asset `require()` is now allowed only in the
|
|
259
|
+
files you hand to `expo()`. If you allowed it everywhere before, a `require()` of an asset outside
|
|
260
|
+
those files reports.
|
|
261
|
+
|
|
262
|
+
| You write by hand today | 0.5.0 gives you |
|
|
263
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------- |
|
|
264
|
+
| The same `no-restricted-imports` list in every override, minus one entry | `restrictedImports({ paths, patterns, scopes })` |
|
|
265
|
+
| `no-runtime-typeof` off for your decoder modules | `decoders(files)`, which also names them in the fix |
|
|
266
|
+
| `react/capitalized-calls` off, `react/style-prop-object` for `StatusBar` | `expo(files, { reactFiles })` |
|
|
267
|
+
| `react/exhaustive-deps` off for the `useEffectAllowedIn` file | `react()` does it |
|
|
268
|
+
|
|
269
|
+
Measured on rummy, 2026-09-19: `vite.config.ts` went from 447 lines to 366. 132 planted findings
|
|
270
|
+
across ten import scopes came out identical, position for position, before and after. The one
|
|
271
|
+
difference is the asset rule, which moved from warn to error and stopped allowing assets outside
|
|
272
|
+
the phone's files.
|
|
273
|
+
|
|
274
|
+
## Moving from 0.3
|
|
275
|
+
|
|
276
|
+
0.4.0 moved the rules' health out of the consumer. Everything below is something you delete, and
|
|
277
|
+
nothing replaces it.
|
|
278
|
+
|
|
279
|
+
| Delete | Why it is gone |
|
|
280
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `@oxlint/plugins` from your `package.json` | An ordinary dependency here now. Nothing left to pin, and nothing to move on a `vp` upgrade. |
|
|
282
|
+
| `plugins: [...]` from `lint` | `recommended` carries the set its rules need. Keep yours only if you add a plugin of your own. |
|
|
283
|
+
| `categories: {...}` from `lint` | `recommended` carries `correctness: "error"`, `suspicious: "warn"`, `perf: "warn"`. |
|
|
284
|
+
| `typeAware: true` from `lint.options` | `recommended` carries it, and `options` merges key by key, so your `typeCheck` still applies. |
|
|
285
|
+
| `fixtures()` from your imports and your overrides | The verify command writes its own config now. The builder no longer exists. |
|
|
286
|
+
| Any comment about keeping `scripts/.lint-fixtures` un-ignored | The check runs under your system temp directory, where nothing is ignored. |
|
|
287
|
+
| Your `verify:lint-fixtures` script, if it passed arguments | `rdjperron-lint-verify` takes none. |
|
|
288
|
+
|
|
289
|
+
Two exports also went: `@rdjperron/lint/verify` (`verifySuites`, `packageSuite`,
|
|
290
|
+
`verifyPluginApiPin`) and `@rdjperron/lint/fixtures/*`. The fixtures still ship, the bin reads them
|
|
291
|
+
itself, and a repo with a second plugin to check now runs that plugin's own command beside this one.
|
|
292
|
+
|
|
293
|
+
New in 0.4.0: `oxc/no-accumulating-spread` at error, measured at zero hits on three repos. If it
|
|
294
|
+
fires on yours, that reducer is quadratic.
|