eslint-plugin-traceability 1.17.1 → 1.19.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 +3 -3
- package/README.md +107 -11
- package/lib/src/rules/no-redundant-annotation.js +8 -1
- package/lib/src/rules/require-branch-annotation.js +16 -0
- package/lib/src/utils/branch-annotation-helpers.d.ts +8 -1
- package/lib/src/utils/branch-annotation-helpers.js +62 -4
- package/lib/src/utils/branch-annotation-report-helpers.d.ts +1 -0
- package/lib/src/utils/branch-annotation-report-helpers.js +84 -19
- package/lib/tests/rules/no-redundant-annotation.test.js +10 -0
- package/lib/tests/rules/require-branch-annotation.test.js +36 -0
- package/lib/tests/utils/branch-annotation-helpers.test.js +52 -0
- package/package.json +1 -1
- package/user-docs/api-reference.md +6 -1
- package/user-docs/examples.md +32 -0
- package/user-docs/migration-guide.md +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# [1.19.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.18.0...v1.19.0) (2025-12-18)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* enforce inside-brace placement mode for branch annotations ([5c2129a](https://github.com/voder-ai/eslint-plugin-traceability/commit/5c2129a7c25717dc4ce14bf55bc4541ae07e5539))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# eslint-plugin-traceability
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An ESLint plugin that creates searchable verification indices in your code, enabling systematic validation of requirement-to-code mapping.
|
|
4
|
+
|
|
5
|
+
**The Core Value:** Transforms an impossible question ("Does code exist somewhere that should support requirement X?") into a tractable question ("Does this annotated code actually support the requirement it claims to?").
|
|
6
|
+
|
|
7
|
+
The plugin requires `@supports` annotations at key verification points (functions and control flow branches), creating direct checkpoints where each annotation is a verifiable claim you can search, find, and verify locally without needing to understand the broader codebase context.
|
|
4
8
|
|
|
5
9
|
## Attribution
|
|
6
10
|
|
|
@@ -89,21 +93,89 @@ export default [
|
|
|
89
93
|
|
|
90
94
|
The plugin exposes several rules. For **new configurations**, the unified function-level rule and `@supports` annotations are the canonical choice; the `@story` and `@req` forms remain available primarily for backward compatibility and gradual migration.
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- `traceability/require-
|
|
95
|
-
|
|
96
|
-
- `traceability/
|
|
97
|
-
|
|
98
|
-
- `traceability/
|
|
99
|
-
|
|
100
|
-
- `traceability/
|
|
101
|
-
|
|
96
|
+
#### Core Verification Index Rules
|
|
97
|
+
|
|
98
|
+
- `traceability/require-traceability` – **Unified function-level verification rule.** Ensures every function has a verification checkpoint. Without this, functions could exist without explaining their purpose, breaking verification completeness. Accepts either `@supports` (preferred for new code) or legacy `@story` / `@req` annotations and is enabled by default in the plugin's `recommended` and `strict` presets.
|
|
99
|
+
|
|
100
|
+
- `traceability/require-story-annotation` – Legacy function-level rule key that focuses on the **story** side of function-level verification. It is kept for backward compatibility and is wired to the same underlying engine as `traceability/require-traceability`. New configurations should normally rely on `traceability/require-traceability` instead.
|
|
101
|
+
|
|
102
|
+
- `traceability/require-req-annotation` – Legacy function-level rule key that focuses on the **requirement** side of function-level verification. Like `traceability/require-story-annotation`, it is retained for backward compatibility. New configurations can usually rely on the unified rule alone unless you have specific reasons to tune the legacy keys separately.
|
|
103
|
+
|
|
104
|
+
- `traceability/require-branch-annotation` – **Creates verification checkpoints at every branch** (if/else/switch/try/catch). This enables local verification - you can check each branch independently without understanding the entire function. Without branch annotations, you must read entire functions to find branch logic. With branch annotations, you can verify each branch by searching for the requirement ID. Branch annotations can use `@supports` (preferred) or the older `@story`/`@req` pair for backward compatibility.
|
|
105
|
+
|
|
106
|
+
- `traceability/valid-annotation-format` – **Ensures annotations are searchable** with consistent patterns. Verification workflows depend on being able to search for requirement IDs reliably. Enforces correct format of traceability annotations, including `@supports` (preferred), `@story`, and `@req`.
|
|
107
|
+
|
|
108
|
+
- `traceability/valid-story-reference` – **Validates story file references** to ensure verification checkpoints point to real documentation. Validates that story references (whether written via `@story` or embedded in `@supports`) point to existing story files.
|
|
109
|
+
|
|
110
|
+
- `traceability/valid-req-reference` – **Validates requirement identifiers** to ensure verification checkpoints reference valid requirements. Validates that requirement identifiers (whether written via `@req` or embedded in `@supports`) point to existing requirement IDs in your story files.
|
|
111
|
+
|
|
112
|
+
- `traceability/require-test-traceability` – **Enforces verification conventions in test files** by requiring file-level `@supports` annotations, story references in `describe` blocks, and `[REQ-...]` prefixes in `it`/`test` names. This ensures tests are traceable back to requirements for comprehensive verification coverage.
|
|
113
|
+
|
|
114
|
+
#### Quality Rules
|
|
115
|
+
|
|
116
|
+
- `traceability/no-redundant-annotation` – **Removes annotations on simple statements** already covered by enclosing scope. These don't create useful verification checkpoints (no branches to verify), so removing them reduces noise without hurting verifiability. It is enabled at severity `warn` in both the `recommended` and `strict` presets by default; consumers can override its severity or disable it explicitly.
|
|
117
|
+
|
|
118
|
+
- `traceability/prefer-supports-annotation` – **Optional migration helper** that recommends converting legacy single-story `@story`/`@req` JSDoc blocks and inline comments into the newer `@supports` format for better verification trails. It is disabled by default and must be explicitly enabled. The legacy rule name `traceability/prefer-implements-annotation` remains available as a deprecated alias.
|
|
102
119
|
|
|
103
120
|
Configuration options: For detailed per-rule options (such as scopes, branch types, and story directory settings), see the individual rule docs in the plugin's user guide and the consolidated [API Reference](user-docs/api-reference.md).
|
|
104
121
|
|
|
105
122
|
For development and contribution guidelines, see the contribution guide in the repository.
|
|
106
123
|
|
|
124
|
+
## How It Works
|
|
125
|
+
|
|
126
|
+
### The Verification Workflow
|
|
127
|
+
|
|
128
|
+
Traceability annotations enable systematic requirement verification through a simple three-step process:
|
|
129
|
+
|
|
130
|
+
1. **Search:** `grep -r "REQ-AUTH-VALIDATION" src/`
|
|
131
|
+
2. **Find:** Get a list of every location claiming to support that requirement
|
|
132
|
+
3. **Verify:** For each result, read the annotation and adjacent code to confirm they match
|
|
133
|
+
|
|
134
|
+
### Why Annotations at Every Branch
|
|
135
|
+
|
|
136
|
+
Each annotation creates a **local verification checkpoint**:
|
|
137
|
+
|
|
138
|
+
- ✅ **Searchable** - Direct search finds all implementation claims
|
|
139
|
+
- ✅ **Local** - Annotation is adjacent to the code being verified
|
|
140
|
+
- ✅ **No context needed** - Verify the claim without understanding control flow
|
|
141
|
+
- ✅ **Parallelizable** - Multiple reviewers work independently
|
|
142
|
+
- ✅ **Complete coverage** - No code can exist without explaining its purpose
|
|
143
|
+
|
|
144
|
+
This enables you to verify requirements systematically rather than trying to remember which code should implement what.
|
|
145
|
+
|
|
146
|
+
### Example: Verifying a Switch Statement
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
switch (severity) {
|
|
150
|
+
// @supports docs/stories/logging.md REQ-SEVERITY-LEVELS
|
|
151
|
+
case "low":
|
|
152
|
+
logLevel = "info";
|
|
153
|
+
break;
|
|
154
|
+
// @supports docs/stories/logging.md REQ-SEVERITY-LEVELS
|
|
155
|
+
case "moderate":
|
|
156
|
+
logLevel = "warn";
|
|
157
|
+
break;
|
|
158
|
+
// @supports docs/stories/logging.md REQ-SEVERITY-LEVELS
|
|
159
|
+
case "high":
|
|
160
|
+
logLevel = "error";
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Verification process:**
|
|
166
|
+
|
|
167
|
+
- Search finds 3 annotations
|
|
168
|
+
- Verify each: "Does this case handle REQ-SEVERITY-LEVELS correctly?"
|
|
169
|
+
- Time: ~30 seconds per case, ~90 seconds total
|
|
170
|
+
- Can be split across team members
|
|
171
|
+
|
|
172
|
+
**Without explicit annotations:**
|
|
173
|
+
|
|
174
|
+
- Must read entire switch to understand what it does
|
|
175
|
+
- Must remember which requirement you're checking
|
|
176
|
+
- Cannot split the work effectively
|
|
177
|
+
- Risk missing cases or misunderstanding intent
|
|
178
|
+
|
|
107
179
|
## Quick Start
|
|
108
180
|
|
|
109
181
|
1. Create a flat ESLint config file (`eslint.config.js`):
|
|
@@ -143,6 +215,30 @@ function initAuth() {
|
|
|
143
215
|
npx eslint "src/**/*.js"
|
|
144
216
|
```
|
|
145
217
|
|
|
218
|
+
## Common Misconceptions
|
|
219
|
+
|
|
220
|
+
### "The annotations are redundant"
|
|
221
|
+
|
|
222
|
+
**Reality:** The apparent redundancy is intentional indexing. Each annotation is a separate verification checkpoint. When the same requirement appears in multiple branches, each annotation creates an independent checkpoint that can be verified in ~30 seconds without understanding the broader code structure.
|
|
223
|
+
|
|
224
|
+
Removing "duplicate" annotations would break the verification workflow by forcing reviewers to trace through code structure instead of verifying local claims.
|
|
225
|
+
|
|
226
|
+
### "This only works for small codebases"
|
|
227
|
+
|
|
228
|
+
**Reality:** The plugin is specifically designed for codebases where manual review is impractical. Small codebases could use manual review; larger ones need systematic, searchable verification indices to make requirement validation tractable.
|
|
229
|
+
|
|
230
|
+
### "This is just documentation"
|
|
231
|
+
|
|
232
|
+
**Reality:** These are verification indices, not documentation. The goal is searchability and local verification, not explaining code. Documentation aims to reduce redundancy; verification indices intentionally create checkpoints for independent validation.
|
|
233
|
+
|
|
234
|
+
### "Inheritance would reduce boilerplate"
|
|
235
|
+
|
|
236
|
+
**Reality:** Inheritance would break verification tractability. When you search for a requirement, you need to find every place that implements it - not find a parent annotation and manually trace what inherits from it. Direct, explicit annotations at each implementation point enable grep-based verification.
|
|
237
|
+
|
|
238
|
+
## Verification Workflow Guide
|
|
239
|
+
|
|
240
|
+
For detailed verification workflows, examples, and best practices, see the [Verification Workflow Guide](docs/verification-workflow-guide.md).
|
|
241
|
+
|
|
146
242
|
## API Reference
|
|
147
243
|
|
|
148
244
|
Detailed API specification and configuration options can be found in the [API Reference](user-docs/api-reference.md).
|
|
@@ -86,7 +86,14 @@ function getScopePairs(context, scopeNode, parent) {
|
|
|
86
86
|
const sourceCode = context.getSourceCode();
|
|
87
87
|
// Branch-style scope: use the branch helpers to collect comment text.
|
|
88
88
|
if (branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES.includes(scopeNode.type)) {
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Inside-brace annotations used as branch-level indicators (inside placement
|
|
91
|
+
* mode) should not be folded into scopePairs for redundancy purposes; only
|
|
92
|
+
* before-brace annotations define the covering scope here.
|
|
93
|
+
*
|
|
94
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-NON-REDUNDANT-INSIDE REQ-PLACEMENT-CONFIG
|
|
95
|
+
*/
|
|
96
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent, "before");
|
|
90
97
|
return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
|
|
91
98
|
}
|
|
92
99
|
const comments = getScopeCommentsFromJSDocAndLeading(sourceCode, scopeNode);
|
|
@@ -99,6 +99,12 @@ const rule = {
|
|
|
99
99
|
items: { type: "string" },
|
|
100
100
|
uniqueItems: true,
|
|
101
101
|
},
|
|
102
|
+
/**
|
|
103
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
|
|
104
|
+
*/
|
|
105
|
+
annotationPlacement: {
|
|
106
|
+
enum: ["before", "inside"],
|
|
107
|
+
},
|
|
102
108
|
},
|
|
103
109
|
additionalProperties: false,
|
|
104
110
|
},
|
|
@@ -124,6 +130,16 @@ const rule = {
|
|
|
124
130
|
return branchTypesOrListener;
|
|
125
131
|
}
|
|
126
132
|
const branchTypes = branchTypesOrListener;
|
|
133
|
+
/**
|
|
134
|
+
* Resolve annotation placement configuration with backward-compatible default.
|
|
135
|
+
*
|
|
136
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
|
|
137
|
+
*/
|
|
138
|
+
const rawOptions = context.options[0] || {};
|
|
139
|
+
const _annotationPlacement = rawOptions.annotationPlacement === "inside" ||
|
|
140
|
+
rawOptions.annotationPlacement === "before"
|
|
141
|
+
? rawOptions.annotationPlacement
|
|
142
|
+
: "before";
|
|
127
143
|
const storyFixCountRef = { count: 0 };
|
|
128
144
|
const handlers = {};
|
|
129
145
|
branchTypes.forEach((type) => {
|
|
@@ -10,6 +10,12 @@ export declare const DEFAULT_BRANCH_TYPES: readonly ["IfStatement", "SwitchCase"
|
|
|
10
10
|
* Type for branch nodes supported by require-branch-annotation rule.
|
|
11
11
|
*/
|
|
12
12
|
export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Placement options for branch annotations relative to their associated branch.
|
|
15
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
16
|
+
* @req REQ-PLACEMENT-CONFIG - Allow configuration of annotation placement (before/inside)
|
|
17
|
+
*/
|
|
18
|
+
export type AnnotationPlacement = "before" | "inside";
|
|
13
19
|
/**
|
|
14
20
|
* Validate branchTypes configuration option and return branch types to enforce,
|
|
15
21
|
* or return an ESLint listener if configuration is invalid.
|
|
@@ -33,8 +39,9 @@ export declare function scanCommentLinesInRange(lines: string[], startIndex: num
|
|
|
33
39
|
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
34
40
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
35
41
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
42
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
36
43
|
*/
|
|
37
|
-
export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any): string;
|
|
44
|
+
export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any, annotationPlacement?: AnnotationPlacement): string;
|
|
38
45
|
/**
|
|
39
46
|
* Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
|
|
40
47
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -195,6 +195,55 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
|
|
|
195
195
|
}
|
|
196
196
|
return beforeText;
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Gather annotation text for simple IfStatement branches, honoring the configured placement.
|
|
200
|
+
* When placement is "before", this helper preserves the existing behavior by returning the
|
|
201
|
+
* leading comment text unchanged. When placement is "inside", it switches to inside-brace
|
|
202
|
+
* semantics and scans for comments at the top of the consequent block.
|
|
203
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
204
|
+
* @supports REQ-INSIDE-BRACE-PLACEMENT
|
|
205
|
+
* @supports REQ-PLACEMENT-CONFIG
|
|
206
|
+
* @supports REQ-DEFAULT-BACKWARD-COMPAT
|
|
207
|
+
*/
|
|
208
|
+
function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText) {
|
|
209
|
+
if (annotationPlacement === "before") {
|
|
210
|
+
return beforeText;
|
|
211
|
+
}
|
|
212
|
+
if (annotationPlacement !== "inside") {
|
|
213
|
+
return beforeText;
|
|
214
|
+
}
|
|
215
|
+
if (!node.consequent || node.consequent.type !== "BlockStatement") {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
const consequent = node.consequent;
|
|
219
|
+
const getCommentsInside = sourceCode.getCommentsInside;
|
|
220
|
+
if (typeof getCommentsInside === "function") {
|
|
221
|
+
try {
|
|
222
|
+
const insideComments = getCommentsInside(consequent) || [];
|
|
223
|
+
const insideText = insideComments.map(extractCommentValue).join(" ");
|
|
224
|
+
if (insideText) {
|
|
225
|
+
return insideText;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// fall through to line-based fallback
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (consequent.loc &&
|
|
233
|
+
consequent.loc.start &&
|
|
234
|
+
consequent.loc.end &&
|
|
235
|
+
typeof consequent.loc.start.line === "number" &&
|
|
236
|
+
typeof consequent.loc.end.line === "number") {
|
|
237
|
+
const lines = sourceCode.lines;
|
|
238
|
+
const startIndex = consequent.loc.start.line - 1;
|
|
239
|
+
const endIndex = consequent.loc.end.line - 1;
|
|
240
|
+
const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
|
|
241
|
+
if (insideText) {
|
|
242
|
+
return insideText;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
198
247
|
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
199
248
|
function scanElseIfPrecedingComments(sourceCode, node) {
|
|
200
249
|
const lines = sourceCode.lines;
|
|
@@ -316,8 +365,9 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
|
|
|
316
365
|
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
317
366
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
318
367
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
368
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
319
369
|
*/
|
|
320
|
-
function gatherBranchCommentText(sourceCode, node, parent) {
|
|
370
|
+
function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement = "before") {
|
|
321
371
|
/**
|
|
322
372
|
* Conditional branch for SwitchCase nodes that may include inline comments.
|
|
323
373
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -332,13 +382,21 @@ function gatherBranchCommentText(sourceCode, node, parent) {
|
|
|
332
382
|
return gatherCatchClauseCommentText(sourceCode, node, beforeText);
|
|
333
383
|
}
|
|
334
384
|
/**
|
|
335
|
-
* Conditional branch for IfStatement
|
|
336
|
-
*
|
|
385
|
+
* Conditional branch for IfStatement nodes, distinguishing between else-if branches
|
|
386
|
+
* (which preserve dual-position behavior) and simple if-branches that can honor
|
|
387
|
+
* the configured annotation placement (before or inside braces).
|
|
337
388
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
389
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
338
390
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
391
|
+
* @supports REQ-INSIDE-BRACE-PLACEMENT
|
|
392
|
+
* @supports REQ-PLACEMENT-CONFIG
|
|
393
|
+
* @supports REQ-DEFAULT-BACKWARD-COMPAT
|
|
339
394
|
*/
|
|
340
395
|
if (node.type === "IfStatement") {
|
|
341
|
-
|
|
396
|
+
if (isElseIfBranch(node, parent)) {
|
|
397
|
+
return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
|
|
398
|
+
}
|
|
399
|
+
return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
|
|
342
400
|
}
|
|
343
401
|
/**
|
|
344
402
|
* Conditional branch for loop nodes that may include annotations either on the loop
|
|
@@ -5,6 +5,7 @@ import type { Rule } from "eslint";
|
|
|
5
5
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
6
6
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
7
7
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
8
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
8
9
|
*/
|
|
9
10
|
export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
|
|
10
11
|
count: number;
|
|
@@ -22,8 +22,11 @@ function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
|
|
|
22
22
|
});
|
|
23
23
|
return { indent, insertPos };
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
27
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
28
|
+
*/
|
|
29
|
+
function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement) {
|
|
27
30
|
let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
|
|
28
31
|
if (node.type === "CatchClause" && node.body) {
|
|
29
32
|
const bodyNode = node.body;
|
|
@@ -50,32 +53,87 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
|
50
53
|
return { indent, insertPos };
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @
|
|
56
|
-
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
57
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
58
|
-
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
56
|
+
* Determine whether a node represents an else-if branch that should be used for
|
|
57
|
+
* determining comment insertion position.
|
|
58
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
59
59
|
*/
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const missingStory = !/@story\b/.test(text) && !hasSupports;
|
|
64
|
-
const missingReq = !/@req\b/.test(text) && !hasSupports;
|
|
65
|
-
let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
|
|
66
|
-
if (node.type === "IfStatement" &&
|
|
60
|
+
function isElseIfBranchForInsert(node, parent) {
|
|
61
|
+
return (node &&
|
|
62
|
+
node.type === "IfStatement" &&
|
|
67
63
|
parent &&
|
|
68
64
|
parent.type === "IfStatement" &&
|
|
69
|
-
parent.alternate === node
|
|
70
|
-
|
|
65
|
+
parent.alternate === node);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compute indentation and insert position for IfStatement branches, handling
|
|
69
|
+
* both simple if and else-if cases, respecting the configured annotation
|
|
70
|
+
* placement and indentation rules.
|
|
71
|
+
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
72
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG REQ-INDENTATION-CORRECT
|
|
73
|
+
*/
|
|
74
|
+
function getIfStatementIndentAndInsertPos(sourceCode, node, options, context) {
|
|
75
|
+
const { parent, annotationPlacement } = options;
|
|
76
|
+
let { indent, insertPos } = context;
|
|
77
|
+
const hasBlockConsequent = node.consequent &&
|
|
71
78
|
node.consequent.type === "BlockStatement" &&
|
|
72
79
|
node.consequent.loc &&
|
|
73
|
-
node.consequent.loc.start
|
|
80
|
+
node.consequent.loc.start;
|
|
81
|
+
if (!hasBlockConsequent) {
|
|
82
|
+
return context;
|
|
83
|
+
}
|
|
84
|
+
const isElseIf = isElseIfBranchForInsert(node, parent);
|
|
85
|
+
const isSimpleIfInsidePlacement = annotationPlacement === "inside" && !isElseIf;
|
|
86
|
+
if (isSimpleIfInsidePlacement || isElseIf) {
|
|
74
87
|
const commentLine = node.consequent.loc.start.line + 1;
|
|
75
88
|
const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
|
|
76
89
|
indent = commentLineInfo.indent;
|
|
77
90
|
insertPos = commentLineInfo.insertPos;
|
|
91
|
+
context.indent = indent;
|
|
92
|
+
context.insertPos = insertPos;
|
|
78
93
|
}
|
|
94
|
+
return context;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compute which annotations are missing for a branch based on its gathered comment text.
|
|
98
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
99
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
100
|
+
*/
|
|
101
|
+
function getBranchMissingFlags(sourceCode, node, parent, annotationPlacement) {
|
|
102
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent, annotationPlacement);
|
|
103
|
+
const hasSupports = /@supports\b/.test(text);
|
|
104
|
+
const missingStory = !/@story\b/.test(text) && !hasSupports;
|
|
105
|
+
const missingReq = !/@req\b/.test(text) && !hasSupports;
|
|
106
|
+
return { missingStory, missingReq };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Compute indentation and insert position used for auto-fix insertion on a branch.
|
|
110
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
111
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
112
|
+
*/
|
|
113
|
+
function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement) {
|
|
114
|
+
const { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
|
|
115
|
+
if (node.type === "IfStatement") {
|
|
116
|
+
const context = { indent, insertPos };
|
|
117
|
+
const updatedContext = getIfStatementIndentAndInsertPos(sourceCode, node, { parent, annotationPlacement }, context);
|
|
118
|
+
return {
|
|
119
|
+
indent: updatedContext.indent,
|
|
120
|
+
insertPos: updatedContext.insertPos,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { indent, insertPos };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compute annotation-related metadata for a branch node.
|
|
127
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
128
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
129
|
+
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
130
|
+
* @supports REQ-DUAL-POSITION-DETECTION
|
|
131
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
132
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
133
|
+
*/
|
|
134
|
+
function getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement) {
|
|
135
|
+
const { missingStory, missingReq } = getBranchMissingFlags(sourceCode, node, parent, annotationPlacement);
|
|
136
|
+
const { indent, insertPos } = getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement);
|
|
79
137
|
return { missingStory, missingReq, indent, insertPos };
|
|
80
138
|
}
|
|
81
139
|
/**
|
|
@@ -84,11 +142,18 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
|
84
142
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
85
143
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
86
144
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
145
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
87
146
|
*/
|
|
88
147
|
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
89
148
|
const sourceCode = context.getSourceCode();
|
|
149
|
+
const rawOptions = context.options && context.options[0];
|
|
150
|
+
const annotationPlacement = rawOptions &&
|
|
151
|
+
(rawOptions.annotationPlacement === "inside" ||
|
|
152
|
+
rawOptions.annotationPlacement === "before")
|
|
153
|
+
? rawOptions.annotationPlacement
|
|
154
|
+
: "before";
|
|
90
155
|
const parent = node.parent;
|
|
91
|
-
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
|
|
156
|
+
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement);
|
|
92
157
|
const actions = [
|
|
93
158
|
{
|
|
94
159
|
missing: missingStory,
|
|
@@ -42,6 +42,10 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
|
|
|
42
42
|
name: "[REQ-CATCH-BLOCK-HANDLING] preserves catch block annotation from issue #6 scenario",
|
|
43
43
|
code: `async function example() {\n try {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (isSafeVersion({ version, vulnerabilityData })) {\n return version;\n }\n\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (!vulnerabilityData.isVulnerable) {\n return version;\n }\n } catch (error) {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n return null;\n }\n}`,
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
name: "[REQ-CATCH-BLOCK-HANDLING] preserves annotations in nested catch blocks with repeated requirements",
|
|
47
|
+
code: `async function nestedCatches() {\n try {\n await checkPrimary();\n } catch (outerError) {\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n try {\n await attemptRecovery(outerError);\n } catch (innerError) {\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n await reportFailure(innerError);\n }\n }\n}`,
|
|
48
|
+
},
|
|
45
49
|
],
|
|
46
50
|
invalid: [
|
|
47
51
|
{
|
|
@@ -92,6 +96,12 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
|
|
|
92
96
|
output: `/**\n * @story docs/stories/009.0-EXAMPLE.story.md\n * @supports REQ-SUP-A, REQ-SUP-B\n */\nfunction example() {\n const supported = checkSupport();\n}`,
|
|
93
97
|
errors: [{ messageId: "redundantAnnotation" }],
|
|
94
98
|
},
|
|
99
|
+
{
|
|
100
|
+
name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation in finally block that repeats try-path coverage",
|
|
101
|
+
code: `async function example() {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n try {\n await doWork();\n } finally {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n await cleanUp();\n }\n}`,
|
|
102
|
+
output: `async function example() {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n try {\n await doWork();\n } finally {\n await cleanUp();\n }\n}`,
|
|
103
|
+
errors: [{ messageId: "redundantAnnotation" }],
|
|
104
|
+
},
|
|
95
105
|
// TODO: rule implementation exists; full invalid-case behavior tests pending refinement
|
|
96
106
|
// {
|
|
97
107
|
// name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation on simple return inside annotated if",
|
|
@@ -8,15 +8,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
9
9
|
* @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
|
|
10
10
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
11
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
11
12
|
* @req REQ-BRANCH-DETECTION - Verify require-branch-annotation rule enforces branch annotations
|
|
12
13
|
* @req REQ-ERROR-SPECIFIC - Branch-level missing-annotation error messages are specific and informative
|
|
13
14
|
* @req REQ-ERROR-CONSISTENCY - Branch-level missing-annotation error messages follow shared conventions
|
|
14
15
|
* @req REQ-ERROR-SUGGESTION - Branch-level missing-annotation errors include suggestions when applicable
|
|
15
16
|
* @req REQ-NESTED-HANDLING - Nested branch annotations are correctly enforced without duplicative reporting
|
|
16
17
|
* @req REQ-SUPPORTS-ALTERNATIVE - Branches annotated only with @supports are treated as fully annotated
|
|
18
|
+
* @req REQ-PLACEMENT-CONFIG - Rule supports configurable annotation placement modes
|
|
19
|
+
* @req REQ-DEFAULT-BACKWARD-COMPAT - Default placement remains backward compatible with existing behavior
|
|
17
20
|
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION REQ-NESTED-HANDLING REQ-SUPPORTS-ALTERNATIVE
|
|
18
21
|
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SPECIFIC REQ-ERROR-CONSISTENCY REQ-ERROR-SUGGESTION
|
|
19
22
|
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF REQ-POSITION-PRIORITY-ELSE-IF REQ-PRETTIER-AUTOFIX-ELSE-IF
|
|
23
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
|
|
20
24
|
*/
|
|
21
25
|
const eslint_1 = require("eslint");
|
|
22
26
|
const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));
|
|
@@ -184,6 +188,22 @@ if (outer) {
|
|
|
184
188
|
if (condition) {}`,
|
|
185
189
|
options: [{ branchTypes: ["IfStatement", "SwitchCase"] }],
|
|
186
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: "[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] if-statement with before-brace annotations using annotationPlacement: 'before'",
|
|
193
|
+
code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
194
|
+
// @req REQ-PLACEMENT-CONFIG
|
|
195
|
+
if (condition) {}`,
|
|
196
|
+
options: [{ annotationPlacement: "before" }],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] if-statement annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
|
|
200
|
+
code: `if (condition) {
|
|
201
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
202
|
+
// @req REQ-INSIDE-BRACE-PLACEMENT
|
|
203
|
+
doSomething();
|
|
204
|
+
}`,
|
|
205
|
+
options: [{ annotationPlacement: "inside" }],
|
|
206
|
+
},
|
|
187
207
|
{
|
|
188
208
|
name: "[REQ-SUPPORTS-ALTERNATIVE] if-statement with only @supports annotation is treated as fully annotated",
|
|
189
209
|
code: `// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
@@ -404,6 +424,22 @@ if (a) {
|
|
|
404
424
|
}`,
|
|
405
425
|
errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
|
|
406
426
|
},
|
|
427
|
+
{
|
|
428
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-brace annotations ignored when annotationPlacement: 'inside'",
|
|
429
|
+
code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
430
|
+
// @req REQ-BEFORE-BRACE-ERROR
|
|
431
|
+
if (condition) {
|
|
432
|
+
doSomething();
|
|
433
|
+
}`,
|
|
434
|
+
options: [{ annotationPlacement: "inside" }],
|
|
435
|
+
output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
436
|
+
// @req REQ-BEFORE-BRACE-ERROR
|
|
437
|
+
if (condition) {
|
|
438
|
+
// @story <story-file>.story.md
|
|
439
|
+
doSomething();
|
|
440
|
+
}`,
|
|
441
|
+
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
442
|
+
},
|
|
407
443
|
],
|
|
408
444
|
});
|
|
409
445
|
runRule({
|
|
@@ -111,3 +111,55 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
|
|
|
111
111
|
expect(loopText).toBe("@story loop branch story loop details");
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
+
/**
|
|
115
|
+
* Tests for annotationPlacement wiring at helper level
|
|
116
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
117
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
118
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-DEFAULT-BACKWARD-COMPAT
|
|
119
|
+
*/
|
|
120
|
+
describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION)", () => {
|
|
121
|
+
it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors configured placement for simple if-statements", () => {
|
|
122
|
+
const sourceCode = {
|
|
123
|
+
lines: [
|
|
124
|
+
"function demo() {",
|
|
125
|
+
" if (condition) {",
|
|
126
|
+
" // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
|
|
127
|
+
" // @req REQ-INSIDE",
|
|
128
|
+
" doSomething();",
|
|
129
|
+
" }",
|
|
130
|
+
"}",
|
|
131
|
+
],
|
|
132
|
+
getCommentsBefore: jest
|
|
133
|
+
.fn()
|
|
134
|
+
.mockReturnValue([
|
|
135
|
+
{ value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
|
|
136
|
+
{ value: "@req REQ-BEFORE" },
|
|
137
|
+
]),
|
|
138
|
+
};
|
|
139
|
+
const ifNode = {
|
|
140
|
+
type: "IfStatement",
|
|
141
|
+
loc: {
|
|
142
|
+
start: { line: 2, column: 2 },
|
|
143
|
+
end: { line: 5, column: 3 },
|
|
144
|
+
},
|
|
145
|
+
consequent: {
|
|
146
|
+
type: "BlockStatement",
|
|
147
|
+
loc: {
|
|
148
|
+
start: { line: 2, column: 18 },
|
|
149
|
+
end: { line: 5, column: 3 },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const parent = {
|
|
154
|
+
type: "BlockStatement",
|
|
155
|
+
body: [ifNode],
|
|
156
|
+
};
|
|
157
|
+
const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "before");
|
|
158
|
+
expect(beforeText).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
|
|
159
|
+
expect(beforeText).toContain("@req REQ-BEFORE");
|
|
160
|
+
const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "inside");
|
|
161
|
+
expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
|
|
162
|
+
expect(insideText).toContain("@req REQ-INSIDE");
|
|
163
|
+
expect(insideText).not.toContain("@req REQ-BEFORE");
|
|
164
|
+
});
|
|
165
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|
|
@@ -290,7 +290,7 @@ The rule accepts an optional configuration object:
|
|
|
290
290
|
- `requireDescribeStory` (boolean, optional) – When `true` (default), requires that each top-level `describe` block include a story reference somewhere in its description text (for example, a path such as `docs/stories/010.0-PAYMENTS.story.md` or a shorter project-specific alias that your team uses consistently).
|
|
291
291
|
- `requireTestReqPrefix` (boolean, optional) – When `true` (default), requires each `it`/`test` block name to begin with a requirement identifier in square brackets, such as `[REQ-PAYMENTS-REFUND]`. The exact `REQ-` pattern is shared with the `valid-annotation-format` rule’s requirement ID checks.
|
|
292
292
|
- `describePattern` (string, optional) – A JavaScript regular expression **source** (without leading and trailing `/`) that the `describe` description text must match when `requireDescribeStory` is enabled. This lets you enforce a project-specific format such as requiring a canonical story path or a `STORY-` style identifier in the `describe` string. If omitted, the default is equivalent to `"Story [0-9]+\\.[0-9]+-"`, which expects the description to include a story label such as `"Story 021.0-DEV-TEST-TRACEABILITY"`. You can override this to instead require full story paths or whatever story-labeling convention your project prefers.
|
|
293
|
-
- `autoFixTestTemplate` (boolean, optional) – When `true` (default), allows the rule’s `--fix` mode to insert a file-level `@supports` placeholder template at the top of test files that are missing it. The template is intentionally non-semantic and includes TODO-style guidance so humans can later replace it with a real story
|
|
293
|
+
- `autoFixTestTemplate` (boolean, optional) – When `true` (default), allows the rule’s `--fix` mode to insert a file-level `@supports` placeholder template at the top of test files that are missing it. The template is intentionally non-semantic and includes TODO-style guidance so humans can later replace it with a real story and requirement IDs; disabling this option prevents the rule from inserting the template automatically.
|
|
294
294
|
- `autoFixTestPrefixFormat` (boolean, optional) – When `true` (default), enables safe normalization of malformed `[REQ-XXX]` prefixes in `it`/`test` names during `--fix`. The rule only rewrites prefixes that already contain a recognizable requirement identifier and limits changes to formatting concerns (spacing, square brackets vs. parentheses, underscore and dash usage, and letter casing) without fabricating new IDs or guessing requirement names.
|
|
295
295
|
- `testSupportsTemplate` (string, optional) – Overrides the default file-level `@supports` placeholder template used when `autoFixTestTemplate` is enabled. This string should be a complete JSDoc-style block (for example, including `/**`, `*`, and `*/`) that encodes your project’s preferred TODO guidance or placeholder story path; it is inserted verbatim at the top of matching test files that lack a `@supports` annotation, and is never interpreted or expanded by the rule.
|
|
296
296
|
|
|
@@ -327,6 +327,8 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
|
|
|
327
327
|
|
|
328
328
|
Description: Detects and optionally removes **redundant** traceability annotations on code that is already covered by an enclosing annotated scope. It focuses on simple, statement-level constructs—such as `return` statements, basic variable declarations, and other leaf statements—where repeating the same `@story` / `@req` / `@supports` information adds noise without improving coverage. When run with `--fix`, the rule offers safe auto-fixes that remove only the redundant comments while preserving all annotations that are required to maintain correct traceability.
|
|
329
329
|
|
|
330
|
+
Catch blocks are treated as separate execution paths for traceability purposes, and annotations inside a `catch` block that intentionally repeat the requirements of the corresponding `try` path are not considered redundant and are never auto-removed. This behavior was introduced as part of story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION (Detect and Remove Redundant Annotations)` to avoid false positives on error-handling paths that implement the same requirement as the success path.
|
|
331
|
+
|
|
330
332
|
The rule is designed to complement the core presence and validation rules: it never treats removing a redundant annotation as valid if doing so would leave the underlying requirement or story **uncovered** according to the plugin’s normal rules. It only targets comments whose traceability content is already implied by a surrounding function, method, or branch annotation.
|
|
331
333
|
|
|
332
334
|
Options:
|
|
@@ -344,6 +346,9 @@ The rule accepts an optional configuration object:
|
|
|
344
346
|
Behavior notes:
|
|
345
347
|
|
|
346
348
|
- The rule only inspects comments that contain recognized traceability annotations (`@story`, `@req`, `@supports`) and are attached to simple statements (returns, expression statements, variable declarations, and similar leaf nodes). It intentionally does **not** attempt to de-duplicate annotations on functions, classes, or major branches, which remain the responsibility of the core rules. When a statement has multiple redundant traceability comments (for example, a small comment block that repeats both @story and @req lines), the rule reports a **single** diagnostic for that statement and, in fix mode, removes all of the redundant annotation comments associated with it in a single grouped fix.
|
|
349
|
+
- **Catch blocks and error-handling paths**
|
|
350
|
+
- The rule never treats annotations inside a `catch` block as redundant, even when they repeat exactly the same `(story, requirement)` pairs that already cover the surrounding `try` statement or other enclosing branches.
|
|
351
|
+
- This preserves explicit traceability for error-handling logic, in line with the requirements captured in story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION`, and avoids collapsing success-path and failure-path coverage into a single, less precise annotation.
|
|
347
352
|
- Auto-fix removes only the redundant traceability lines (and any now-empty comment delimiters when safe) while preserving surrounding non-traceability text in the same comment where possible.
|
|
348
353
|
- When no enclosing scope with compatible coverage is found within `maxScopeDepth`, the annotation is not considered redundant and is left unchanged.
|
|
349
354
|
|
package/user-docs/examples.md
CHANGED
|
@@ -187,3 +187,35 @@ Depending on your Prettier version and configuration, the exact layout of the `e
|
|
|
187
187
|
- For most branch types, `traceability/require-branch-annotation` associates comments immediately before the branch keyword (such as `if`, `else`, `switch`, `case`) with that branch. Branches can be annotated either with a single `@supports` line (preferred), or with the older `@story`/`@req` pair for backward compatibility. The rule treats a valid `@supports` annotation as satisfying both the story and requirement presence checks.
|
|
188
188
|
- For `catch` clauses and `else if` branches, the rule is formatter-aware and also looks at comments between the condition and the block, as well as the first comment-only lines inside the block body, so you do not need to fight Prettier if it moves your annotations.
|
|
189
189
|
- When annotations exist in more than one place around an `else if` branch, the rule prefers comments immediately before the `else if` line, then comments between the condition and the block, and finally comments inside the block body, matching the behavior described in the API reference and stories `025.0` and `026.0`.
|
|
190
|
+
|
|
191
|
+
## 7. Redundant annotations and catch blocks
|
|
192
|
+
|
|
193
|
+
When `traceability/no-redundant-annotation` is enabled (for example, via the recommended preset), `catch` blocks are always treated as distinct execution paths. Repeating the same `(story, requirement)` pair in a `catch` block as in the corresponding `try` path is not considered redundant and will be preserved. This behavior is part of the improvements captured in story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION`.
|
|
194
|
+
|
|
195
|
+
In the following example, running ESLint with `--fix` will not remove the `@supports` annotation on the `catch` block, even though it repeats the requirement from the `try` path, because it represents separate error-handling coverage.
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
function filterSafeVersions(allVersions) {
|
|
199
|
+
try {
|
|
200
|
+
const stable = allVersions.filter((v) => !v.includes("-beta"));
|
|
201
|
+
|
|
202
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
203
|
+
if (stable.length > 0) {
|
|
204
|
+
return stable;
|
|
205
|
+
}
|
|
206
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
207
|
+
else if (allVersions.length > 0) {
|
|
208
|
+
// Fall back to all versions if we have no clearly stable ones
|
|
209
|
+
return allVersions;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fallback when the input list is empty
|
|
213
|
+
return [];
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
216
|
+
// traceability/no-redundant-annotation keeps this as separate error-handling coverage,
|
|
217
|
+
// even though it repeats the requirement from the try path.
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
@@ -123,7 +123,7 @@ A typical migration path is:
|
|
|
123
123
|
- Enable it as `"warn"` to get non-breaking guidance and auto-fixes for straightforward cases.
|
|
124
124
|
- Optionally move to `"error"` once you want to strictly enforce `@supports` usage for all JSDoc blocks that are eligible for safe conversion.
|
|
125
125
|
|
|
126
|
-
#### When to keep `@story` +
|
|
126
|
+
#### When to keep `@story` + `req`
|
|
127
127
|
|
|
128
128
|
Keep your current annotations if:
|
|
129
129
|
|
|
@@ -229,6 +229,8 @@ In all cases, the rule is conservative:
|
|
|
229
229
|
|
|
230
230
|
The rule operates over both `@supports` and legacy `@story`/`@req` style annotations, so it continues to work even in mixed codebases during a long-running migration.
|
|
231
231
|
|
|
232
|
+
In addition, `catch` blocks are treated as distinct execution paths: repeating the same `(story, requirement)` pair in a `catch` block is **not** considered redundant, because the error-handling path is typically validated and reasoned about separately from the main control flow.
|
|
233
|
+
|
|
232
234
|
A simplified example, using an illustrative story path that represents a file in **your** documentation tree:
|
|
233
235
|
|
|
234
236
|
Before (redundant duplication inside a branch):
|
|
@@ -271,6 +273,38 @@ if (cart.items.length === 0) {
|
|
|
271
273
|
}
|
|
272
274
|
```
|
|
273
275
|
|
|
276
|
+
#### Example: try/if/else-if/catch with non-redundant catch annotation
|
|
277
|
+
|
|
278
|
+
The following example shows a `try` block with an `if` / `else if` chain that validates a safe operation, and a `catch` block that handles the error path for the **same** requirement. Both paths are annotated with the same `(story, requirement)` pair to make it clear that the requirement covers normal execution and error handling:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
async function performSafeOperation(input) {
|
|
282
|
+
try {
|
|
283
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
284
|
+
if (input == null) {
|
|
285
|
+
throw new Error("Missing input");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
289
|
+
if (typeof input === "string") {
|
|
290
|
+
return await doSafeStringOperation(input);
|
|
291
|
+
} else if (Array.isArray(input)) {
|
|
292
|
+
return await doSafeArrayOperation(input);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return await doSafeFallbackOperation(input);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// This catch represents the error-handling path for the same safe-operation requirement.
|
|
298
|
+
// Even though the coverage matches the try/if/else-if chain above, it is *not* redundant:
|
|
299
|
+
// it documents how failures are handled for the same requirement.
|
|
300
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
301
|
+
return handleSafeOperationFailure(error, input);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Here, `traceability/no-redundant-annotation` recognizes the `catch` block as a separate execution path from the main `try` body. The annotation in the `catch` remains intact and is **not** treated as redundant, even though it repeats the same `docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION` coverage as the guarded `if` / `else if` chain in the `try`. This behavior was introduced and validated as part of story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION (Detect and Remove Redundant Annotations)` to prevent regressions in real-world `try/if/else-if/catch` scenarios like the one discussed there.
|
|
307
|
+
|
|
274
308
|
#### Safe migration workflow
|
|
275
309
|
|
|
276
310
|
To use `traceability/no-redundant-annotation` safely during your v1.x migration:
|