eslint-plugin-traceability 1.11.4 → 1.12.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/lib/src/utils/branch-annotation-helpers.js +106 -34
- package/lib/tests/integration/else-if-annotation-prettier.integration.test.js +59 -4
- package/lib/tests/rules/require-branch-annotation.test.js +30 -1
- package/lib/tests/utils/branch-annotation-else-if-insert-position.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-else-if-insert-position.test.js +80 -0
- package/lib/tests/utils/branch-annotation-else-if-position.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-else-if-position.test.js +107 -0
- package/lib/tests/utils/req-annotation-detection.test.js +65 -0
- package/package.json +1 -1
- package/user-docs/api-reference.md +22 -5
- package/user-docs/examples.md +62 -0
- package/user-docs/migration-guide.md +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# [1.12.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.11.4...v1.12.0) (2025-12-07)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* accept [@supports](https://github.com/supports) annotations on branches as alternative format ([6773a3a](https://github.com/voder-ai/eslint-plugin-traceability/commit/6773a3ae190e9b9adc605c7d17112f44401e5b24))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
@@ -86,6 +86,25 @@ function validateBranchTypes(context) {
|
|
|
86
86
|
function extractCommentValue(_c) {
|
|
87
87
|
return _c.value;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Collect a single contiguous comment line at the given index, appending its
|
|
91
|
+
* trimmed text to the accumulator. Returns true when a valid comment was
|
|
92
|
+
* collected and false when scanning should stop (blank or non-comment line).
|
|
93
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
94
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
|
|
95
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
|
|
96
|
+
*/
|
|
97
|
+
function collectCommentLine(lines, index, comments) {
|
|
98
|
+
const line = lines[index];
|
|
99
|
+
if (!line || !line.trim()) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
comments.push(line.trim());
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
89
108
|
function isElseIfBranch(node, parent) {
|
|
90
109
|
return (node &&
|
|
91
110
|
node.type === "IfStatement" &&
|
|
@@ -123,14 +142,9 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
|
|
|
123
142
|
const comments = [];
|
|
124
143
|
let i = startIndex + 1;
|
|
125
144
|
while (i <= endIndex) {
|
|
126
|
-
|
|
127
|
-
if (!line || !line.trim()) {
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
145
|
+
if (!collectCommentLine(lines, i, comments)) {
|
|
131
146
|
break;
|
|
132
147
|
}
|
|
133
|
-
comments.push(line.trim());
|
|
134
148
|
i++;
|
|
135
149
|
}
|
|
136
150
|
const insideText = comments.join(" ");
|
|
@@ -140,9 +154,75 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
|
|
|
140
154
|
}
|
|
141
155
|
return beforeText;
|
|
142
156
|
}
|
|
157
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
158
|
+
function scanElseIfPrecedingComments(sourceCode, node) {
|
|
159
|
+
const lines = sourceCode.lines;
|
|
160
|
+
if (!node.loc || !node.loc.start || typeof node.loc.start.line !== "number") {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
const startLine = node.loc.start.line - 1;
|
|
164
|
+
const comments = [];
|
|
165
|
+
let i = startLine - 1;
|
|
166
|
+
let scanned = 0;
|
|
167
|
+
while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
|
|
168
|
+
const line = lines[i];
|
|
169
|
+
if (!line || !line.trim()) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
comments.unshift(line.trim());
|
|
176
|
+
i--;
|
|
177
|
+
scanned++;
|
|
178
|
+
}
|
|
179
|
+
return comments.join(" ");
|
|
180
|
+
}
|
|
181
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
182
|
+
function hasValidElseIfBlockLoc(node) {
|
|
183
|
+
const hasBlockConsequent = node.consequent &&
|
|
184
|
+
node.consequent.type === "BlockStatement" &&
|
|
185
|
+
node.consequent.loc &&
|
|
186
|
+
node.consequent.loc.start;
|
|
187
|
+
return !!(node.test &&
|
|
188
|
+
node.test.loc &&
|
|
189
|
+
node.test.loc.end &&
|
|
190
|
+
hasBlockConsequent);
|
|
191
|
+
}
|
|
192
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
193
|
+
function scanElseIfBetweenConditionAndBody(sourceCode, node) {
|
|
194
|
+
const lines = sourceCode.lines;
|
|
195
|
+
const conditionEndLine = node.test.loc.end.line;
|
|
196
|
+
const consequentStartLine = node.consequent.loc.start.line;
|
|
197
|
+
const comments = [];
|
|
198
|
+
for (let lineIndex = conditionEndLine; lineIndex < consequentStartLine - 1; lineIndex++) {
|
|
199
|
+
if (!collectCommentLine(lines, lineIndex, comments)) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return comments.join(" ");
|
|
204
|
+
}
|
|
205
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
206
|
+
function scanElseIfInsideBlockComments(sourceCode, node) {
|
|
207
|
+
const lines = sourceCode.lines;
|
|
208
|
+
const consequentStartLine = node.consequent.loc.start.line;
|
|
209
|
+
const comments = [];
|
|
210
|
+
// Intentionally start from the block's start line (using the same 1-based line value as provided by the parser)
|
|
211
|
+
// so that, when indexing into sourceCode.lines, this corresponds to the first logical line inside the block body
|
|
212
|
+
// for typical formatter layouts.
|
|
213
|
+
let lineIndex = consequentStartLine;
|
|
214
|
+
while (lineIndex < lines.length) {
|
|
215
|
+
if (!collectCommentLine(lines, lineIndex, comments)) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
lineIndex++;
|
|
219
|
+
}
|
|
220
|
+
return comments.join(" ");
|
|
221
|
+
}
|
|
143
222
|
/**
|
|
144
223
|
* Gather annotation text for IfStatement else-if branches, supporting comments placed
|
|
145
|
-
* between the else-if condition and the consequent
|
|
224
|
+
* before the else keyword, between the else-if condition and the consequent body,
|
|
225
|
+
* and in the first comment-only lines inside the consequent block body.
|
|
146
226
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
147
227
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
148
228
|
* @supports REQ-FALLBACK-LOGIC
|
|
@@ -154,31 +234,23 @@ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
|
|
|
154
234
|
if (!isElseIfBranch(node, parent)) {
|
|
155
235
|
return beforeText;
|
|
156
236
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return beforeText;
|
|
237
|
+
const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
|
|
238
|
+
if (beforeElseText &&
|
|
239
|
+
(/@story\b/.test(beforeElseText) || /@req\b/.test(beforeElseText))) {
|
|
240
|
+
return beforeElseText;
|
|
162
241
|
}
|
|
163
|
-
if (!node
|
|
242
|
+
if (!hasValidElseIfBlockLoc(node)) {
|
|
164
243
|
return beforeText;
|
|
165
244
|
}
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
comments.push(line.trim());
|
|
245
|
+
const betweenText = scanElseIfBetweenConditionAndBody(sourceCode, node);
|
|
246
|
+
if (betweenText) {
|
|
247
|
+
return betweenText;
|
|
248
|
+
}
|
|
249
|
+
const insideText = scanElseIfInsideBlockComments(sourceCode, node);
|
|
250
|
+
if (insideText) {
|
|
251
|
+
return insideText;
|
|
179
252
|
}
|
|
180
|
-
|
|
181
|
-
return betweenText || beforeText;
|
|
253
|
+
return beforeText;
|
|
182
254
|
}
|
|
183
255
|
/**
|
|
184
256
|
* Gather leading comment text for a branch node.
|
|
@@ -345,11 +417,13 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
|
345
417
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
346
418
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
347
419
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
420
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
348
421
|
*/
|
|
349
422
|
function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
350
423
|
const text = gatherBranchCommentText(sourceCode, node, parent);
|
|
351
|
-
const
|
|
352
|
-
const
|
|
424
|
+
const hasSupports = /@supports\b/.test(text);
|
|
425
|
+
const missingStory = !/@story\b/.test(text) && !hasSupports;
|
|
426
|
+
const missingReq = !/@req\b/.test(text) && !hasSupports;
|
|
353
427
|
let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
|
|
354
428
|
if (isElseIfBranch(node, parent) &&
|
|
355
429
|
node.consequent &&
|
|
@@ -378,13 +452,11 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
|
378
452
|
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
379
453
|
const sourceCode = context.getSourceCode();
|
|
380
454
|
/**
|
|
381
|
-
* Determine the direct parent of the node using the
|
|
455
|
+
* Determine the direct parent of the node using the parent reference on the node.
|
|
382
456
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
383
457
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
384
458
|
*/
|
|
385
|
-
const
|
|
386
|
-
const ancestors = contextAny.getAncestors?.() || [];
|
|
387
|
-
const parent = ancestors.length > 0 ? ancestors[ancestors.length - 1] : undefined;
|
|
459
|
+
const parent = node.parent;
|
|
388
460
|
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
|
|
389
461
|
const actions = [
|
|
390
462
|
{
|
|
@@ -73,8 +73,9 @@ else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
|
73
73
|
}
|
|
74
74
|
`;
|
|
75
75
|
const formatted = formatWithPrettier(original);
|
|
76
|
-
// Sanity
|
|
77
|
-
|
|
76
|
+
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
77
|
+
// but the exact layout and comment movement may vary between versions.
|
|
78
|
+
expect(formatted).toContain("else if");
|
|
78
79
|
expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
79
80
|
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
80
81
|
expect(result.status).toBe(0);
|
|
@@ -109,8 +110,62 @@ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherConditio
|
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
else {
|
|
112
|
-
it.skip("
|
|
113
|
-
|
|
113
|
+
it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
|
|
114
|
+
const original = `
|
|
115
|
+
function doA() {
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function doB() {
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
124
|
+
// @req REQ-BRANCH-DETECTION
|
|
125
|
+
if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
|
|
126
|
+
doA();
|
|
127
|
+
}
|
|
128
|
+
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
129
|
+
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
130
|
+
else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
131
|
+
doB();
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
const formatted = formatWithPrettier(original);
|
|
135
|
+
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
136
|
+
// but the exact layout and comment movement may vary between versions.
|
|
137
|
+
expect(formatted).toContain("else if");
|
|
138
|
+
expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
139
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
140
|
+
expect(result.status).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
|
|
143
|
+
const original = `
|
|
144
|
+
function doA() {
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function doB() {
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
153
|
+
// @req REQ-BRANCH-DETECTION
|
|
154
|
+
if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
|
|
155
|
+
doA();
|
|
156
|
+
} else if (
|
|
157
|
+
anotherVeryLongConditionThatForcesWrapping && someOtherCondition
|
|
158
|
+
) {
|
|
159
|
+
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
160
|
+
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
161
|
+
doB();
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
const formatted = formatWithPrettier(original);
|
|
165
|
+
// Note: Prettier's exact layout of the else-if and its comments may differ between versions;
|
|
166
|
+
// the rule should accept any of the supported annotation positions regardless of formatting.
|
|
167
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
168
|
+
expect(result.status).toBe(0);
|
|
114
169
|
});
|
|
115
170
|
}
|
|
116
171
|
});
|
|
@@ -13,7 +13,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
* @req REQ-ERROR-CONSISTENCY - Branch-level missing-annotation error messages follow shared conventions
|
|
14
14
|
* @req REQ-ERROR-SUGGESTION - Branch-level missing-annotation errors include suggestions when applicable
|
|
15
15
|
* @req REQ-NESTED-HANDLING - Nested branch annotations are correctly enforced without duplicative reporting
|
|
16
|
-
* @
|
|
16
|
+
* @req REQ-SUPPORTS-ALTERNATIVE - Branches annotated only with @supports are treated as fully annotated
|
|
17
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION REQ-NESTED-HANDLING REQ-SUPPORTS-ALTERNATIVE
|
|
17
18
|
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SPECIFIC REQ-ERROR-CONSISTENCY REQ-ERROR-SUGGESTION
|
|
18
19
|
* @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
|
|
19
20
|
*/
|
|
@@ -158,6 +159,34 @@ if (outer) {
|
|
|
158
159
|
if (condition) {}`,
|
|
159
160
|
options: [{ branchTypes: ["IfStatement", "SwitchCase"] }],
|
|
160
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: "[REQ-SUPPORTS-ALTERNATIVE] if-statement with only @supports annotation is treated as fully annotated",
|
|
164
|
+
code: `// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
165
|
+
if (shouldHandleAlternative) {
|
|
166
|
+
handleAlternative();
|
|
167
|
+
}`,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "[REQ-SUPPORTS-ALTERNATIVE] try/catch where both branches are annotated only with @supports",
|
|
171
|
+
code: `// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
172
|
+
try {
|
|
173
|
+
mightThrow();
|
|
174
|
+
}
|
|
175
|
+
// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
176
|
+
catch (error) {
|
|
177
|
+
recoverFrom(error);
|
|
178
|
+
}`,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "[REQ-SUPPORTS-ALTERNATIVE] else-if branch with @supports inside the block body",
|
|
182
|
+
code: `// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
183
|
+
if (mode === 'primary') {
|
|
184
|
+
handlePrimary();
|
|
185
|
+
} else if (mode === 'alternative') {
|
|
186
|
+
// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
187
|
+
handleAlternativeMode();
|
|
188
|
+
}`,
|
|
189
|
+
},
|
|
161
190
|
],
|
|
162
191
|
invalid: [
|
|
163
192
|
{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for else-if insert position calculation.
|
|
5
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
6
|
+
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
7
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-PRETTIER-AUTOFIX-ELSE-IF
|
|
8
|
+
*/
|
|
9
|
+
const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
|
|
10
|
+
describe("Else-if insert position (Story 026.0-DEV-ELSE-IF-ANNOTATION-POSITION)", () => {
|
|
11
|
+
it("[REQ-PRETTIER-AUTOFIX-ELSE-IF] inserts annotations on a dedicated line inside the else-if block body", () => {
|
|
12
|
+
const lines = [
|
|
13
|
+
"if (a) {",
|
|
14
|
+
" doA();",
|
|
15
|
+
"}",
|
|
16
|
+
"else if (b) {",
|
|
17
|
+
" doB();",
|
|
18
|
+
"}",
|
|
19
|
+
];
|
|
20
|
+
const fixer = {
|
|
21
|
+
insertTextBeforeRange: jest.fn((r, t) => ({
|
|
22
|
+
r,
|
|
23
|
+
t,
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
const context = {
|
|
27
|
+
getSourceCode() {
|
|
28
|
+
return {
|
|
29
|
+
lines,
|
|
30
|
+
getCommentsBefore() {
|
|
31
|
+
return [];
|
|
32
|
+
},
|
|
33
|
+
getIndexFromLoc({ line, column }) {
|
|
34
|
+
// simple line/column to index mapping for the test: assume each line ends with "\n"
|
|
35
|
+
const prefix = lines.slice(0, line - 1).join("\n");
|
|
36
|
+
return prefix.length + (line > 1 ? 1 : 0) + column;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
report({ fix }) {
|
|
41
|
+
// immediately invoke the fixer to exercise the insert position
|
|
42
|
+
if (typeof fix === "function") {
|
|
43
|
+
fix(fixer);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const node = {
|
|
48
|
+
type: "IfStatement",
|
|
49
|
+
loc: { start: { line: 4 } },
|
|
50
|
+
test: { loc: { end: { line: 4 } } },
|
|
51
|
+
consequent: {
|
|
52
|
+
type: "BlockStatement",
|
|
53
|
+
loc: { start: { line: 4 } },
|
|
54
|
+
body: [
|
|
55
|
+
{
|
|
56
|
+
type: "ExpressionStatement",
|
|
57
|
+
loc: { start: { line: 5 } },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const parent = {
|
|
63
|
+
type: "IfStatement",
|
|
64
|
+
alternate: node,
|
|
65
|
+
};
|
|
66
|
+
node.parent = parent;
|
|
67
|
+
const storyFixCountRef = { count: 0 };
|
|
68
|
+
(0, branch_annotation_helpers_1.reportMissingAnnotations)(context, node, storyFixCountRef);
|
|
69
|
+
expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
|
|
70
|
+
const [range, text] = fixer.insertTextBeforeRange.mock
|
|
71
|
+
.calls[0];
|
|
72
|
+
// ensure we are inserting before the first statement in the else-if body (line 5)
|
|
73
|
+
const expectedIndex = context
|
|
74
|
+
.getSourceCode()
|
|
75
|
+
.getIndexFromLoc({ line: 5, column: 0 });
|
|
76
|
+
expect(range).toEqual([expectedIndex, expectedIndex]);
|
|
77
|
+
// and that the inserted text is prefixed with the inner indentation from line 5
|
|
78
|
+
expect(text.startsWith(" ")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
|
|
4
|
+
function createMockSourceCode(options) {
|
|
5
|
+
const { lines = [], commentsBefore = [] } = options;
|
|
6
|
+
return {
|
|
7
|
+
lines,
|
|
8
|
+
getCommentsBefore() {
|
|
9
|
+
return commentsBefore;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe("gatherBranchCommentText else-if behavior (Story 026.0-DEV-ELSE-IF-ANNOTATION-POSITION)", () => {
|
|
14
|
+
it("[REQ-DUAL-POSITION-DETECTION-ELSE-IF] detects annotations placed before the else-if keyword", () => {
|
|
15
|
+
const sourceCode = createMockSourceCode({
|
|
16
|
+
commentsBefore: [
|
|
17
|
+
{
|
|
18
|
+
value: "@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
19
|
+
},
|
|
20
|
+
{ value: "@req REQ-DUAL-POSITION-DETECTION-ELSE-IF" },
|
|
21
|
+
],
|
|
22
|
+
// lines are unused in this case because we short-circuit on before-text annotations.
|
|
23
|
+
lines: [],
|
|
24
|
+
});
|
|
25
|
+
const node = {
|
|
26
|
+
type: "IfStatement",
|
|
27
|
+
loc: { start: { line: 10 } },
|
|
28
|
+
};
|
|
29
|
+
const parent = {
|
|
30
|
+
type: "IfStatement",
|
|
31
|
+
alternate: node,
|
|
32
|
+
};
|
|
33
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
|
|
34
|
+
expect(text).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
35
|
+
expect(text).toContain("@req REQ-DUAL-POSITION-DETECTION-ELSE-IF");
|
|
36
|
+
});
|
|
37
|
+
it("[REQ-FALLBACK-LOGIC-ELSE-IF] falls back to annotations between condition and body when before-else-if comments lack annotations", () => {
|
|
38
|
+
const lines = [
|
|
39
|
+
"if (a) {",
|
|
40
|
+
" doA();",
|
|
41
|
+
"} else if (b && c) {",
|
|
42
|
+
" // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
43
|
+
" // @req REQ-FALLBACK-LOGIC-ELSE-IF",
|
|
44
|
+
" doB();",
|
|
45
|
+
"}",
|
|
46
|
+
];
|
|
47
|
+
const sourceCode = createMockSourceCode({
|
|
48
|
+
commentsBefore: [{ value: "// some unrelated comment" }],
|
|
49
|
+
lines,
|
|
50
|
+
});
|
|
51
|
+
const node = {
|
|
52
|
+
type: "IfStatement",
|
|
53
|
+
loc: { start: { line: 3 } },
|
|
54
|
+
test: { loc: { end: { line: 3 } } },
|
|
55
|
+
consequent: {
|
|
56
|
+
type: "BlockStatement",
|
|
57
|
+
loc: { start: { line: 6 } },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const parent = {
|
|
61
|
+
type: "IfStatement",
|
|
62
|
+
alternate: node,
|
|
63
|
+
};
|
|
64
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
|
|
65
|
+
expect(text).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
66
|
+
expect(text).toContain("@req REQ-FALLBACK-LOGIC-ELSE-IF");
|
|
67
|
+
});
|
|
68
|
+
it("[REQ-POSITION-PRIORITY-ELSE-IF] prefers before-else-if annotations when both positions are present", () => {
|
|
69
|
+
const lines = [
|
|
70
|
+
"if (a) {",
|
|
71
|
+
" doA();",
|
|
72
|
+
"} else if (b) {",
|
|
73
|
+
" // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
74
|
+
" // @req REQ-POSITION-PRIORITY-ELSE-IF-BETWEEN",
|
|
75
|
+
" doB();",
|
|
76
|
+
"}",
|
|
77
|
+
];
|
|
78
|
+
const sourceCode = createMockSourceCode({
|
|
79
|
+
commentsBefore: [
|
|
80
|
+
{
|
|
81
|
+
value: "@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
82
|
+
},
|
|
83
|
+
{ value: "@req REQ-POSITION-PRIORITY-ELSE-IF" },
|
|
84
|
+
],
|
|
85
|
+
lines,
|
|
86
|
+
});
|
|
87
|
+
const node = {
|
|
88
|
+
type: "IfStatement",
|
|
89
|
+
loc: { start: { line: 3 } },
|
|
90
|
+
test: { loc: { end: { line: 3 } } },
|
|
91
|
+
consequent: {
|
|
92
|
+
type: "BlockStatement",
|
|
93
|
+
loc: { start: { line: 6 } },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const parent = {
|
|
97
|
+
type: "IfStatement",
|
|
98
|
+
alternate: node,
|
|
99
|
+
};
|
|
100
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
|
|
101
|
+
// The helper should use the before-else-if annotations and not need to
|
|
102
|
+
// fall back to between-condition-and-body comments.
|
|
103
|
+
expect(text).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
104
|
+
expect(text).toContain("@req REQ-POSITION-PRIORITY-ELSE-IF");
|
|
105
|
+
expect(text).not.toContain("REQ-POSITION-PRIORITY-ELSE-IF-BETWEEN");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -157,6 +157,20 @@ describe("reqAnnotationDetection advanced heuristics (Story 003.0-DEV-FUNCTION-A
|
|
|
157
157
|
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
158
158
|
expect(has).toBe(false);
|
|
159
159
|
});
|
|
160
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when range[0] is not a number", () => {
|
|
161
|
+
const context = {
|
|
162
|
+
getSourceCode() {
|
|
163
|
+
return createMockSourceCode({ text: "/* @req REQ-IN-TEXT-BUT-INVALID-RANGE */" });
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const node = {
|
|
167
|
+
// First element of range is not a number; guard on numeric start index should trigger
|
|
168
|
+
range: ["not-a-number", 10],
|
|
169
|
+
parent: {},
|
|
170
|
+
};
|
|
171
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
172
|
+
expect(has).toBe(false);
|
|
173
|
+
});
|
|
160
174
|
it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns true when text window contains @req", () => {
|
|
161
175
|
const fullText = `
|
|
162
176
|
// some header
|
|
@@ -244,4 +258,55 @@ describe("reqAnnotationDetection advanced heuristics (Story 003.0-DEV-FUNCTION-A
|
|
|
244
258
|
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, { parent: {} });
|
|
245
259
|
expect(has).toBe(true);
|
|
246
260
|
});
|
|
261
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] linesBeforeHasReq returns true when preceding lines contain @req marker", () => {
|
|
262
|
+
const context = {
|
|
263
|
+
getSourceCode() {
|
|
264
|
+
return createMockSourceCode({
|
|
265
|
+
lines: [
|
|
266
|
+
"// some header",
|
|
267
|
+
"/** @req REQ-LINE-BEFORE */",
|
|
268
|
+
"function foo() {}",
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const node = {
|
|
274
|
+
// Node starts on line 3 (1-based), so line 2 is inspected by linesBeforeHasReq
|
|
275
|
+
loc: { start: { line: 3 } },
|
|
276
|
+
parent: {},
|
|
277
|
+
};
|
|
278
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
279
|
+
expect(has).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] parentChainHasReq returns true when leadingComments contain @supports and getCommentsBefore is unusable", () => {
|
|
282
|
+
const context = {
|
|
283
|
+
getSourceCode() {
|
|
284
|
+
return {
|
|
285
|
+
// Not a callable function; forces parentChainHasReq to rely on leadingComments
|
|
286
|
+
getCommentsBefore: 42,
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
const node = {
|
|
291
|
+
parent: {
|
|
292
|
+
leadingComments: [
|
|
293
|
+
{ value: "some other comment" },
|
|
294
|
+
{
|
|
295
|
+
value: "@supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-FROM-LEADING-COMMENT",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
parent: {},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
302
|
+
expect(has).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] returns true when jsdoc has @req even if context is undefined", () => {
|
|
305
|
+
const jsdoc = { value: "/** @req REQ-JSDOC-NO-CONTEXT */" };
|
|
306
|
+
const node = {
|
|
307
|
+
parent: {},
|
|
308
|
+
};
|
|
309
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], undefined, node);
|
|
310
|
+
expect(has).toBe(true);
|
|
311
|
+
});
|
|
247
312
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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",
|
|
@@ -15,7 +15,7 @@ In addition to the core `@story` and `@req` annotations, the plugin also underst
|
|
|
15
15
|
`@supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND`
|
|
16
16
|
to indicate that a given function supports a particular requirement from a payments story document within that project’s own `docs/stories` tree. For a detailed explanation of `@supports` behavior and validation, see [Migration Guide](migration-guide.md) (section **3.1 Multi-story @supports annotations**). Additional background on multi-story semantics is available in the project’s internal rule documentation, which is intended for maintainers rather than end users.
|
|
17
17
|
|
|
18
|
-
The `prefer-supports-annotation` rule is an **opt-in migration helper** that is disabled by default and **not** part of any built-in preset. It can be enabled and given a severity like `"warn"` or `"error"` using normal ESLint rule configuration when you want to gradually encourage multi-story `@supports` usage. The legacy rule key `traceability/prefer-implements-annotation` remains available as a **deprecated alias** for backward compatibility, but new configurations should prefer `traceability/prefer-supports-annotation`. Detailed behavior and migration guidance are documented in the project’s internal rule documentation, which is targeted
|
|
18
|
+
The `prefer-supports-annotation` rule is an **opt-in migration helper** that is disabled by default and **not** part of any built-in preset. It can be enabled and given a severity like `"warn"` or `"error"` using normal ESLint rule configuration when you want to gradually encourage multi-story `@supports` usage. The legacy rule key `traceability/prefer-implements-annotation` remains available as a **deprecated alias** for backward compatibility, but new configurations should prefer `traceability/prefer-supports-annotation`. Detailed behavior and migration guidance are documented in the project’s internal rule documentation, which is targeted for maintainers; typical end users can rely on the high-level guidance in this API reference and the [Migration Guide](migration-guide.md).
|
|
19
19
|
|
|
20
20
|
### traceability/require-story-annotation
|
|
21
21
|
|
|
@@ -68,7 +68,9 @@ function initAuth() {
|
|
|
68
68
|
|
|
69
69
|
### traceability/require-branch-annotation
|
|
70
70
|
|
|
71
|
-
Description: Ensures significant code branches (if/else, loops, switch cases, try/catch) have both `@story` and `@req` annotations in
|
|
71
|
+
Description: Ensures significant code branches (if/else chains, loops, switch cases, try/catch) have both `@story` and `@req` annotations in nearby comments. When you adopt multi-story `@supports` annotations, a single `@supports <storyPath> <REQ-ID>...` line placed in any of the valid branch comment locations is treated as satisfying both the story and requirement presence checks for that branch, while detailed format validation of the `@supports` value (including story paths and requirement IDs) continues to be handled by `traceability/valid-annotation-format`, `traceability/valid-story-reference`, and `traceability/valid-req-reference`.
|
|
72
|
+
|
|
73
|
+
For most branches, the rule looks for annotations in comments immediately preceding the branch keyword (for example, the line above an `if` or `for` statement). For `catch` clauses and `else if` branches, the rule is formatter-aware and accepts annotations in additional positions that common formatters like Prettier use when they reflow code.
|
|
72
74
|
|
|
73
75
|
Options:
|
|
74
76
|
|
|
@@ -76,8 +78,22 @@ Options:
|
|
|
76
78
|
|
|
77
79
|
Behavior notes:
|
|
78
80
|
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
+
- **Catch clauses**:
|
|
82
|
+
- Valid locations for `@story` / `@req` annotations are either immediately before the `catch` keyword or on the first comment-only lines inside the catch block (before any executable statements). A single `@supports` annotation in either of these locations is also accepted as covering both story and requirement presence for the catch branch.
|
|
83
|
+
- If annotations exist in both locations, the comments immediately before `catch` take precedence for validation and reporting.
|
|
84
|
+
- When auto-fixing missing annotations on a catch clause, the rule inserts placeholder comments inside the catch block body so that formatters like Prettier keep them attached to the branch.
|
|
85
|
+
|
|
86
|
+
- **Else-if branches**:
|
|
87
|
+
- Valid locations for `@story` / `@req` annotations include:
|
|
88
|
+
- Line or block comments immediately before the `else if` line.
|
|
89
|
+
- Comment-only lines between the `else if (condition)` and the opening `{` of the consequent block (for styles where the condition and block are on separate lines).
|
|
90
|
+
- The first comment-only lines inside the consequent block body, which is where formatters like Prettier often move comments when they wrap long `else if` conditions. For a concrete before/after example of this formatter-aware behavior, see [user-docs/examples.md](examples.md) (section **6. Branch annotations with if/else/else-if and Prettier**).
|
|
91
|
+
- When annotations appear in more than one of these locations, the rule prefers the comments immediately before the `else if` line, then comments between the condition and the block, and finally comments inside the block body. This precedence is designed to closely mirror real-world formatter behavior and matches the formatter-aware scenarios described in stories 025.0 and 026.0.
|
|
92
|
+
- When auto-fixing missing annotations on an `else if` branch, the rule inserts placeholder comments as the first comment-only line inside the consequent block body (just after the opening `{`), which is a stable location under Prettier and similar formatters. As with catch clauses, a single `@supports` annotation placed in any of these accepted locations is treated as equivalent to having both `@story` and `@req` comments for that branch, with deep format and existence checks delegated to the other validation rules.
|
|
93
|
+
|
|
94
|
+
For a concrete illustration of how these rules interact with Prettier, see the formatter-aware if/else/else-if example in [user-docs/examples.md](examples.md) (section **6. Branch annotations with if/else/else-if and Prettier**), which shows both the hand-written and formatted code that the rule considers valid.
|
|
95
|
+
|
|
96
|
+
These behaviors are intentionally limited to `catch` clauses and `else if` branches; other branch types (plain `if`, `else`, loops, and `switch` cases) continue to use the simpler "comments immediately before the branch" association model for both validation and auto-fix placement.
|
|
81
97
|
|
|
82
98
|
Default Severity: `error`
|
|
83
99
|
Example:
|
|
@@ -689,4 +705,5 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
|
|
|
689
705
|
In CI:
|
|
690
706
|
|
|
691
707
|
```bash
|
|
692
|
-
npm run traceability:verify
|
|
708
|
+
npm run traceability:verify
|
|
709
|
+
```
|
package/user-docs/examples.md
CHANGED
|
@@ -114,3 +114,65 @@ function performOperation(input: string): string {
|
|
|
114
114
|
return "ok";
|
|
115
115
|
}
|
|
116
116
|
```
|
|
117
|
+
|
|
118
|
+
## 6. Branch annotations with if/else/else-if and Prettier
|
|
119
|
+
|
|
120
|
+
This example shows how to keep `traceability/require-branch-annotation` satisfied while still running Prettier on your code.
|
|
121
|
+
|
|
122
|
+
### 6.1 Before formatting
|
|
123
|
+
|
|
124
|
+
In this version, annotations are placed immediately before each significant branch. This is a simple layout that is easy to read and accepted by the rule:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
function pickCategory(score: number): string {
|
|
128
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
129
|
+
// @req REQ-BRANCH-DETECTION
|
|
130
|
+
if (score >= 80) {
|
|
131
|
+
return "high";
|
|
132
|
+
}
|
|
133
|
+
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
134
|
+
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
135
|
+
else if (score >= 50) {
|
|
136
|
+
return "medium";
|
|
137
|
+
}
|
|
138
|
+
// You can annotate `else` using the same pattern if you treat it as a significant branch.
|
|
139
|
+
else {
|
|
140
|
+
return "low";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
You can run just the branch-annotation rule via the CLI:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx eslint --no-eslintrc \
|
|
149
|
+
--rule "traceability/require-branch-annotation:error" \
|
|
150
|
+
pick-category.ts
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 6.2 After formatting with Prettier
|
|
154
|
+
|
|
155
|
+
Prettier may reflow your `else if` line, wrap the condition, or move comments into the body of the branch. The `traceability/require-branch-annotation` rule is formatter-aware and will still recognize valid annotations in supported positions, such as the first comment-only lines inside the block body:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
function pickCategory(score: number): string {
|
|
159
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
160
|
+
// @req REQ-BRANCH-DETECTION
|
|
161
|
+
if (score >= 80) {
|
|
162
|
+
return "high";
|
|
163
|
+
} else if (score >= 50) {
|
|
164
|
+
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
165
|
+
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
166
|
+
return "medium";
|
|
167
|
+
} else {
|
|
168
|
+
return "low";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Depending on your Prettier version and configuration, the exact layout of the `else if` line and braces may differ, but as long as your annotations are in one of the supported locations, the rule will accept them.
|
|
174
|
+
|
|
175
|
+
- Notes:
|
|
176
|
+
- For most branch types, `traceability/require-branch-annotation` associates comments immediately before the branch keyword (such as `if`, `else`, `switch`, `case`) with that branch.
|
|
177
|
+
- 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.
|
|
178
|
+
- 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`.
|
|
@@ -188,6 +188,15 @@ You can introduce `@supports` gradually without breaking existing code:
|
|
|
188
188
|
|
|
189
189
|
Detailed semantics and edge cases (path validation, scoped requirement IDs, and multi-story fixtures) are ultimately governed by your own stories and requirements. For typical migrations, this guide together with the plugin’s API reference is sufficient.
|
|
190
190
|
|
|
191
|
+
### 3.2 Else-if branch annotations and formatter compatibility
|
|
192
|
+
|
|
193
|
+
Versions 1.x of `eslint-plugin-traceability` extend the `traceability/require-branch-annotation` rule to better support formatter-driven layouts for `else if` branches. In most projects you **do not need to change existing annotations**:
|
|
194
|
+
|
|
195
|
+
- Comments immediately before an `else if` line remain valid and continue to satisfy the rule.
|
|
196
|
+
- When formatters such as Prettier move comments between the `else if (condition)` and the opening `{`, or into the first comment-only lines inside the `{ ... }` block, those annotations are now also recognized and associated with the correct branch.
|
|
197
|
+
|
|
198
|
+
If you previously added suppressions or workaround comments around `else if` branches due to formatter conflicts, you can usually remove those workarounds after upgrading to 1.x as long as your annotations live in one of the supported locations. For new code, you can place annotations either directly above the `else if` or, when you know a formatter will wrap a long condition, on the first comment-only line inside the consequent block body, which is where the rule places auto-fix placeholders by default.
|
|
199
|
+
|
|
191
200
|
## 4. Test and Validate
|
|
192
201
|
|
|
193
202
|
Run your test suite to confirm everything passes:
|