eslint-plugin-traceability 1.19.0 → 1.19.2

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.19.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.18.0...v1.19.0) (2025-12-18)
1
+ ## [1.19.2](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.1...v1.19.2) (2025-12-18)
2
2
 
3
3
 
4
- ### Features
4
+ ### Bug Fixes
5
5
 
6
- * enforce inside-brace placement mode for branch annotations ([5c2129a](https://github.com/voder-ai/eslint-plugin-traceability/commit/5c2129a7c25717dc4ce14bf55bc4541ae07e5539))
6
+ * honor annotationPlacement for else-if branches and refactor helpers ([8f747dc](https://github.com/voder-ai/eslint-plugin-traceability/commit/8f747dc8546ae8e5033cfe4aa9c394ca668a45d5))
7
7
 
8
8
  # Changelog
9
9
 
@@ -9,6 +9,7 @@ exports.reportMissingReq = reportMissingReq;
9
9
  const branch_annotation_report_helpers_1 = require("./branch-annotation-report-helpers");
10
10
  Object.defineProperty(exports, "reportMissingAnnotations", { enumerable: true, get: function () { return branch_annotation_report_helpers_1.reportMissingAnnotations; } });
11
11
  const branch_annotation_loop_helpers_1 = require("./branch-annotation-loop-helpers");
12
+ const branch_annotation_if_helpers_1 = require("./branch-annotation-if-helpers");
12
13
  const PRE_COMMENT_OFFSET = 2; // number of lines above branch to inspect for comments
13
14
  /**
14
15
  * Valid branch types for require-branch-annotation rule.
@@ -153,24 +154,7 @@ function scanCommentLinesInRange(lines, startIndex, endIndexInclusive) {
153
154
  }
154
155
  return comments.join(" ");
155
156
  }
156
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
157
- function isElseIfBranch(node, parent) {
158
- return (node &&
159
- node.type === "IfStatement" &&
160
- parent &&
161
- parent.type === "IfStatement" &&
162
- parent.alternate === node);
163
- }
164
- /**
165
- * Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
166
- * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
167
- * @req REQ-DUAL-POSITION-DETECTION
168
- * @req REQ-FALLBACK-LOGIC
169
- */
170
- function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
171
- if (/@story\b/.test(beforeText) || /@req\b/.test(beforeText)) {
172
- return beforeText;
173
- }
157
+ function getInsideCatchCommentText(sourceCode, node) {
174
158
  const getCommentsInside = sourceCode.getCommentsInside;
175
159
  if (node.body && typeof getCommentsInside === "function") {
176
160
  try {
@@ -193,6 +177,31 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
193
177
  return insideText;
194
178
  }
195
179
  }
180
+ return "";
181
+ }
182
+ /**
183
+ * Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
184
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
185
+ * @req REQ-DUAL-POSITION-DETECTION
186
+ * @req REQ-FALLBACK-LOGIC
187
+ */
188
+ function gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText) {
189
+ if (annotationPlacement === "inside") {
190
+ const insideText = getInsideCatchCommentText(sourceCode, node);
191
+ if (insideText) {
192
+ return insideText;
193
+ }
194
+ return "";
195
+ }
196
+ if (/@story\b/.test(beforeText) ||
197
+ /@req\b/.test(beforeText) ||
198
+ /@supports\b/.test(beforeText)) {
199
+ return beforeText;
200
+ }
201
+ const insideText = getInsideCatchCommentText(sourceCode, node);
202
+ if (insideText) {
203
+ return insideText;
204
+ }
196
205
  return beforeText;
197
206
  }
198
207
  /**
@@ -245,119 +254,89 @@ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, before
245
254
  return "";
246
255
  }
247
256
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
248
- function scanElseIfPrecedingComments(sourceCode, node) {
257
+ function gatherSwitchCaseCommentText(sourceCode, node) {
249
258
  const lines = sourceCode.lines;
250
- if (!node.loc || !node.loc.start || typeof node.loc.start.line !== "number") {
251
- return "";
252
- }
253
- const startLine = node.loc.start.line - 1;
259
+ const startLine = node.loc.start.line;
260
+ let i = startLine - PRE_COMMENT_OFFSET;
254
261
  const comments = [];
255
- let i = startLine - 1;
256
- let scanned = 0;
257
- while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
258
- const commentText = getCommentTextAtLine(lines, i);
259
- if (!commentText) {
260
- break;
261
- }
262
- comments.unshift(commentText);
262
+ while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
263
+ comments.unshift(lines[i].trim());
263
264
  i--;
264
- scanned++;
265
265
  }
266
266
  return comments.join(" ");
267
267
  }
268
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
269
- function hasValidElseIfBlockLoc(node) {
270
- const hasBlockConsequent = node.consequent &&
271
- node.consequent.type === "BlockStatement" &&
272
- node.consequent.loc &&
273
- node.consequent.loc.start;
274
- return !!(node.test &&
275
- node.test.loc &&
276
- node.test.loc.end &&
277
- hasBlockConsequent);
278
- }
279
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
280
- function scanElseIfBetweenConditionAndBody(sourceCode, node) {
281
- const lines = sourceCode.lines;
282
- const conditionEndLine = node.test.loc.end.line;
283
- const consequentStartLine = node.consequent.loc.start.line;
284
- // Lines in sourceCode are 0-based indexes, but loc.line values are 1-based.
285
- // We want to scan comments strictly between the condition and the
286
- // consequent body, so we start at the line after the condition's end and
287
- // stop at the line immediately before the consequent's starting line.
288
- const startIndex = conditionEndLine; // already the next logical line index when 0-based
289
- const endIndexExclusive = consequentStartLine - 1;
290
- if (endIndexExclusive <= startIndex) {
291
- return "";
268
+ /**
269
+ * Helper that gathers comment text for non-IfStatement branch types using
270
+ * straightforward behavior (SwitchCase, CatchClause, and loop statements).
271
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
272
+ * @supports REQ-COMMENT-ASSOCIATION
273
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
274
+ * @supports REQ-PLACEMENT-CONFIG
275
+ */
276
+ function gatherNonIfBranchCommentText(sourceCode, node, context) {
277
+ const { annotationPlacement, beforeText } = context;
278
+ if (node.type === "SwitchCase") {
279
+ return gatherSwitchCaseCommentText(sourceCode, node);
292
280
  }
293
- return scanCommentLinesInRange(lines, startIndex, endIndexExclusive - 1);
294
- }
295
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
296
- function scanElseIfInsideBlockComments(sourceCode, node) {
297
- const lines = sourceCode.lines;
298
- const consequentStartLine = node.consequent.loc.start.line;
299
- const comments = [];
300
- // Intentionally start from the block's start line (using the same 1-based line value as provided by the parser)
301
- // so that, when indexing into sourceCode.lines, this corresponds to the first logical line inside the block body
302
- // for typical formatter layouts.
303
- let lineIndex = consequentStartLine;
304
- while (lineIndex < lines.length) {
305
- if (!collectCommentLine(lines, lineIndex, comments)) {
306
- break;
307
- }
308
- lineIndex++;
281
+ if (node.type === "CatchClause") {
282
+ return gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText);
309
283
  }
310
- return comments.join(" ");
284
+ if (node.type === "ForStatement" ||
285
+ node.type === "ForInStatement" ||
286
+ node.type === "ForOfStatement" ||
287
+ node.type === "WhileStatement" ||
288
+ node.type === "DoWhileStatement") {
289
+ return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, annotationPlacement, beforeText);
290
+ }
291
+ return null;
311
292
  }
312
293
  /**
313
- * Gather annotation text for IfStatement else-if branches, supporting comments placed
314
- * before the else keyword, between the else-if condition and the consequent body,
315
- * and in the first comment-only lines inside the consequent block body.
294
+ * Helper that gathers comment text for IfStatement branches, including both
295
+ * simple if and else-if specific logic.
296
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
297
+ * @supports REQ-COMMENT-ASSOCIATION
316
298
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
317
299
  * @supports REQ-DUAL-POSITION-DETECTION
318
- * @supports REQ-FALLBACK-LOGIC
300
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
301
+ * @supports REQ-PLACEMENT-CONFIG
302
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
319
303
  */
320
- function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
321
- if (beforeText &&
322
- (/@story\b/.test(beforeText) ||
323
- /@req\b/.test(beforeText) ||
324
- /@supports\b/.test(beforeText))) {
325
- return beforeText;
326
- }
327
- if (!isElseIfBranch(node, parent)) {
328
- return beforeText;
329
- }
330
- const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
331
- if (beforeElseText &&
332
- (/@story\b/.test(beforeElseText) ||
333
- /@req\b/.test(beforeElseText) ||
334
- /@supports\b/.test(beforeElseText))) {
335
- return beforeElseText;
304
+ function gatherIfBranchCommentText(sourceCode, node, parent, context) {
305
+ const { annotationPlacement, beforeText } = context;
306
+ if (node.type !== "IfStatement") {
307
+ return null;
336
308
  }
337
- if (!hasValidElseIfBlockLoc(node)) {
338
- return beforeText;
309
+ if ((0, branch_annotation_if_helpers_1.isElseIfBranch)(node, parent)) {
310
+ return (0, branch_annotation_if_helpers_1.gatherElseIfCommentText)(sourceCode, node, parent, {
311
+ annotationPlacement,
312
+ beforeText,
313
+ });
339
314
  }
340
- const betweenText = scanElseIfBetweenConditionAndBody(sourceCode, node);
341
- if (betweenText) {
342
- return betweenText;
315
+ return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
316
+ }
317
+ /**
318
+ * Internal helper that performs type-based dispatch for gathering branch comment text.
319
+ * This keeps the public gatherBranchCommentTextByType wrapper small for ESLint limits.
320
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
321
+ * @supports REQ-COMMENT-ASSOCIATION
322
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
323
+ * @supports REQ-DUAL-POSITION-DETECTION
324
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
325
+ * @supports REQ-PLACEMENT-CONFIG
326
+ */
327
+ function gatherBranchCommentTextByTypeInternal(sourceCode, node, parent, context) {
328
+ const nonIfResult = gatherNonIfBranchCommentText(sourceCode, node, context);
329
+ if (nonIfResult != null) {
330
+ return nonIfResult;
343
331
  }
344
- const insideText = scanElseIfInsideBlockComments(sourceCode, node);
345
- if (insideText) {
346
- return insideText;
332
+ const ifResult = gatherIfBranchCommentText(sourceCode, node, parent, context);
333
+ if (ifResult != null) {
334
+ return ifResult;
347
335
  }
348
- return beforeText;
336
+ return null;
349
337
  }
350
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
351
- function gatherSwitchCaseCommentText(sourceCode, node) {
352
- const lines = sourceCode.lines;
353
- const startLine = node.loc.start.line;
354
- let i = startLine - PRE_COMMENT_OFFSET;
355
- const comments = [];
356
- while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
357
- comments.unshift(lines[i].trim());
358
- i--;
359
- }
360
- return comments.join(" ");
338
+ function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
339
+ return gatherBranchCommentTextByTypeInternal(sourceCode, node, parent, context);
361
340
  }
362
341
  /**
363
342
  * Gather leading comment text for a branch node.
@@ -368,47 +347,14 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
368
347
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
369
348
  */
370
349
  function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement = "before") {
371
- /**
372
- * Conditional branch for SwitchCase nodes that may include inline comments.
373
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
374
- * @req REQ-TRACEABILITY-SWITCHCASE-COMMENTS - Trace collection of preceding comments for SwitchCase
375
- */
376
- if (node.type === "SwitchCase") {
377
- return gatherSwitchCaseCommentText(sourceCode, node);
378
- }
379
350
  const beforeComments = sourceCode.getCommentsBefore(node) || [];
380
351
  const beforeText = beforeComments.map(extractCommentValue).join(" ");
381
- if (node.type === "CatchClause") {
382
- return gatherCatchClauseCommentText(sourceCode, node, beforeText);
383
- }
384
- /**
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).
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
390
- * @supports REQ-DUAL-POSITION-DETECTION
391
- * @supports REQ-INSIDE-BRACE-PLACEMENT
392
- * @supports REQ-PLACEMENT-CONFIG
393
- * @supports REQ-DEFAULT-BACKWARD-COMPAT
394
- */
395
- if (node.type === "IfStatement") {
396
- if (isElseIfBranch(node, parent)) {
397
- return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
398
- }
399
- return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
400
- }
401
- /**
402
- * Conditional branch for loop nodes that may include annotations either on the loop
403
- * statement itself or at the top of the loop body, allowing flexible placement.
404
- * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-LOOP-ANNOTATION REQ-LOOP-PLACEMENT-FLEXIBLE
405
- */
406
- if (node.type === "ForStatement" ||
407
- node.type === "ForInStatement" ||
408
- node.type === "ForOfStatement" ||
409
- node.type === "WhileStatement" ||
410
- node.type === "DoWhileStatement") {
411
- return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, beforeText);
352
+ const handled = gatherBranchCommentTextByType(sourceCode, node, parent, {
353
+ annotationPlacement,
354
+ beforeText,
355
+ });
356
+ if (handled != null) {
357
+ return handled;
412
358
  }
413
359
  return beforeText;
414
360
  }
@@ -0,0 +1,12 @@
1
+ import type { Rule } from "eslint";
2
+ import type { AnnotationPlacement } from "./branch-annotation-helpers";
3
+ export declare function isElseIfBranch(node: any, parent: any | undefined): boolean;
4
+ export declare function hasValidElseIfBlockLoc(node: any): boolean;
5
+ export declare function scanElseIfPrecedingComments(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
6
+ export declare function scanElseIfBetweenConditionAndBody(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
7
+ export declare function scanElseIfInsideBlockComments(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
8
+ export declare function getInsideElseIfCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
9
+ export declare function gatherElseIfCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent: any | undefined, options: {
10
+ annotationPlacement: AnnotationPlacement;
11
+ beforeText: string;
12
+ }): string;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isElseIfBranch = isElseIfBranch;
4
+ exports.hasValidElseIfBlockLoc = hasValidElseIfBlockLoc;
5
+ exports.scanElseIfPrecedingComments = scanElseIfPrecedingComments;
6
+ exports.scanElseIfBetweenConditionAndBody = scanElseIfBetweenConditionAndBody;
7
+ exports.scanElseIfInsideBlockComments = scanElseIfInsideBlockComments;
8
+ exports.getInsideElseIfCommentText = getInsideElseIfCommentText;
9
+ exports.gatherElseIfCommentText = gatherElseIfCommentText;
10
+ const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
11
+ /**
12
+ * Small shared helpers for IfStatement/else-if specific annotation handling.
13
+ * Extracted from branch-annotation-helpers to keep that file within ESLint
14
+ * max-lines limits while preserving behaviour.
15
+ *
16
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
17
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
18
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
19
+ */
20
+ const PRE_COMMENT_OFFSET = 2; // kept in sync with main helpers
21
+ function getCommentTextAtLine(lines, index) {
22
+ const line = lines[index];
23
+ if (!line || !line.trim()) {
24
+ return null;
25
+ }
26
+ if (!/^\s*(\/\/|\/\*)/.test(line)) {
27
+ return null;
28
+ }
29
+ return line.trim();
30
+ }
31
+ function isElseIfBranch(node, parent) {
32
+ return (node &&
33
+ node.type === "IfStatement" &&
34
+ parent &&
35
+ parent.type === "IfStatement" &&
36
+ parent.alternate === node);
37
+ }
38
+ function hasValidElseIfBlockLoc(node) {
39
+ const hasBlockConsequent = node.consequent &&
40
+ node.consequent.type === "BlockStatement" &&
41
+ node.consequent.loc &&
42
+ node.consequent.loc.start;
43
+ return !!(node.test &&
44
+ node.test.loc &&
45
+ node.test.loc.end &&
46
+ hasBlockConsequent);
47
+ }
48
+ function scanElseIfPrecedingComments(sourceCode, node) {
49
+ const lines = sourceCode.lines;
50
+ if (!node.loc || !node.loc.start || typeof node.loc.start.line !== "number") {
51
+ return "";
52
+ }
53
+ const startLine = node.loc.start.line - 1;
54
+ const comments = [];
55
+ let i = startLine - 1;
56
+ let scanned = 0;
57
+ while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
58
+ const commentText = getCommentTextAtLine(lines, i);
59
+ if (!commentText) {
60
+ break;
61
+ }
62
+ comments.unshift(commentText);
63
+ i--;
64
+ scanned++;
65
+ }
66
+ return comments.join(" ");
67
+ }
68
+ function scanElseIfBetweenConditionAndBody(sourceCode, node) {
69
+ const lines = sourceCode.lines;
70
+ const conditionEndLine = node.test.loc.end.line;
71
+ const consequentStartLine = node.consequent.loc.start.line;
72
+ const startIndex = conditionEndLine;
73
+ const endIndexExclusive = consequentStartLine - 1;
74
+ if (endIndexExclusive <= startIndex) {
75
+ return "";
76
+ }
77
+ return (0, branch_annotation_helpers_1.scanCommentLinesInRange)(lines, startIndex, endIndexExclusive - 1);
78
+ }
79
+ function scanElseIfInsideBlockComments(sourceCode, node) {
80
+ const lines = sourceCode.lines;
81
+ const consequentStartLine = node.consequent.loc.start.line;
82
+ const comments = [];
83
+ let lineIndex = consequentStartLine;
84
+ while (lineIndex < lines.length) {
85
+ const lineText = getCommentTextAtLine(lines, lineIndex);
86
+ if (!lineText) {
87
+ break;
88
+ }
89
+ comments.push(lineText);
90
+ lineIndex++;
91
+ }
92
+ return comments.join(" ");
93
+ }
94
+ function getInsideElseIfCommentText(sourceCode, node) {
95
+ if (!hasValidElseIfBlockLoc(node)) {
96
+ return "";
97
+ }
98
+ const insideText = scanElseIfInsideBlockComments(sourceCode, node);
99
+ if (insideText) {
100
+ return insideText;
101
+ }
102
+ return "";
103
+ }
104
+ function gatherElseIfCommentText(sourceCode, node, parent, options) {
105
+ const { annotationPlacement, beforeText } = options;
106
+ if (!isElseIfBranch(node, parent)) {
107
+ return beforeText;
108
+ }
109
+ if (annotationPlacement === "inside") {
110
+ return getInsideElseIfCommentText(sourceCode, node);
111
+ }
112
+ if (beforeText &&
113
+ (/@story\b/.test(beforeText) ||
114
+ /@req\b/.test(beforeText) ||
115
+ /@supports\b/.test(beforeText))) {
116
+ return beforeText;
117
+ }
118
+ const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
119
+ if (beforeElseText &&
120
+ (/@story\b/.test(beforeElseText) ||
121
+ /@req\b/.test(beforeElseText) ||
122
+ /@supports\b/.test(beforeElseText))) {
123
+ return beforeElseText;
124
+ }
125
+ if (!hasValidElseIfBlockLoc(node)) {
126
+ return beforeText;
127
+ }
128
+ const betweenText = scanElseIfBetweenConditionAndBody(sourceCode, node);
129
+ if (betweenText) {
130
+ return betweenText;
131
+ }
132
+ const insideText = scanElseIfInsideBlockComments(sourceCode, node);
133
+ if (insideText) {
134
+ return insideText;
135
+ }
136
+ return beforeText;
137
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Rule } from "eslint";
2
+ import { type AnnotationPlacement } from "./branch-annotation-helpers";
2
3
  /**
3
4
  * Gather annotation text for loop branches, supporting annotations either on the
4
5
  * loop statement itself or on the first comment lines inside the loop body.
@@ -6,4 +7,4 @@ import type { Rule } from "eslint";
6
7
  * @req REQ-LOOP-ANNOTATION
7
8
  * @req REQ-LOOP-PLACEMENT-FLEXIBLE
8
9
  */
9
- export declare function gatherLoopCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, beforeText: string): string;
10
+ export declare function gatherLoopCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, annotationPlacement: AnnotationPlacement, beforeText: string): string;
@@ -2,19 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.gatherLoopCommentText = gatherLoopCommentText;
4
4
  const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
5
- /**
6
- * Gather annotation text for loop branches, supporting annotations either on the
7
- * loop statement itself or on the first comment lines inside the loop body.
8
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
9
- * @req REQ-LOOP-ANNOTATION
10
- * @req REQ-LOOP-PLACEMENT-FLEXIBLE
11
- */
12
- function gatherLoopCommentText(sourceCode, node, beforeText) {
13
- if (/@story\b/.test(beforeText) ||
14
- /@req\b/.test(beforeText) ||
15
- /@supports\b/.test(beforeText)) {
16
- return beforeText;
17
- }
5
+ function getInsideLoopCommentText(sourceCode, node) {
18
6
  const body = node.body;
19
7
  if (body &&
20
8
  body.type === "BlockStatement" &&
@@ -32,5 +20,31 @@ function gatherLoopCommentText(sourceCode, node, beforeText) {
32
20
  return insideText;
33
21
  }
34
22
  }
23
+ return "";
24
+ }
25
+ /**
26
+ * Gather annotation text for loop branches, supporting annotations either on the
27
+ * loop statement itself or on the first comment lines inside the loop body.
28
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
29
+ * @req REQ-LOOP-ANNOTATION
30
+ * @req REQ-LOOP-PLACEMENT-FLEXIBLE
31
+ */
32
+ function gatherLoopCommentText(sourceCode, node, annotationPlacement, beforeText) {
33
+ if (annotationPlacement === "inside") {
34
+ const insideText = getInsideLoopCommentText(sourceCode, node);
35
+ if (insideText) {
36
+ return insideText;
37
+ }
38
+ return "";
39
+ }
40
+ if (/@story\b/.test(beforeText) ||
41
+ /@req\b/.test(beforeText) ||
42
+ /@supports\b/.test(beforeText)) {
43
+ return beforeText;
44
+ }
45
+ const insideText = getInsideLoopCommentText(sourceCode, node);
46
+ if (insideText) {
47
+ return insideText;
48
+ }
35
49
  return beforeText;
36
50
  }
@@ -83,7 +83,8 @@ function getIfStatementIndentAndInsertPos(sourceCode, node, options, context) {
83
83
  }
84
84
  const isElseIf = isElseIfBranchForInsert(node, parent);
85
85
  const isSimpleIfInsidePlacement = annotationPlacement === "inside" && !isElseIf;
86
- if (isSimpleIfInsidePlacement || isElseIf) {
86
+ if (annotationPlacement === "inside" &&
87
+ (isSimpleIfInsidePlacement || isElseIf)) {
87
88
  const commentLine = node.consequent.loc.start.line + 1;
88
89
  const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
89
90
  indent = commentLineInfo.indent;
@@ -201,6 +201,28 @@ if (condition) {}`,
201
201
  // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
202
202
  // @req REQ-INSIDE-BRACE-PLACEMENT
203
203
  doSomething();
204
+ }`,
205
+ options: [{ annotationPlacement: "inside" }],
206
+ },
207
+ {
208
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] catch clause annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
209
+ code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
210
+ // @req REQ-BRANCH-TRY
211
+ try {
212
+ doSomething();
213
+ } catch (error) {
214
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
215
+ // @req REQ-INSIDE-CATCH
216
+ handleError(error);
217
+ }`,
218
+ options: [{ annotationPlacement: "inside" }],
219
+ },
220
+ {
221
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] for-of loop annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
222
+ code: `for (const item of items) {
223
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
224
+ // @req REQ-LOOP-INSIDE
225
+ process(item);
204
226
  }`,
205
227
  options: [{ annotationPlacement: "inside" }],
206
228
  },
@@ -437,6 +459,102 @@ if (condition) {
437
459
  if (condition) {
438
460
  // @story <story-file>.story.md
439
461
  doSomething();
462
+ }`,
463
+ errors: makeMissingAnnotationErrors("@story", "@req"),
464
+ },
465
+ {
466
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-loop annotations ignored when annotationPlacement: 'inside' for loops",
467
+ code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
468
+ // @req REQ-LOOP-BEFORE
469
+ for (const item of items) {
470
+ process(item);
471
+ }`,
472
+ options: [{ annotationPlacement: "inside" }],
473
+ output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
474
+ // @req REQ-LOOP-BEFORE
475
+ // @story <story-file>.story.md
476
+ for (const item of items) {
477
+ process(item);
478
+ }`,
479
+ errors: makeMissingAnnotationErrors("@story", "@req"),
480
+ },
481
+ {
482
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-catch annotations ignored when annotationPlacement: 'inside' for CatchClause",
483
+ code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
484
+ // @req REQ-BRANCH-TRY
485
+ try {
486
+ doSomething();
487
+ }
488
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
489
+ // @req REQ-CATCH-BEFORE
490
+ catch (error) {
491
+ handleError(error);
492
+ }`,
493
+ options: [{ annotationPlacement: "inside" }],
494
+ output: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
495
+ // @req REQ-BRANCH-TRY
496
+ try {
497
+ doSomething();
498
+ }
499
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
500
+ // @req REQ-CATCH-BEFORE
501
+ catch (error) {
502
+ // @story <story-file>.story.md
503
+ handleError(error);
504
+ }`,
505
+ errors: makeMissingAnnotationErrors("@story", "@req"),
506
+ },
507
+ {
508
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-else-if annotations ignored when annotationPlacement: 'inside' for else-if branch (Story 028.0)",
509
+ code: `if (a) {
510
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
511
+ // @req REQ-OUTER-IF-INSIDE
512
+ doA();
513
+ }
514
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
515
+ // @req REQ-ELSE-IF-BEFORE
516
+ else if (b) {
517
+ doB();
518
+ }`,
519
+ options: [{ annotationPlacement: "inside" }],
520
+ output: `if (a) {
521
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
522
+ // @req REQ-OUTER-IF-INSIDE
523
+ doA();
524
+ }
525
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
526
+ // @req REQ-ELSE-IF-BEFORE
527
+ else if (b) {
528
+ // @story <story-file>.story.md
529
+ doB();
530
+ }`,
531
+ errors: makeMissingAnnotationErrors("@story", "@req"),
532
+ },
533
+ {
534
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] else-if branch annotated inside block but initial if branch missing annotation under annotationPlacement: 'inside' (Story 028.0)",
535
+ code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
536
+ // @req REQ-INSIDE-OUTER-IF
537
+ if (a) {
538
+ doA();
539
+ } else if (b) {
540
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
541
+ // @req REQ-INSIDE-ELSE-IF
542
+ doB();
543
+ } else {
544
+ doC();
545
+ }`,
546
+ options: [{ annotationPlacement: "inside" }],
547
+ output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
548
+ // @req REQ-INSIDE-OUTER-IF
549
+ if (a) {
550
+ // @story <story-file>.story.md
551
+ doA();
552
+ } else if (b) {
553
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
554
+ // @req REQ-INSIDE-ELSE-IF
555
+ doB();
556
+ } else {
557
+ doC();
440
558
  }`,
441
559
  errors: makeMissingAnnotationErrors("@story", "@req"),
442
560
  },
@@ -8,7 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  */
9
9
  const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
10
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", () => {
11
+ it("[REQ-PRETTIER-AUTOFIX-ELSE-IF] inserts annotations before the else-if line in Prettier-compatible default 'before' mode", () => {
12
12
  const lines = [
13
13
  "if (a) {",
14
14
  " doA();",
@@ -24,6 +24,7 @@ describe("Else-if insert position (Story 026.0-DEV-ELSE-IF-ANNOTATION-POSITION)"
24
24
  })),
25
25
  };
26
26
  const context = {
27
+ options: [{ annotationPlacement: "before" }],
27
28
  getSourceCode() {
28
29
  return {
29
30
  lines,
@@ -69,12 +70,12 @@ describe("Else-if insert position (Story 026.0-DEV-ELSE-IF-ANNOTATION-POSITION)"
69
70
  expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
70
71
  const [range, text] = fixer.insertTextBeforeRange.mock
71
72
  .calls[0];
72
- // ensure we are inserting before the first statement in the else-if body (line 5)
73
+ // ensure we are inserting before the else-if line (line 4) when placement is 'before'
73
74
  const expectedIndex = context
74
75
  .getSourceCode()
75
- .getIndexFromLoc({ line: 5, column: 0 });
76
+ .getIndexFromLoc({ line: 4, column: 0 });
76
77
  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);
78
+ // and that the inserted text is prefixed with the base indentation from line 4
79
+ expect(text.startsWith("")).toBe(true);
79
80
  });
80
81
  });
@@ -110,6 +110,87 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
110
110
  expect(sourceCodeLoop.getCommentsBefore).toHaveBeenCalledWith(forNode);
111
111
  expect(loopText).toBe("@story loop branch story loop details");
112
112
  });
