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.
Files changed (107) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +73 -32
  3. package/docs/ci-cd-pipeline.md +224 -0
  4. package/docs/cli-integration.md +22 -0
  5. package/docs/code-quality-refactor-opportunities-2025-12-03.md +78 -0
  6. package/docs/config-presets.md +38 -0
  7. package/docs/conventional-commits-guide.md +185 -0
  8. package/docs/custom-rules-development-guide.md +659 -0
  9. package/docs/decisions/0001-allow-dynamic-require-for-built-plugins.md +26 -0
  10. package/docs/decisions/001-typescript-for-eslint-plugin.accepted.md +111 -0
  11. package/docs/decisions/002-jest-for-eslint-testing.accepted.md +137 -0
  12. package/docs/decisions/003-code-quality-ratcheting-plan.md +48 -0
  13. package/docs/decisions/004-automated-version-bumping-for-ci-cd.md +196 -0
  14. package/docs/decisions/005-github-actions-validation-tooling.accepted.md +144 -0
  15. package/docs/decisions/006-semantic-release-for-automated-publishing.accepted.md +227 -0
  16. package/docs/decisions/007-github-releases-over-changelog.accepted.md +216 -0
  17. package/docs/decisions/008-ci-audit-flags.accepted.md +60 -0
  18. package/docs/decisions/009-security-focused-lint-rules.accepted.md +64 -0
  19. package/docs/decisions/010-implements-annotation-for-multi-story-requirements.proposed.md +184 -0
  20. package/docs/decisions/adr-0001-console-usage-for-cli-guards.md +190 -0
  21. package/docs/decisions/adr-accept-dev-dep-risk-glob.md +40 -0
  22. package/docs/decisions/adr-commit-branch-tests.md +54 -0
  23. package/docs/decisions/adr-maintenance-cli-interface.md +140 -0
  24. package/docs/decisions/adr-pre-push-parity.md +112 -0
  25. package/docs/decisions/code-quality-ratcheting-plan.md +53 -0
  26. package/docs/dependency-health.md +238 -0
  27. package/docs/eslint-9-setup-guide.md +517 -0
  28. package/docs/eslint-plugin-development-guide.md +487 -0
  29. package/docs/functionality-coverage-2025-12-03.md +250 -0
  30. package/docs/jest-testing-guide.md +100 -0
  31. package/docs/rules/prefer-implements-annotation.md +219 -0
  32. package/docs/rules/require-branch-annotation.md +71 -0
  33. package/docs/rules/require-req-annotation.md +203 -0
  34. package/docs/rules/require-story-annotation.md +159 -0
  35. package/docs/rules/valid-annotation-format.md +418 -0
  36. package/docs/rules/valid-req-reference.md +153 -0
  37. package/docs/rules/valid-story-reference.md +120 -0
  38. package/docs/security-incidents/2025-11-17-glob-cli-incident.md +45 -0
  39. package/docs/security-incidents/2025-11-18-brace-expansion-redos.md +45 -0
  40. package/docs/security-incidents/2025-11-18-bundled-dev-deps-accepted-risk.md +93 -0
  41. package/docs/security-incidents/2025-11-18-tar-race-condition.md +43 -0
  42. package/docs/security-incidents/2025-12-03-dependency-health-review.md +58 -0
  43. package/docs/security-incidents/SECURITY-INCIDENT-2025-11-18-semantic-release-bundled-npm.known-error.md +104 -0
  44. package/docs/security-incidents/SECURITY-INCIDENT-TEMPLATE.md +37 -0
  45. package/docs/security-incidents/dependency-override-rationale.md +57 -0
  46. package/docs/security-incidents/dev-deps-high.json +116 -0
  47. package/docs/security-incidents/handling-procedure.md +54 -0
  48. package/docs/stories/001.0-DEV-PLUGIN-SETUP.story.md +92 -0
  49. package/docs/stories/002.0-DEV-ESLINT-CONFIG.story.md +82 -0
  50. package/docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md +112 -0
  51. package/docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md +153 -0
  52. package/docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md +138 -0
  53. package/docs/stories/006.0-DEV-FILE-VALIDATION.story.md +144 -0
  54. package/docs/stories/007.0-DEV-ERROR-REPORTING.story.md +163 -0
  55. package/docs/stories/008.0-DEV-AUTO-FIX.story.md +150 -0
  56. package/docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md +117 -0
  57. package/docs/stories/010.0-DEV-DEEP-VALIDATION.story.md +124 -0
  58. package/docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md +149 -0
  59. package/docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md +216 -0
  60. package/docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md +236 -0
  61. package/docs/stories/developer-story.map.md +120 -0
  62. package/docs/ts-jest-presets-guide.md +548 -0
  63. package/lib/src/index.d.ts +2 -2
  64. package/lib/src/index.js +2 -0
  65. package/lib/src/maintenance/batch.d.ts +5 -0
  66. package/lib/src/maintenance/batch.js +5 -0
  67. package/lib/src/maintenance/cli.js +34 -212
  68. package/lib/src/maintenance/commands.d.ts +32 -0
  69. package/lib/src/maintenance/commands.js +139 -0
  70. package/lib/src/maintenance/detect.d.ts +2 -0
  71. package/lib/src/maintenance/detect.js +4 -0
  72. package/lib/src/maintenance/flags.d.ts +99 -0
  73. package/lib/src/maintenance/flags.js +121 -0
  74. package/lib/src/maintenance/report.d.ts +2 -0
  75. package/lib/src/maintenance/report.js +2 -0
  76. package/lib/src/maintenance/update.d.ts +4 -0
  77. package/lib/src/maintenance/update.js +4 -0
  78. package/lib/src/rules/helpers/require-story-io.d.ts +3 -0
  79. package/lib/src/rules/helpers/require-story-io.js +20 -6
  80. package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
  81. package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
  82. package/lib/src/rules/helpers/valid-annotation-options.js +15 -4
  83. package/lib/src/rules/helpers/valid-annotation-utils.js +5 -0
  84. package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
  85. package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
  86. package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +3 -4
  87. package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
  88. package/lib/src/rules/prefer-implements-annotation.js +276 -0
  89. package/lib/src/rules/valid-annotation-format.js +87 -28
  90. package/lib/src/rules/valid-req-reference.js +71 -0
  91. package/lib/src/utils/reqAnnotationDetection.d.ts +4 -1
  92. package/lib/src/utils/reqAnnotationDetection.js +43 -15
  93. package/lib/tests/maintenance/cli.test.js +89 -0
  94. package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
  95. package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
  96. package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
  97. package/lib/tests/rules/require-req-annotation.test.js +8 -1
  98. package/lib/tests/rules/require-story-annotation.test.js +9 -4
  99. package/lib/tests/rules/valid-annotation-format.test.js +78 -0
  100. package/lib/tests/rules/valid-req-reference.test.js +34 -0
  101. package/lib/tests/utils/ts-language-options.d.ts +1 -7
  102. package/lib/tests/utils/ts-language-options.js +8 -5
  103. package/package.json +7 -3
  104. package/user-docs/api-reference.md +507 -0
  105. package/user-docs/eslint-9-setup-guide.md +639 -0
  106. package/user-docs/examples.md +74 -0
  107. 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)