Skip to content

Commit 3ab9ab5

Browse files
committed
update skills
1 parent fd689d8 commit 3ab9ab5

1 file changed

Lines changed: 377 additions & 0 deletions

File tree

  • .codex/skills/eslint-rule-paradigm
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
---
2+
name: eslint-rule-paradigm
3+
description: Best-practice workflow for designing, implementing, and testing ESLint rules/plugins (meta, schema/defaultOptions, RuleTester, fixes/suggestions, flat config). Includes authoritative links, example sources, test templates, packaging that supports `defineConfig(plugin.configs.recommended)`, and repo integration via `pnpm run lint-app`.
4+
---
5+
6+
# ESLint Rule Paradigm
7+
8+
## When to use
9+
10+
* Creating a new ESLint rule or refactoring an existing one
11+
* Building/maintaining plugin configs (especially for flat config)
12+
* Adding analyzers and validating behavior in a realistic TS project (packages/app)
13+
14+
---
15+
16+
## Primary docs (authoritative)
17+
18+
ESLint:
19+
20+
* Custom rules: https://eslint.org/docs/latest/extend/custom-rules
21+
* Custom rule tutorial: https://eslint.org/docs/latest/extend/custom-rule-tutorial
22+
* Plugins (shape, exports): https://eslint.org/docs/latest/extend/plugins
23+
* Flat config - configure plugins: https://eslint.org/docs/latest/use/configure/plugins
24+
* Plugin migration to flat config (meta.name, meta.version, recommended structure): https://eslint.org/docs/latest/extend/plugin-migration-flat-config
25+
* Node.js API - RuleTester basics: https://eslint.org/docs/latest/integrate/nodejs-api
26+
27+
TypeScript ecosystem:
28+
29+
* typescript-eslint custom rules: https://typescript-eslint.io/developers/custom-rules/
30+
* @typescript-eslint/rule-tester: https://typescript-eslint.io/packages/rule-tester/
31+
32+
---
33+
34+
## Where to copy examples from (high signal)
35+
36+
1. ESLint core repo (canonical layout and style):
37+
38+
* rules: `lib/rules/*`
39+
* tests: `tests/lib/rules/*`
40+
* docs: `docs/src/rules/*`
41+
* repo: https://github.com/eslint/eslint
42+
43+
2. ESLint Custom Rule Tutorial:
44+
45+
* small end-to-end example with RuleTester + packaging
46+
47+
3. typescript-eslint docs and packages:
48+
49+
* patterns for typed rules, parser services, safer ergonomics
50+
51+
---
52+
53+
## Core workflow
54+
55+
1. **Problem framing**
56+
57+
* Specify the invariant precisely: forbidden/required property.
58+
* Specify the exact AST contexts: node kinds + minimal data required.
59+
* Decide if you need types or pure syntax is enough.
60+
61+
2. **Rule classification**
62+
63+
* `meta.type`: `problem | suggestion | layout`
64+
* Autofix -> set `meta.fixable: "code" | "whitespace"`
65+
* Suggestions -> set `meta.hasSuggestions: true`
66+
67+
3. **Options contract first**
68+
69+
* `meta.schema`: JSON Schema for options (`[]` if no options).
70+
* `defaultOptions`: top-level export field (NOT inside `meta`).
71+
* Compatibility:
72+
73+
* don't change meaning of existing options
74+
* add new options as optional with defaults
75+
76+
4. **Messages contract**
77+
78+
* Put all messages under `meta.messages`.
79+
* Report via `{ messageId, data }`.
80+
* Keep message IDs stable (they're public API).
81+
82+
5. **Implementation constraints**
83+
84+
* Deterministic, no I/O, no global state.
85+
* No AST mutation.
86+
* Performance:
87+
88+
* avoid expensive scans per-node
89+
* cache derived data from `context.getSourceCode()`
90+
* for cross-file-ish logic within one file: collect in visitors, finalize in `Program:exit`
91+
92+
6. **Type-aware path (optional)**
93+
94+
* Only touch types when parser services exist.
95+
* If types are required: document requirement and fail gracefully (no crash).
96+
97+
7. **Testing**
98+
99+
* Unit tests: RuleTester (valid/invalid + options + fix/suggestions).
100+
* Integration loop: `packages/app` via `pnpm run lint-app` (realistic output).
101+
102+
8. **Packaging**
103+
104+
* Export plugin object with `meta`, `rules`, `configs`.
105+
* Ensure flat-config `recommended` works standalone via `defineConfig(plugin.configs.recommended)`.
106+
107+
---
108+
109+
# Templates
110+
111+
## Minimal rule skeleton
112+
113+
```js
114+
export default {
115+
meta: {
116+
type: "suggestion",
117+
docs: { description: "...", recommended: false },
118+
schema: [],
119+
messages: {
120+
bad: "...",
121+
},
122+
// fixable: "code", // only if you provide `fix`
123+
// hasSuggestions: true, // only if you provide `suggest`
124+
},
125+
defaultOptions: [],
126+
create(context) {
127+
const sourceCode = context.getSourceCode();
128+
129+
return {
130+
Identifier(node) {
131+
// lightweight analysis
132+
context.report({ node, messageId: "bad" });
133+
},
134+
};
135+
},
136+
};
137+
```
138+
139+
## Fix vs suggestion policy
140+
141+
* Use **autofix** only if the transform is local + unambiguous + syntax-safe + semantics-safe.
142+
* If there is any ambiguity, prefer **suggestions**.
143+
144+
---
145+
146+
# Testing templates (RuleTester)
147+
148+
## Baseline RuleTester (ESLint)
149+
150+
```js
151+
import rule from "../src/rules/my-rule.js";
152+
import { RuleTester } from "eslint";
153+
154+
const ruleTester = new RuleTester({
155+
languageOptions: { ecmaVersion: 2022, sourceType: "module" },
156+
});
157+
158+
ruleTester.run("my-rule", rule, {
159+
valid: [
160+
"const ok = 1;",
161+
{ code: "const ok2 = 2;", options: [{ someOpt: true }] },
162+
],
163+
invalid: [
164+
{
165+
code: "const bad = 1;",
166+
options: [{ someOpt: false }],
167+
errors: [{ messageId: "bad" }],
168+
},
169+
],
170+
});
171+
```
172+
173+
## Autofix test
174+
175+
```js
176+
ruleTester.run("my-rule", rule, {
177+
valid: [],
178+
invalid: [
179+
{
180+
code: "let x = 1;",
181+
output: "const x = 1;",
182+
errors: [{ messageId: "useConst" }],
183+
},
184+
],
185+
});
186+
```
187+
188+
## Suggestions test
189+
190+
```js
191+
ruleTester.run("my-rule", rule, {
192+
valid: [],
193+
invalid: [
194+
{
195+
code: "const foo = 'baz';",
196+
errors: [
197+
{
198+
messageId: "wrongFoo",
199+
suggestions: [
200+
{ messageId: "replaceWithBar", output: "const foo = 'bar';" },
201+
],
202+
},
203+
],
204+
},
205+
],
206+
});
207+
```
208+
209+
## TS-friendly testing (@typescript-eslint/rule-tester)
210+
211+
```ts
212+
import { RuleTester } from "@typescript-eslint/rule-tester";
213+
import rule from "../src/rules/my-rule";
214+
215+
const ruleTester = new RuleTester({
216+
languageOptions: { ecmaVersion: 2022, sourceType: "module" },
217+
});
218+
219+
ruleTester.run("my-rule", rule, {
220+
valid: [{ code: "const x: number = 1;" }],
221+
invalid: [{ code: "const y: any = 1;", errors: [{ messageId: "noAny" }] }],
222+
});
223+
```
224+
225+
### Error assertion checklist
226+
227+
* Always assert `messageId`.
228+
* Assert `data` keys when used.
229+
* Use `output` for fixes.
230+
* Use `suggestions: [{ messageId, output }]` for suggestions.
231+
232+
---
233+
234+
# Flat config packaging that never errors with `defineConfig(plugin.configs.recommended)`
235+
236+
## Goal
237+
238+
Your plugin must be usable like this (no extra `plugins: {}` required in user config):
239+
240+
```ts
241+
import plugin from "<plugin-package-name>";
242+
import { defineConfig } from "eslint/config";
243+
244+
export default defineConfig(
245+
plugin.configs.recommended,
246+
// optional overrides
247+
);
248+
```
249+
250+
## Invariant (must hold)
251+
252+
`configs.recommended` MUST be a flat config object (or an array of flat config objects) that **self-registers the plugin**:
253+
254+
* `plugins: { "<plugin-namespace>": pluginObject }`
255+
* `rules: { "<plugin-namespace>/<rule>": "error" | ... }`
256+
257+
If `configs.recommended` does not include `plugins: { namespace: plugin }`, ESLint will report "unknown rule" / "plugin missing" when the user only spreads recommended.
258+
259+
## Recommended implementation pattern (no circular refs problems)
260+
261+
Define `plugin` first, then attach configs using `Object.assign`, so `configs.recommended` can reference `plugin`.
262+
263+
```ts
264+
import type { Linter } from "eslint";
265+
266+
const plugin = {
267+
meta: {
268+
name: "<plugin-package-name>",
269+
version: "1.0.0",
270+
},
271+
rules: {
272+
"<rule-a>": ruleA,
273+
"<rule-b>": ruleB,
274+
"<rule-c>": ruleC,
275+
"<rule-d>": ruleD,
276+
},
277+
configs: {},
278+
} satisfies Linter.Plugin;
279+
280+
Object.assign(plugin.configs, {
281+
recommended: {
282+
name: "<plugin-namespace>/recommended",
283+
plugins: {
284+
"<plugin-namespace>": plugin,
285+
},
286+
rules: {
287+
"<plugin-namespace>/<rule-a>": "error",
288+
"<plugin-namespace>/<rule-b>": "error",
289+
"<plugin-namespace>/<rule-c>": "error",
290+
"<plugin-namespace>/<rule-d>": "error",
291+
},
292+
} satisfies Linter.FlatConfig,
293+
});
294+
295+
export default plugin;
296+
```
297+
298+
### Notes
299+
300+
* Namespace (`"<plugin-namespace>"`) must match what you want in rule IDs (`<plugin-namespace>/<rule>`).
301+
* In `plugin.rules`, keys do NOT include namespace.
302+
* In `configs.recommended.rules`, keys DO include namespace.
303+
304+
## If you prefer `recommended` as an array
305+
306+
This is also valid and composes better when you need multiple layers.
307+
308+
```ts
309+
Object.assign(plugin.configs, {
310+
recommended: [
311+
{
312+
name: "<plugin-namespace>/recommended",
313+
plugins: { "<plugin-namespace>": plugin },
314+
rules: {
315+
"<plugin-namespace>/<rule-a>": "error",
316+
"<plugin-namespace>/<rule-b>": "error",
317+
},
318+
},
319+
{
320+
name: "<plugin-namespace>/recommended-overrides",
321+
rules: {
322+
"<plugin-namespace>/<rule-c>": "warn",
323+
},
324+
},
325+
] satisfies Linter.FlatConfig[],
326+
});
327+
```
328+
329+
---
330+
331+
# Repo integration: `packages/app` (live playground)
332+
333+
## Purpose
334+
335+
`packages/app` is an intentionally imperfect TS app used to validate analyzers/rules in a realistic run.
336+
You intentionally introduce mistakes (typos, wrong names, missing imports, etc.) to verify:
337+
338+
* your rule triggers
339+
* message wording is correct
340+
* suggestions/fixes appear where intended
341+
342+
## Run
343+
344+
From repo root:
345+
346+
```bash
347+
pnpm run lint-app
348+
```
349+
350+
This runs linting for `@effect-template/app` and prints diagnostics from ESLint (and other tools in the pipeline). Use it as fast feedback.
351+
352+
## How to add a new scenario
353+
354+
* Add/change code under `packages/app/src/**` to trigger one intended analyzer.
355+
* Keep each scenario small and isolated.
356+
* Use recognizable "intentional mistakes" (e.g. `ru1Main`, `modul3`, `formatGree7ing`) so the output is self-explanatory.
357+
358+
## Guardrails
359+
360+
* Don't "fix" the playground errors unless you are intentionally changing expected analyzer behavior.
361+
* If you change messages/suggestions, update:
362+
363+
* unit tests (RuleTester)
364+
* and playground scenarios if they serve as demonstration
365+
366+
---
367+
368+
# Ship-ready checklist
369+
370+
* [ ] invariant is precise (AST scope + intended behavior)
371+
* [ ] `meta.type` chosen; `meta.fixable`/`meta.hasSuggestions` consistent with behavior
372+
* [ ] `meta.schema` + `defaultOptions` defined first; options compatibility preserved
373+
* [ ] reports use `messageId` (+ `data`), messages stable
374+
* [ ] perf: no heavy scans per-node; caching or `Program:exit` aggregation
375+
* [ ] RuleTester covers: valid/invalid, option matrix, fixes/suggestions, edge syntax
376+
* [ ] `packages/app` scenario demonstrates analyzer (`pnpm run lint-app`)
377+
* [ ] `configs.recommended` is self-contained and works via `defineConfig(plugin.configs.recommended)`

0 commit comments

Comments
 (0)