113
+ it("[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] uses inside-loop comments when annotationPlacement is 'inside' and ignores before-loop annotations", () => {
114
+ const sourceCode = {
115
+ lines: [
116
+ "// @story before-loop should be ignored in inside mode",
117
+ "for (const item of items) {",
118
+ " // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
119
+ " // @req REQ-LOOP-INSIDE",
120
+ " process(item);",
121
+ "}",
122
+ ],
123
+ getCommentsBefore: jest
124
+ .fn()
125
+ .mockReturnValue([
126
+ { value: "@story before-loop should be ignored in inside mode" },
127
+ ]),
128
+ };
129
+ const loopNode = {
130
+ type: "ForOfStatement",
131
+ loc: {
132
+ start: { line: 2, column: 0 },
133
+ end: { line: 5, column: 1 },
134
+ },
135
+ body: {
136
+ type: "BlockStatement",
137
+ loc: {
138
+ start: { line: 2, column: 27 },
139
+ end: { line: 5, column: 1 },
140
+ },
141
+ },
142
+ };
143
+ const parent = {
144
+ type: "BlockStatement",
145
+ body: [loopNode],
146
+ };
147
+ const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, loopNode, parent, "inside");
148
+ expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
149
+ expect(insideText).toContain("@req REQ-LOOP-INSIDE");
150
+ expect(insideText).not.toContain("before-loop should be ignored");
151
+ });
152
+ it("[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] uses inside-catch comments when annotationPlacement is 'inside' and ignores before-catch annotations", () => {
153
+ const sourceCode = {
154
+ lines: [
155
+ "// @story before-catch should be ignored in inside mode",
156
+ "try {",
157
+ " doSomething();",
158
+ "}",
159
+ "catch (error) {",
160
+ " // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
161
+ " // @req REQ-CATCH-INSIDE",
162
+ " handleError(error);",
163
+ "}",
164
+ ],
165
+ getCommentsBefore: jest
166
+ .fn()
167
+ .mockReturnValue([
168
+ { value: "@story before-catch should be ignored in inside mode" },
169
+ ]),
170
+ };
171
+ const catchNode = {
172
+ type: "CatchClause",
173
+ loc: {
174
+ start: { line: 5, column: 0 },
175
+ end: { line: 8, column: 1 },
176
+ },
177
+ body: {
178
+ type: "BlockStatement",
179
+ loc: {
180
+ start: { line: 5, column: 14 },
181
+ end: { line: 8, column: 1 },
182
+ },
183
+ },
184
+ };
185
+ const parent = {
186
+ type: "TryStatement",
187
+ handler: catchNode,
188
+ };
189
+ const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, catchNode, parent, "inside");
190
+ expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
191
+ expect(insideText).toContain("@req REQ-CATCH-INSIDE");
192
+ expect(insideText).not.toContain("before-catch should be ignored");
193
+ });
113
194
  });
