eslint-plugin-traceability 1.7.0 → 1.8.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/CHANGELOG.md +82 -0
- package/README.md +73 -32
- package/docs/ci-cd-pipeline.md +224 -0
- package/docs/cli-integration.md +22 -0
- package/docs/code-quality-refactor-opportunities-2025-12-03.md +78 -0
- package/docs/config-presets.md +38 -0
- package/docs/conventional-commits-guide.md +185 -0
- package/docs/custom-rules-development-guide.md +659 -0
- package/docs/decisions/0001-allow-dynamic-require-for-built-plugins.md +26 -0
- package/docs/decisions/001-typescript-for-eslint-plugin.accepted.md +111 -0
- package/docs/decisions/002-jest-for-eslint-testing.accepted.md +137 -0
- package/docs/decisions/003-code-quality-ratcheting-plan.md +48 -0
- package/docs/decisions/004-automated-version-bumping-for-ci-cd.md +196 -0
- package/docs/decisions/005-github-actions-validation-tooling.accepted.md +144 -0
- package/docs/decisions/006-semantic-release-for-automated-publishing.accepted.md +227 -0
- package/docs/decisions/007-github-releases-over-changelog.accepted.md +216 -0
- package/docs/decisions/008-ci-audit-flags.accepted.md +60 -0
- package/docs/decisions/009-security-focused-lint-rules.accepted.md +64 -0
- package/docs/decisions/010-implements-annotation-for-multi-story-requirements.proposed.md +184 -0
- package/docs/decisions/adr-0001-console-usage-for-cli-guards.md +190 -0
- package/docs/decisions/adr-accept-dev-dep-risk-glob.md +40 -0
- package/docs/decisions/adr-commit-branch-tests.md +54 -0
- package/docs/decisions/adr-maintenance-cli-interface.md +140 -0
- package/docs/decisions/adr-pre-push-parity.md +112 -0
- package/docs/decisions/code-quality-ratcheting-plan.md +53 -0
- package/docs/dependency-health.md +238 -0
- package/docs/eslint-9-setup-guide.md +517 -0
- package/docs/eslint-plugin-development-guide.md +487 -0
- package/docs/functionality-coverage-2025-12-03.md +250 -0
- package/docs/jest-testing-guide.md +100 -0
- package/docs/rules/prefer-implements-annotation.md +219 -0
- package/docs/rules/require-branch-annotation.md +71 -0
- package/docs/rules/require-req-annotation.md +203 -0
- package/docs/rules/require-story-annotation.md +159 -0
- package/docs/rules/valid-annotation-format.md +418 -0
- package/docs/rules/valid-req-reference.md +153 -0
- package/docs/rules/valid-story-reference.md +120 -0
- package/docs/security-incidents/2025-11-17-glob-cli-incident.md +45 -0
- package/docs/security-incidents/2025-11-18-brace-expansion-redos.md +45 -0
- package/docs/security-incidents/2025-11-18-bundled-dev-deps-accepted-risk.md +93 -0
- package/docs/security-incidents/2025-11-18-tar-race-condition.md +43 -0
- package/docs/security-incidents/2025-12-03-dependency-health-review.md +58 -0
- package/docs/security-incidents/SECURITY-INCIDENT-2025-11-18-semantic-release-bundled-npm.known-error.md +104 -0
- package/docs/security-incidents/SECURITY-INCIDENT-TEMPLATE.md +37 -0
- package/docs/security-incidents/dependency-override-rationale.md +57 -0
- package/docs/security-incidents/dev-deps-high.json +116 -0
- package/docs/security-incidents/handling-procedure.md +54 -0
- package/docs/stories/001.0-DEV-PLUGIN-SETUP.story.md +92 -0
- package/docs/stories/002.0-DEV-ESLINT-CONFIG.story.md +82 -0
- package/docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md +112 -0
- package/docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md +153 -0
- package/docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md +138 -0
- package/docs/stories/006.0-DEV-FILE-VALIDATION.story.md +144 -0
- package/docs/stories/007.0-DEV-ERROR-REPORTING.story.md +163 -0
- package/docs/stories/008.0-DEV-AUTO-FIX.story.md +150 -0
- package/docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md +117 -0
- package/docs/stories/010.0-DEV-DEEP-VALIDATION.story.md +124 -0
- package/docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md +149 -0
- package/docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md +216 -0
- package/docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md +236 -0
- package/docs/stories/developer-story.map.md +120 -0
- package/docs/ts-jest-presets-guide.md +548 -0
- package/lib/src/index.d.ts +2 -2
- package/lib/src/index.js +2 -0
- package/lib/src/maintenance/batch.d.ts +5 -0
- package/lib/src/maintenance/batch.js +5 -0
- package/lib/src/maintenance/cli.js +34 -212
- package/lib/src/maintenance/commands.d.ts +32 -0
- package/lib/src/maintenance/commands.js +139 -0
- package/lib/src/maintenance/detect.d.ts +2 -0
- package/lib/src/maintenance/detect.js +4 -0
- package/lib/src/maintenance/flags.d.ts +99 -0
- package/lib/src/maintenance/flags.js +121 -0
- package/lib/src/maintenance/report.d.ts +2 -0
- package/lib/src/maintenance/report.js +2 -0
- package/lib/src/maintenance/update.d.ts +4 -0
- package/lib/src/maintenance/update.js +4 -0
- package/lib/src/rules/helpers/require-story-io.d.ts +3 -0
- package/lib/src/rules/helpers/require-story-io.js +20 -6
- package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
- package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
- package/lib/src/rules/helpers/valid-annotation-options.js +15 -4
- package/lib/src/rules/helpers/valid-annotation-utils.js +5 -0
- package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
- package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +3 -4
- package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
- package/lib/src/rules/prefer-implements-annotation.js +276 -0
- package/lib/src/rules/valid-annotation-format.js +87 -28
- package/lib/src/rules/valid-req-reference.js +71 -0
- package/lib/src/utils/reqAnnotationDetection.d.ts +4 -1
- package/lib/src/utils/reqAnnotationDetection.js +43 -15
- package/lib/tests/maintenance/cli.test.js +89 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
- package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
- package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
- package/lib/tests/rules/require-req-annotation.test.js +8 -1
- package/lib/tests/rules/require-story-annotation.test.js +9 -4
- package/lib/tests/rules/valid-annotation-format.test.js +78 -0
- package/lib/tests/rules/valid-req-reference.test.js +34 -0
- package/lib/tests/utils/ts-language-options.d.ts +1 -7
- package/lib/tests/utils/ts-language-options.js +8 -5
- package/package.json +7 -3
- package/user-docs/api-reference.md +507 -0
- package/user-docs/eslint-9-setup-guide.md +639 -0
- package/user-docs/examples.md +74 -0
- package/user-docs/migration-guide.md +158 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
# Custom ESLint Rules Development Guide
|
|
2
|
+
|
|
3
|
+
This guide provides comprehensive information for developing custom ESLint rules based on the official [ESLint Custom Rules documentation](https://eslint.org/docs/latest/extend/custom-rules).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Rule Structure](#rule-structure)
|
|
8
|
+
- [The Context Object](#the-context-object)
|
|
9
|
+
- [Working with the AST](#working-with-the-ast)
|
|
10
|
+
- [Reporting Problems](#reporting-problems)
|
|
11
|
+
- [Applying Fixes](#applying-fixes)
|
|
12
|
+
- [Providing Suggestions](#providing-suggestions)
|
|
13
|
+
- [Rule Options and Schema](#rule-options-and-schema)
|
|
14
|
+
- [Accessing Source Code](#accessing-source-code)
|
|
15
|
+
- [Variable Scopes](#variable-scopes)
|
|
16
|
+
- [Testing Rules](#testing-rules)
|
|
17
|
+
- [Best Practices](#best-practices)
|
|
18
|
+
|
|
19
|
+
## Rule Structure
|
|
20
|
+
|
|
21
|
+
Every ESLint rule must export an object with two main properties:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
module.exports = {
|
|
25
|
+
meta: {
|
|
26
|
+
type: "problem" | "suggestion" | "layout",
|
|
27
|
+
docs: {
|
|
28
|
+
description: "Description of the rule",
|
|
29
|
+
url: "https://your-docs-url.com",
|
|
30
|
+
},
|
|
31
|
+
fixable: "code" | "whitespace", // Optional, required if rule provides fixes
|
|
32
|
+
hasSuggestions: true, // Optional, required if rule provides suggestions
|
|
33
|
+
schema: [], // Rule options schema
|
|
34
|
+
messages: {
|
|
35
|
+
// Message templates
|
|
36
|
+
messageId: "Message text with {{placeholder}}",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
return {
|
|
41
|
+
// Visitor methods
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Rule Types
|
|
48
|
+
|
|
49
|
+
- **`problem`**: Identifies code that will cause errors or confusing behavior. High priority.
|
|
50
|
+
- **`suggestion`**: Identifies code that could be improved but won't cause errors.
|
|
51
|
+
- **`layout`**: Concerns whitespace, formatting, and code appearance.
|
|
52
|
+
|
|
53
|
+
### Meta Properties
|
|
54
|
+
|
|
55
|
+
- **`type`**: (Required) One of "problem", "suggestion", or "layout"
|
|
56
|
+
- **`docs`**: Documentation metadata
|
|
57
|
+
- `description`: Short description of the rule
|
|
58
|
+
- `url`: Full documentation URL
|
|
59
|
+
- `recommended`: Whether enabled by default (boolean for plugins)
|
|
60
|
+
- **`fixable`**: Either "code" or "whitespace" if the rule can auto-fix
|
|
61
|
+
- **`hasSuggestions`**: Set to `true` if rule provides manual suggestions
|
|
62
|
+
- **`schema`**: JSON Schema defining valid rule options
|
|
63
|
+
- **`defaultOptions`**: Default values for rule options
|
|
64
|
+
- **`messages`**: Named message templates with placeholders
|
|
65
|
+
- **`deprecated`**: Deprecation info (boolean or object)
|
|
66
|
+
|
|
67
|
+
## The Context Object
|
|
68
|
+
|
|
69
|
+
The `context` object is passed to the `create()` function and provides:
|
|
70
|
+
|
|
71
|
+
### Properties
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
create(context) {
|
|
75
|
+
const {
|
|
76
|
+
id, // Rule ID
|
|
77
|
+
filename, // File being linted
|
|
78
|
+
physicalFilename, // Full path on disk
|
|
79
|
+
cwd, // Current working directory
|
|
80
|
+
options, // Array of rule options (without severity)
|
|
81
|
+
sourceCode, // SourceCode object
|
|
82
|
+
settings, // Shared settings from config
|
|
83
|
+
languageOptions // Language configuration
|
|
84
|
+
} = context;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Methods
|
|
89
|
+
|
|
90
|
+
- **`context.report(descriptor)`**: Report a problem in the code
|
|
91
|
+
|
|
92
|
+
## Working with the AST
|
|
93
|
+
|
|
94
|
+
ESLint uses the [ESTree](https://github.com/estree/estree) AST format. The `create()` function returns an object with visitor methods:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
create(context) {
|
|
98
|
+
return {
|
|
99
|
+
// Visit node while going down the tree
|
|
100
|
+
Identifier(node) {
|
|
101
|
+
// Handle Identifier nodes
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Visit node while going up the tree
|
|
105
|
+
"FunctionExpression:exit"(node) {
|
|
106
|
+
// Handle function exits
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Use selectors for more specific matching
|
|
110
|
+
"IfStatement > BlockStatement"(node) {
|
|
111
|
+
// Handle if statements with block bodies
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Code path analysis
|
|
115
|
+
onCodePathStart(codePath, node) {
|
|
116
|
+
// Start of code path
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
onCodePathEnd(codePath, node) {
|
|
120
|
+
// End of code path
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Exploring the AST
|
|
127
|
+
|
|
128
|
+
Use [Code Explorer](http://explorer.eslint.org/) to visualize AST structure for any JavaScript code.
|
|
129
|
+
|
|
130
|
+
## Reporting Problems
|
|
131
|
+
|
|
132
|
+
### Basic Reporting
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
context.report({
|
|
136
|
+
node: node,
|
|
137
|
+
message: "Unexpected identifier",
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Using Message IDs (Recommended)
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// In meta.messages
|
|
145
|
+
meta: {
|
|
146
|
+
messages: {
|
|
147
|
+
avoidName: "Avoid using variables named '{{name}}'";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// In create function
|
|
152
|
+
context.report({
|
|
153
|
+
node,
|
|
154
|
+
messageId: "avoidName",
|
|
155
|
+
data: {
|
|
156
|
+
name: node.name,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Report Descriptor Properties
|
|
162
|
+
|
|
163
|
+
- **`messageId`**: ID from `meta.messages` (recommended)
|
|
164
|
+
- **`message`**: Direct message string (alternative to messageId)
|
|
165
|
+
- **`node`**: AST node related to the problem
|
|
166
|
+
- **`loc`**: Specific location object (overrides node location)
|
|
167
|
+
- `start`: `{ line: number, column: number }`
|
|
168
|
+
- `end`: `{ line: number, column: number }`
|
|
169
|
+
- **`data`**: Placeholder values for message template
|
|
170
|
+
- **`fix`**: Function to apply automatic fix
|
|
171
|
+
- **`suggest`**: Array of manual fix suggestions
|
|
172
|
+
|
|
173
|
+
## Applying Fixes
|
|
174
|
+
|
|
175
|
+
### Basic Fix Example
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
context.report({
|
|
179
|
+
node,
|
|
180
|
+
message: "Missing semicolon",
|
|
181
|
+
fix(fixer) {
|
|
182
|
+
return fixer.insertTextAfter(node, ";");
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Fixer Methods
|
|
188
|
+
|
|
189
|
+
- `insertTextAfter(nodeOrToken, text)`
|
|
190
|
+
- `insertTextAfterRange(range, text)`
|
|
191
|
+
- `insertTextBefore(nodeOrToken, text)`
|
|
192
|
+
- `insertTextBeforeRange(range, text)`
|
|
193
|
+
- `remove(nodeOrToken)`
|
|
194
|
+
- `removeRange(range)`
|
|
195
|
+
- `replaceText(nodeOrToken, text)`
|
|
196
|
+
- `replaceTextRange(range, text)`
|
|
197
|
+
|
|
198
|
+
### Multiple Fixes
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
fix(fixer) {
|
|
202
|
+
return [
|
|
203
|
+
fixer.insertTextBefore(node, "const "),
|
|
204
|
+
fixer.insertTextAfter(node, " = value")
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Or use a generator
|
|
209
|
+
*fix(fixer) {
|
|
210
|
+
yield fixer.insertTextBefore(node, "const ");
|
|
211
|
+
yield fixer.insertTextAfter(node, " = value");
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Fix Best Practices
|
|
216
|
+
|
|
217
|
+
1. **Don't change runtime behavior**: Fixes should be safe transformations
|
|
218
|
+
2. **Make fixes small**: Avoid large refactorings that might conflict
|
|
219
|
+
3. **One fix per message**: Return result of fixer operation
|
|
220
|
+
4. **Don't worry about style**: Other rules will clean up after initial fixes
|
|
221
|
+
|
|
222
|
+
## Providing Suggestions
|
|
223
|
+
|
|
224
|
+
Suggestions are manual fixes that users can apply through their editor:
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
meta: {
|
|
228
|
+
hasSuggestions: true,
|
|
229
|
+
messages: {
|
|
230
|
+
unnecessaryEscape: "Unnecessary escape: \\{{char}}",
|
|
231
|
+
removeEscape: "Remove the `\\`",
|
|
232
|
+
escapeBackslash: "Replace with `\\\\`"
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// In create()
|
|
237
|
+
context.report({
|
|
238
|
+
node,
|
|
239
|
+
messageId: "unnecessaryEscape",
|
|
240
|
+
data: { char },
|
|
241
|
+
suggest: [
|
|
242
|
+
{
|
|
243
|
+
messageId: "removeEscape",
|
|
244
|
+
fix(fixer) {
|
|
245
|
+
return fixer.removeRange(range);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
messageId: "escapeBackslash",
|
|
250
|
+
fix(fixer) {
|
|
251
|
+
return fixer.insertTextBeforeRange(range, "\\");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Suggestion Best Practices
|
|
259
|
+
|
|
260
|
+
1. **Don't suggest large refactors**: Keep suggestions focused
|
|
261
|
+
2. **Don't conform to user styles**: Suggestions are stand-alone changes
|
|
262
|
+
3. **Provide meaningful descriptions**: Use clear messageIds or desc
|
|
263
|
+
|
|
264
|
+
## Rule Options and Schema
|
|
265
|
+
|
|
266
|
+
### Array Schema Format
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
meta: {
|
|
270
|
+
schema: [
|
|
271
|
+
{
|
|
272
|
+
enum: ["always", "never"],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
exceptRange: { type: "boolean" },
|
|
278
|
+
},
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Valid configs:
|
|
285
|
+
// ["error"]
|
|
286
|
+
// ["error", "always"]
|
|
287
|
+
// ["error", "never", { exceptRange: true }]
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Object Schema Format
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
meta: {
|
|
294
|
+
schema: {
|
|
295
|
+
type: "array",
|
|
296
|
+
minItems: 0,
|
|
297
|
+
maxItems: 2,
|
|
298
|
+
items: [
|
|
299
|
+
{ enum: ["always", "never"] },
|
|
300
|
+
{
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
exceptRange: { type: "boolean" }
|
|
304
|
+
},
|
|
305
|
+
additionalProperties: false
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Default Options
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
meta: {
|
|
316
|
+
defaultOptions: [
|
|
317
|
+
{
|
|
318
|
+
alias: "basic",
|
|
319
|
+
threshold: 10
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
schema: [
|
|
323
|
+
{
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
alias: { type: "string" },
|
|
327
|
+
threshold: { type: "number" }
|
|
328
|
+
},
|
|
329
|
+
additionalProperties: false
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// User config ["error", { threshold: 20 }] results in:
|
|
335
|
+
// options[0] = { alias: "basic", threshold: 20 }
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Accessing Source Code
|
|
339
|
+
|
|
340
|
+
### Getting Source Code Object
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
create(context) {
|
|
344
|
+
const sourceCode = context.sourceCode;
|
|
345
|
+
|
|
346
|
+
// Get all source text
|
|
347
|
+
const allSource = sourceCode.getText();
|
|
348
|
+
|
|
349
|
+
// Get text for specific node
|
|
350
|
+
const nodeText = sourceCode.getText(node);
|
|
351
|
+
|
|
352
|
+
// Get text with surrounding context
|
|
353
|
+
const withPrev = sourceCode.getText(node, 2); // 2 chars before
|
|
354
|
+
const withNext = sourceCode.getText(node, 0, 2); // 2 chars after
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Working with Tokens
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
// Get tokens
|
|
362
|
+
const firstToken = sourceCode.getFirstToken(node);
|
|
363
|
+
const lastToken = sourceCode.getLastToken(node);
|
|
364
|
+
const tokenAfter = sourceCode.getTokenAfter(node);
|
|
365
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
|
366
|
+
const allTokens = sourceCode.getTokens(node);
|
|
367
|
+
|
|
368
|
+
// Get tokens between nodes
|
|
369
|
+
const tokensBetween = sourceCode.getTokensBetween(node1, node2);
|
|
370
|
+
|
|
371
|
+
// Skip options
|
|
372
|
+
const token = sourceCode.getTokenAfter(node, {
|
|
373
|
+
skip: 2, // Skip 2 tokens
|
|
374
|
+
includeComments: true, // Include comment tokens
|
|
375
|
+
filter: (token) => token.type !== "Punctuator",
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Working with Comments
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
// Get all comments
|
|
383
|
+
const allComments = sourceCode.getAllComments();
|
|
384
|
+
|
|
385
|
+
// Get comments around a node
|
|
386
|
+
const commentsBefore = sourceCode.getCommentsBefore(node);
|
|
387
|
+
const commentsAfter = sourceCode.getCommentsAfter(node);
|
|
388
|
+
const commentsInside = sourceCode.getCommentsInside(node);
|
|
389
|
+
|
|
390
|
+
// Check if comments exist
|
|
391
|
+
const hasComments = sourceCode.commentsExistBetween(node1, node2);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### SourceCode Properties
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
sourceCode.text; // Full source text
|
|
398
|
+
sourceCode.ast; // Program node
|
|
399
|
+
sourceCode.lines; // Array of lines
|
|
400
|
+
sourceCode.hasBOM; // Has Unicode BOM
|
|
401
|
+
sourceCode.scopeManager; // Scope manager
|
|
402
|
+
sourceCode.visitorKeys; // Visitor keys for traversal
|
|
403
|
+
sourceCode.parserServices; // Parser-specific services
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Variable Scopes
|
|
407
|
+
|
|
408
|
+
### Getting Scope
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
create(context) {
|
|
412
|
+
return {
|
|
413
|
+
Identifier(node) {
|
|
414
|
+
const scope = context.sourceCode.getScope(node);
|
|
415
|
+
|
|
416
|
+
// Check variables in scope
|
|
417
|
+
scope.variables.forEach(variable => {
|
|
418
|
+
console.log(variable.name);
|
|
419
|
+
|
|
420
|
+
// Check references
|
|
421
|
+
variable.references.forEach(ref => {
|
|
422
|
+
if (ref.isWrite()) {
|
|
423
|
+
// Variable is written to
|
|
424
|
+
}
|
|
425
|
+
if (ref.isRead()) {
|
|
426
|
+
// Variable is read from
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Check definitions
|
|
431
|
+
variable.defs.forEach(def => {
|
|
432
|
+
console.log(def.type); // "Variable", "FunctionName", etc.
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Scope Types
|
|
441
|
+
|
|
442
|
+
| AST Node Type | Scope Type |
|
|
443
|
+
| ----------------------- | ---------- |
|
|
444
|
+
| Program | global |
|
|
445
|
+
| FunctionDeclaration | function |
|
|
446
|
+
| FunctionExpression | function |
|
|
447
|
+
| ArrowFunctionExpression | function |
|
|
448
|
+
| ClassDeclaration | class |
|
|
449
|
+
| ClassExpression | class |
|
|
450
|
+
| BlockStatement | block |
|
|
451
|
+
| ForStatement | for |
|
|
452
|
+
| SwitchStatement | switch |
|
|
453
|
+
| CatchClause | catch |
|
|
454
|
+
|
|
455
|
+
### Marking Variables as Used
|
|
456
|
+
|
|
457
|
+
```javascript
|
|
458
|
+
// Mark variable as used in current scope
|
|
459
|
+
context.sourceCode.markVariableAsUsed("myVar", node);
|
|
460
|
+
|
|
461
|
+
// Mark variable as used in global scope
|
|
462
|
+
context.sourceCode.markVariableAsUsed("myVar");
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Testing Rules
|
|
466
|
+
|
|
467
|
+
Use ESLint's `RuleTester` for testing:
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
const RuleTester = require("eslint").RuleTester;
|
|
471
|
+
const rule = require("../rules/my-rule");
|
|
472
|
+
|
|
473
|
+
const ruleTester = new RuleTester({
|
|
474
|
+
languageOptions: {
|
|
475
|
+
ecmaVersion: 2022,
|
|
476
|
+
sourceType: "module",
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
ruleTester.run("my-rule", rule, {
|
|
481
|
+
valid: [
|
|
482
|
+
"var foo = 'bar';",
|
|
483
|
+
{
|
|
484
|
+
code: "const foo = 'bar';",
|
|
485
|
+
options: ["always"],
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
|
|
489
|
+
invalid: [
|
|
490
|
+
{
|
|
491
|
+
code: "var foo = 'bar';",
|
|
492
|
+
errors: [
|
|
493
|
+
{
|
|
494
|
+
messageId: "unexpected",
|
|
495
|
+
type: "VariableDeclaration",
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
code: "var foo = 'bar';",
|
|
501
|
+
output: "const foo = 'bar';", // Expected fix output
|
|
502
|
+
errors: [
|
|
503
|
+
{
|
|
504
|
+
messageId: "useConst",
|
|
505
|
+
suggest: [
|
|
506
|
+
{
|
|
507
|
+
messageId: "useConstSuggestion",
|
|
508
|
+
output: "const foo = 'bar';",
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## Best Practices
|
|
519
|
+
|
|
520
|
+
### General
|
|
521
|
+
|
|
522
|
+
1. **Use messageIds**: More maintainable than inline messages
|
|
523
|
+
2. **Document thoroughly**: Include examples in `meta.docs`
|
|
524
|
+
3. **Test extensively**: Cover edge cases and error conditions
|
|
525
|
+
4. **Follow naming conventions**: Use kebab-case for rule names
|
|
526
|
+
5. **Keep rules focused**: One rule, one responsibility
|
|
527
|
+
|
|
528
|
+
### Fixes
|
|
529
|
+
|
|
530
|
+
1. **Safe transformations only**: Never change runtime behavior
|
|
531
|
+
2. **Minimal changes**: Smallest possible fix
|
|
532
|
+
3. **No style enforcement in fixes**: Let other rules handle formatting
|
|
533
|
+
4. **Test fix output**: Ensure fixes produce valid code
|
|
534
|
+
|
|
535
|
+
### Suggestions
|
|
536
|
+
|
|
537
|
+
1. **Manual actions only**: Use suggestions for behavior-changing fixes
|
|
538
|
+
2. **Clear descriptions**: Users should understand what will happen
|
|
539
|
+
3. **Limited scope**: Don't suggest large refactorings
|
|
540
|
+
|
|
541
|
+
### Performance
|
|
542
|
+
|
|
543
|
+
1. **Limit AST traversals**: Use specific selectors
|
|
544
|
+
2. **Cache computed values**: Don't recalculate in each visitor
|
|
545
|
+
3. **Early returns**: Exit visitor functions when possible
|
|
546
|
+
4. **Profile if needed**: Use `TIMING=1` environment variable
|
|
547
|
+
|
|
548
|
+
### Schema
|
|
549
|
+
|
|
550
|
+
1. **Always define schema**: Even if empty (`schema: []`)
|
|
551
|
+
2. **Validate thoroughly**: Prevent invalid configurations
|
|
552
|
+
3. **Provide defaults**: Use `defaultOptions` for common cases
|
|
553
|
+
4. **Document options**: Include in rule documentation
|
|
554
|
+
|
|
555
|
+
## Additional Resources
|
|
556
|
+
|
|
557
|
+
- [ESLint Official Custom Rules Documentation](https://eslint.org/docs/latest/extend/custom-rules)
|
|
558
|
+
- [Code Explorer](http://explorer.eslint.org/) - Visualize AST
|
|
559
|
+
- [ESTree Specification](https://github.com/estree/estree)
|
|
560
|
+
- [Scope Manager Interface](https://eslint.org/docs/latest/extend/scope-manager-interface)
|
|
561
|
+
- [Code Path Analysis](https://eslint.org/docs/latest/extend/code-path-analysis)
|
|
562
|
+
- [Selectors](https://eslint.org/docs/latest/extend/selectors)
|
|
563
|
+
|
|
564
|
+
## Example: Complete Rule
|
|
565
|
+
|
|
566
|
+
```javascript
|
|
567
|
+
/**
|
|
568
|
+
* @fileoverview Disallow var, prefer const or let
|
|
569
|
+
* @story docs/stories/example.story.md
|
|
570
|
+
*/
|
|
571
|
+
|
|
572
|
+
module.exports = {
|
|
573
|
+
meta: {
|
|
574
|
+
type: "suggestion",
|
|
575
|
+
docs: {
|
|
576
|
+
description: "Disallow var, prefer const or let",
|
|
577
|
+
url: "https://example.com/rules/no-var",
|
|
578
|
+
},
|
|
579
|
+
fixable: "code",
|
|
580
|
+
hasSuggestions: true,
|
|
581
|
+
schema: [
|
|
582
|
+
{
|
|
583
|
+
type: "object",
|
|
584
|
+
properties: {
|
|
585
|
+
allowInLegacy: { type: "boolean" },
|
|
586
|
+
},
|
|
587
|
+
additionalProperties: false,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
defaultOptions: [{ allowInLegacy: false }],
|
|
591
|
+
messages: {
|
|
592
|
+
unexpectedVar: "Unexpected var, use const or let instead",
|
|
593
|
+
replaceWithLet: "Replace with 'let'",
|
|
594
|
+
replaceWithConst: "Replace with 'const'",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
create(context) {
|
|
599
|
+
const sourceCode = context.sourceCode;
|
|
600
|
+
const options = context.options[0] || {};
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
VariableDeclaration(node) {
|
|
604
|
+
if (node.kind !== "var") {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Check if in legacy file
|
|
609
|
+
if (options.allowInLegacy && isLegacyFile(context.filename)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const varToken = sourceCode.getFirstToken(node);
|
|
614
|
+
|
|
615
|
+
context.report({
|
|
616
|
+
node,
|
|
617
|
+
messageId: "unexpectedVar",
|
|
618
|
+
fix(fixer) {
|
|
619
|
+
// Auto-fix to let (conservative)
|
|
620
|
+
return fixer.replaceText(varToken, "let");
|
|
621
|
+
},
|
|
622
|
+
suggest: [
|
|
623
|
+
{
|
|
624
|
+
messageId: "replaceWithLet",
|
|
625
|
+
fix(fixer) {
|
|
626
|
+
return fixer.replaceText(varToken, "let");
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
messageId: "replaceWithConst",
|
|
631
|
+
fix(fixer) {
|
|
632
|
+
return fixer.replaceText(varToken, "const");
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
});
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
function isLegacyFile(filename) {
|
|
643
|
+
return filename.includes("/legacy/");
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
## Rule Lifecycle
|
|
648
|
+
|
|
649
|
+
1. **Configuration**: Rule is enabled with severity and options
|
|
650
|
+
2. **Validation**: Options are validated against schema
|
|
651
|
+
3. **Creation**: `create()` is called with context
|
|
652
|
+
4. **Traversal**: Visitor methods called during AST traversal
|
|
653
|
+
5. **Reporting**: Problems reported via `context.report()`
|
|
654
|
+
6. **Fixing**: Fixes applied if `--fix` flag used
|
|
655
|
+
7. **Output**: Lint results returned to user
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
This guide covers the essential concepts for developing custom ESLint rules. For more advanced topics, refer to the official ESLint documentation and explore the source code of existing rules.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Title: Allow dynamic require in lint-plugin-check.js
|
|
2
|
+
Status: Accepted
|
|
3
|
+
Date: 2025-11-20
|
|
4
|
+
|
|
5
|
+
Context:
|
|
6
|
+
|
|
7
|
+
- The script lint-plugin-check.js inspects built plugin artifacts to validate metadata and exported rules.
|
|
8
|
+
- The script needs to load plugin bundles by path at runtime. The exact filenames/paths are not known at author-time and are produced by the build step.
|
|
9
|
+
- Static imports are not feasible for this use-case; dynamic require is the simplest reliable mechanism to load those built artifacts.
|
|
10
|
+
|
|
11
|
+
Decision:
|
|
12
|
+
|
|
13
|
+
- Permit the use of dynamic require for lint-plugin-check.js to load built plugin files by path at runtime.
|
|
14
|
+
- Restrict this allowance to this single script. All other code should continue to follow existing rules against dynamic requires.
|
|
15
|
+
- Reference and run this script from package.json scripts and CI tasks (for example, "scripts": { "lint:plugin:check": "node scripts/lint-plugin-check.js" }) so that plugin validation runs as part of development and CI workflows.
|
|
16
|
+
|
|
17
|
+
Consequences:
|
|
18
|
+
|
|
19
|
+
- Simplicity: dynamic require avoids complex build-time wiring for artifact paths.
|
|
20
|
+
- Security: acceptable because the script runs against artifacts produced by our own build in controlled environments (local dev / CI). Do not use the pattern to load arbitrary external code.
|
|
21
|
+
- Maintainability: keep the dynamic-require usage isolated and documented (this ADR) so future reviewers understand the rationale.
|
|
22
|
+
|
|
23
|
+
References:
|
|
24
|
+
|
|
25
|
+
- scripts/lint-plugin-check.js (usage and implementation)
|
|
26
|
+
- package.json scripts (referencing lint-plugin-check.js)
|