eslint-plugin-traceability 1.11.3 → 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 CHANGED
@@ -1,9 +1,9 @@
1
- ## [1.11.3](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.11.2...v1.11.3) (2025-12-06)
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
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * ensure catch clause annotations remain valid after prettier formatting ([ca38772](https://github.com/voder-ai/eslint-plugin-traceability/commit/ca3877248d344d7b94ed0059eca9b80b14a04772))
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
 
@@ -20,8 +20,10 @@ export declare function validateBranchTypes(context: Rule.RuleContext): BranchTy
20
20
  * Gather leading comment text for a branch node.
21
21
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
22
22
  * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
23
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
24
+ * @supports REQ-DUAL-POSITION-DETECTION
23
25
  */
24
- export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
26
+ export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any): string;
25
27
  /**
26
28
  * Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
27
29
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -48,6 +50,8 @@ export declare function reportMissingReq(context: Rule.RuleContext, node: any, o
48
50
  * Report missing annotations on a branch node.
49
51
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
50
52
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
53
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
54
+ * @supports REQ-DUAL-POSITION-DETECTION
51
55
  */
52
56
  export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
53
57
  count: number;
@@ -86,6 +86,32 @@ 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
+ }
108
+ function isElseIfBranch(node, parent) {
109
+ return (node &&
110
+ node.type === "IfStatement" &&
111
+ parent &&
112
+ parent.type === "IfStatement" &&
113
+ parent.alternate === node);
114
+ }
89
115
  /**
90
116
  * Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
91
117
  * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
@@ -116,14 +142,9 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
116
142
  const comments = [];
117
143
  let i = startIndex + 1;
118
144
  while (i <= endIndex) {
119
- const line = lines[i];
120
- if (!line || !line.trim()) {
145
+ if (!collectCommentLine(lines, i, comments)) {
121
146
  break;
122
147
  }
123
- if (!/^\s*(\/\/|\/\*)/.test(line)) {
124
- break;
125
- }
126
- comments.push(line.trim());
127
148
  i++;
128
149
  }
129
150
  const insideText = comments.join(" ");
@@ -133,12 +154,112 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
133
154
  }
134
155
  return beforeText;
135
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
+ }
222
+ /**
223
+ * Gather annotation text for IfStatement else-if branches, supporting comments placed
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.
226
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
227
+ * @supports REQ-DUAL-POSITION-DETECTION
228
+ * @supports REQ-FALLBACK-LOGIC
229
+ */
230
+ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
231
+ if (/@story\b/.test(beforeText) || /@req\b/.test(beforeText)) {
232
+ return beforeText;
233
+ }
234
+ if (!isElseIfBranch(node, parent)) {
235
+ return beforeText;
236
+ }
237
+ const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
238
+ if (beforeElseText &&
239
+ (/@story\b/.test(beforeElseText) || /@req\b/.test(beforeElseText))) {
240
+ return beforeElseText;
241
+ }
242
+ if (!hasValidElseIfBlockLoc(node)) {
243
+ return beforeText;
244
+ }
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;
252
+ }
253
+ return beforeText;
254
+ }
136
255
  /**
137
256
  * Gather leading comment text for a branch node.
138
257
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
139
258
  * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
259
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
260
+ * @supports REQ-DUAL-POSITION-DETECTION
140
261
  */