114
195
  /**
115
196
  * Tests for annotationPlacement wiring at helper level
@@ -162,4 +243,72 @@ describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-AN
162
243
  expect(insideText).toContain("@req REQ-INSIDE");
163
244
  expect(insideText).not.toContain("@req REQ-BEFORE");
164
245
  });
246
+ it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors Story 028.0 inside-placement semantics for else-if branches while preserving Story 026.0 before-else behavior", () => {
247
+ const sourceCode = {
248
+ lines: [
249
+ "function demoElseIf(x) {", // 1
250
+ " if (x === 1) {", // 2
251
+ " // @story inside-if", // 3
252
+ " doOne();", // 4
253
+ " }", // 5
254
+ " // @story docs/stories/026.0-DEV-BRANCH-ANNOTATIONS-ELSE-BRANCHES.story.md", // 6 (before else-if)
255
+ " // @req REQ-BEFORE-ELSE", // 7
256
+ " else if (x === 2) {", // 8
257
+ " // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md", // 9 (inside else-if)
258
+ " // @req REQ-ELSE-IF-INSIDE", // 10
259
+ " doTwo();", // 11
260
+ " }", // 12
261
+ "}", // 13
262
+ ],
263
+ getCommentsBefore: jest.fn().mockImplementation((node) => {
264
+ // Simulate ESLint getCommentsBefore only returning comments that are truly
265
+ // "before" the node they are querying.
266
+ // Our chain has:
267
+ // - before-if comments not used in this test
268
+ // - line 6-7 as before-else-if comments
269
+ if (node && node.loc && node.loc.start && node.loc.start.line === 2) {
270
+ // before the initial if (not used in assertions here)
271
+ return [
272
+ { value: "@story BEFORE-IF" },
273
+ { value: "@req REQ-BEFORE-IF" },
274
+ ];
275
+ }
276
+ if (node && node.loc && node.loc.start && node.loc.start.line === 8) {
277
+ // before the else-if branch (Story 026.0 semantics)
278
+ return [
279
+ {
280
+ value: "@story docs/stories/026.0-DEV-BRANCH-ANNOTATIONS-ELSE-BRANCHES.story.md",
281
+ },
282
+ { value: "@req REQ-BEFORE-ELSE" },
283
+ ];
284
+ }
285
+ return [];
286
+ }),
287
+ };
288
+ const elseIfNode = {
289
+ type: "IfStatement",
290
+ loc: {
291
+ start: { line: 8, column: 2 },
292
+ end: { line: 12, column: 3 },
293
+ },
294
+ consequent: {
295
+ type: "BlockStatement",
296
+ loc: {
297
+ start: { line: 8, column: 22 },
298
+ end: { line: 12, column: 3 },
299
+ },
300
+ },
301
+ };
302
+ const parent = {
303
+ type: "IfStatement",
304
+ alternate: elseIfNode,
305
+ };
306
+ const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, elseIfNode, parent, "before");
307
+ expect(beforeText).toContain("@story docs/stories/026.0-DEV-BRANCH-ANNOTATIONS-ELSE-BRANCHES.story.md");
308
+ expect(beforeText).toContain("@req REQ-BEFORE-ELSE");
309
+ const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, elseIfNode, parent, "inside");
310
+ expect(insideText).toBe("");
311
+ expect(insideText).not.toContain("REQ-BEFORE-ELSE");
312
+ expect(insideText).not.toContain("docs/stories/026.0-DEV-BRANCH-ANNOTATIONS-ELSE-BRANCHES.story.md");
313
+ });
165
314
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
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",