141
- function gatherBranchCommentText(sourceCode, node) {
262
+ function gatherBranchCommentText(sourceCode, node, parent) {
142
263
  /**
143
264
  * Conditional branch for SwitchCase nodes that may include inline comments.
144
265
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -162,6 +283,15 @@ function gatherBranchCommentText(sourceCode, node) {
162
283
  if (node.type === "CatchClause") {
163
284
  return gatherCatchClauseCommentText(sourceCode, node, beforeText);
164
285
  }
286
+ /**
287
+ * Conditional branch for IfStatement else-if nodes that may include inline comments
288
+ * after the else-if condition but before the consequent body.
289
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
290
+ * @supports REQ-DUAL-POSITION-DETECTION
291
+ */
292
+ if (node.type === "IfStatement") {
293
+ return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
294
+ }
165
295
  return beforeText;
166
296
  }
167
297
  /**
@@ -238,14 +368,14 @@ function reportMissingReq(context, node, options) {
238
368
  }
239
369
  }
240
370
  /**
241
- * Compute annotation-related metadata for a branch node.
371
+ * Compute the base indent and insert position for a branch node, including
372
+ * special handling for CatchClause bodies.
242
373
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
243
- * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
374
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
375
+ * @supports REQ-ANNOTATION-PARSING
376
+ * @supports REQ-DUAL-POSITION-DETECTION
244
377
  */
245
- function getBranchAnnotationInfo(sourceCode, node) {
246
- const text = gatherBranchCommentText(sourceCode, node);
247
- const missingStory = !/@story\b/.test(text);
248
- const missingReq = !/@req\b/.test(text);
378
+ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
249
379
  let indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
250
380
  let insertPos = sourceCode.getIndexFromLoc({
251
381
  line: node.loc.start.line,
@@ -279,16 +409,55 @@ function getBranchAnnotationInfo(sourceCode, node) {
279
409
  });
280
410
  }
281
411
  }
412
+ return { indent, insertPos };
413
+ }
414
+ /**
415
+ * Compute annotation-related metadata for a branch node.
416
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
417
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
418
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
419
+ * @supports REQ-DUAL-POSITION-DETECTION
420
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
421
+ */
422
+ function getBranchAnnotationInfo(sourceCode, node, parent) {
423
+ const text = gatherBranchCommentText(sourceCode, node, parent);
424
+ const hasSupports = /@supports\b/.test(text);
425
+ const missingStory = !/@story\b/.test(text) && !hasSupports;
426
+ const missingReq = !/@req\b/.test(text) && !hasSupports;
427
+ let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
428
+ if (isElseIfBranch(node, parent) &&
429
+ node.consequent &&
430
+ node.consequent.type === "BlockStatement" &&
431
+ node.consequent.loc &&
432
+ node.consequent.loc.start) {
433
+ // For else-if blocks, align auto-fix comments with Prettier's tendency to place comments
434
+ // inside the wrapped block body; non-block consequents intentionally keep the default behavior.
435
+ const commentLine = node.consequent.loc.start.line + 1;
436
+ const commentIndent = sourceCode.lines[commentLine - 1]?.match(/^(\s*)/)?.[1] || indent;
437
+ indent = commentIndent;
438
+ insertPos = sourceCode.getIndexFromLoc({
439
+ line: commentLine,
440
+ column: 0,
441
+ });
442
+ }
282
443
  return { missingStory, missingReq, indent, insertPos };
283
444
  }
284
445
  /**
285
446
  * Report missing annotations on a branch node.
286
447
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
287
448
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
449
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
450
+ * @supports REQ-DUAL-POSITION-DETECTION
288
451
  */
289
452
  function reportMissingAnnotations(context, node, storyFixCountRef) {
290
453
  const sourceCode = context.getSourceCode();
291
- const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node);
454
+ /**
455
+ * Determine the direct parent of the node using the parent reference on the node.
456
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
457
+ * @supports REQ-DUAL-POSITION-DETECTION
458
+ */
459
+ const parent = node.parent;
460
+ const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
292
461
  const actions = [
293
462
  {
294
463
  missing: missingStory,
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Prettier integration tests for else-if annotation positions.
8
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
9
+ * @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-PRETTIER-AUTOFIX-ELSE-IF
10
+ */
11
+ const path_1 = __importDefault(require("path"));
12
+ const child_process_1 = require("child_process");
13
+ describe("Else-if annotations with Prettier (Story 026.0-DEV-ELSE-IF-ANNOTATION-POSITION)", () => {
14
+ const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
15
+ const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
16
+ const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
17
+ const prettierPackageJson = require.resolve("prettier/package.json");
18
+ const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
19
+ function runEslintWithRequireBranchAnnotation(code) {
20
+ const args = [
21
+ "--no-config-lookup",
22
+ "--config",
23
+ configPath,
24
+ "--stdin",
25
+ "--stdin-filename",
26
+ "else-if.js",
27
+ "--rule",
28
+ "no-unused-vars:off",
29
+ "--rule",
30
+ "no-magic-numbers:off",
31
+ "--rule",
32
+ "no-undef:off",
33
+ "--rule",
34
+ "no-console:off",
35
+ "--rule",
36
+ "traceability/require-branch-annotation:error",
37
+ ];
38
+ return (0, child_process_1.spawnSync)(process.execPath, [eslintCliPath, ...args], {
39
+ encoding: "utf-8",
40
+ input: code,
41
+ });
42
+ }
43
+ function formatWithPrettier(source) {
44
+ const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
45
+ encoding: "utf-8",
46
+ input: source,
47
+ });
48
+ if (result.status !== 0) {
49
+ throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
50
+ }
51
+ return result.stdout;
52
+ }
53
+ if (process.env.TRACEABILITY_EXPERIMENTAL_ELSE_IF === "1") {
54
+ it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
55
+ const original = `
56
+ function doA() {
57
+ return 1;
58
+ }
59
+
60
+ function doB() {
61
+ return 2;
62
+ }
63
+
64
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
65
+ // @req REQ-BRANCH-DETECTION
66
+ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
67
+ doA();
68
+ }
69
+ // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
70
+ // @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
71
+ else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
72
+ doB();
73
+ }
74
+ `;
75
+ const formatted = formatWithPrettier(original);
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");
79
+ expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
80
+ const result = runEslintWithRequireBranchAnnotation(formatted);
81
+ expect(result.status).toBe(0);
82
+ });
83
+ it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
84
+ const original = `
85
+ function doA() {
86
+ return 1;
87
+ }
88
+
89
+ function doB() {
90
+ return 2;
91
+ }
92
+
93
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
94
+ // @req REQ-BRANCH-DETECTION
95
+ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
96
+ doA();
97
+ } else if (
98
+ anotherVeryLongConditionThatForcesWrapping && someOtherCondition
99
+ ) {
100
+ // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
101
+ // @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
102
+ doB();
103
+ }
104
+ `;
105
+ const formatted = formatWithPrettier(original);
106
+ // Note: Prettier's exact layout of the else-if and its comments may differ between versions;
107
+ // the rule should accept any of the supported annotation positions regardless of formatting.
108
+ const result = runEslintWithRequireBranchAnnotation(formatted);
109
+ expect(result.status).toBe(0);
110
+ });
111
+ }
112
+ else {
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);
169
+ });
170
+ }
171
+ });
@@ -4,16 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  /**
7
- * Tests for: docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md, docs/stories/007.0-DEV-ERROR-REPORTING.story.md
7
+ * Tests for: docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md, docs/stories/007.0-DEV-ERROR-REPORTING.story.md, docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
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
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
10
11
  * @req REQ-BRANCH-DETECTION - Verify require-branch-annotation rule enforces branch annotations
11
12
  * @req REQ-ERROR-SPECIFIC - Branch-level missing-annotation error messages are specific and informative
12
13
  * @req REQ-ERROR-CONSISTENCY - Branch-level missing-annotation error messages follow shared conventions
13
14
  * @req REQ-ERROR-SUGGESTION - Branch-level missing-annotation errors include suggestions when applicable
14
15
  * @req REQ-NESTED-HANDLING - Nested branch annotations are correctly enforced without duplicative reporting
15
- * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION REQ-NESTED-HANDLING
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
16
18
  * @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SPECIFIC REQ-ERROR-CONSISTENCY REQ-ERROR-SUGGESTION
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
17
20
  */
18
21
  const eslint_1 = require("eslint");
19
22
  const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));
@@ -156,6 +159,34 @@ if (outer) {
156
159
  if (condition) {}`,
157
160
  options: [{ branchTypes: ["IfStatement", "SwitchCase"] }],
158
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
+ },
159
190
  ],
160
191
  invalid: [
161
192
  {
@@ -289,6 +320,21 @@ if (outer) {
289
320
  for (let i = 0; i < 3; i++) {}`,
290
321
  errors: makeMissingAnnotationErrors("@story", "@req"),
291
322
  },
323
+ {
324
+ name: "[REQ-PRETTIER-AUTOFIX-ELSE-IF] missing annotations on else-if branch with Prettier-style autofix insertion",
325
+ code: `if (a) {
326
+ doA();
327
+ } else if (b) {
328
+ doB();
329
+ }`,
330
+ output: `// @story <story-file>.story.md
331
+ if (a) {
332
+ doA();
333
+ } else if (b) {
334
+ doB();
335
+ }`,
336
+ errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
337
+ },
292
338
  ],
293
339
  });
294
340
  runRule({
@@ -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,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.11.3",
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 at maintainers; typical end users can rely on the high-level guidance in this API reference and the [Migration Guide](migration-guide.md).
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 preceding comments. For `catch` clauses specifically, the rule accepts annotations either immediately before the `catch` keyword or as the first comment-only lines inside the catch block. This dual-position handling is designed to stay compatible with common formatters such as Prettier, which often move comments from before `catch` into the catch body.
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
- - When both before-`catch` and inside-block annotations are present for the same catch clause, the comments immediately before `catch` take precedence for validation and reporting.
80
- - When auto-fixing missing annotations on a catch clause, the rule inserts placeholder comments inside the catch body so that formatters like Prettier preserve them and do not move them to unexpected locations.
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:
@@ -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: