eslint-plugin-traceability 1.19.1 → 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,10 +1,9 @@
1
- ## [1.19.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.0...v1.19.1) (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
4
  ### Bug Fixes
5
5
 
6
- * apply inside placement semantics to loop branches ([e0ba06f](https://github.com/voder-ai/eslint-plugin-traceability/commit/e0ba06fb9aba6947c6ac675f65cc1fb4639dc785))
7
- * honor inside placement for catch clauses in branch annotation rule ([0e7a8e0](https://github.com/voder-ai/eslint-plugin-traceability/commit/0e7a8e01505bfa1e37e013dfda00e866f975ad33))
6
+ * honor annotationPlacement for else-if branches and refactor helpers ([8f747dc](https://github.com/voder-ai/eslint-plugin-traceability/commit/8f747dc8546ae8e5033cfe4aa9c394ca668a45d5))
8
7
 
9
8
  # Changelog
10
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,14 +154,6 @@ 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
157
  function getInsideCatchCommentText(sourceCode, node) {
165
158
  const getCommentsInside = sourceCode.getCommentsInside;
166
159
  if (node.body && typeof getCommentsInside === "function") {
@@ -261,109 +254,6 @@ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, before
261
254
  return "";
262
255
  }
263
256
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
264
- function scanElseIfPrecedingComments(sourceCode, node) {
265
- const lines = sourceCode.lines;
266
- if (!node.loc || !node.loc.start || typeof node.loc.start.line !== "number") {
267
- return "";
268
- }
269
- const startLine = node.loc.start.line - 1;
270
- const comments = [];
271
- let i = startLine - 1;
272
- let scanned = 0;
273
- while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
274
- const commentText = getCommentTextAtLine(lines, i);
275
- if (!commentText) {
276
- break;
277
- }
278
- comments.unshift(commentText);
279
- i--;
280
- scanned++;
281
- }
282
- return comments.join(" ");
283
- }
284
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
285
- function hasValidElseIfBlockLoc(node) {
286
- const hasBlockConsequent = node.consequent &&
287
- node.consequent.type === "BlockStatement" &&
288
- node.consequent.loc &&
289
- node.consequent.loc.start;
290
- return !!(node.test &&
291
- node.test.loc &&
292
- node.test.loc.end &&
293
- hasBlockConsequent);
294
- }
295
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
296
- function scanElseIfBetweenConditionAndBody(sourceCode, node) {
297
- const lines = sourceCode.lines;
298
- const conditionEndLine = node.test.loc.end.line;
299
- const consequentStartLine = node.consequent.loc.start.line;
300
- // Lines in sourceCode are 0-based indexes, but loc.line values are 1-based.
301
- // We want to scan comments strictly between the condition and the
302
- // consequent body, so we start at the line after the condition's end and
303
- // stop at the line immediately before the consequent's starting line.
304
- const startIndex = conditionEndLine; // already the next logical line index when 0-based
305
- const endIndexExclusive = consequentStartLine - 1;
306
- if (endIndexExclusive <= startIndex) {
307
- return "";
308
- }
309
- return scanCommentLinesInRange(lines, startIndex, endIndexExclusive - 1);
310
- }
311
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
312
- function scanElseIfInsideBlockComments(sourceCode, node) {
313
- const lines = sourceCode.lines;
314
- const consequentStartLine = node.consequent.loc.start.line;
315
- const comments = [];
316
- // Intentionally start from the block's start line (using the same 1-based line value as provided by the parser)
317
- // so that, when indexing into sourceCode.lines, this corresponds to the first logical line inside the block body
318
- // for typical formatter layouts.
319
- let lineIndex = consequentStartLine;
320
- while (lineIndex < lines.length) {
321
- if (!collectCommentLine(lines, lineIndex, comments)) {
322
- break;
323
- }
324
- lineIndex++;
325
- }
326
- return comments.join(" ");
327
- }
328
- /**
329
- * Gather annotation text for IfStatement else-if branches, supporting comments placed
330
- * before the else keyword, between the else-if condition and the consequent body,
331
- * and in the first comment-only lines inside the consequent block body.
332
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
333
- * @supports REQ-DUAL-POSITION-DETECTION
334
- * @supports REQ-FALLBACK-LOGIC
335
- */
336
- function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
337
- if (beforeText &&
338
- (/@story\b/.test(beforeText) ||
339
- /@req\b/.test(beforeText) ||
340
- /@supports\b/.test(beforeText))) {
341
- return beforeText;
342
- }
343
- if (!isElseIfBranch(node, parent)) {
344
- return beforeText;
345
- }
346
- const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
347
- if (beforeElseText &&
348
- (/@story\b/.test(beforeElseText) ||
349
- /@req\b/.test(beforeElseText) ||
350
- /@supports\b/.test(beforeElseText))) {
351
- return beforeElseText;
352
- }
353
- if (!hasValidElseIfBlockLoc(node)) {
354
- return beforeText;
355
- }
356
- const betweenText = scanElseIfBetweenConditionAndBody(sourceCode, node);
357
- if (betweenText) {
358
- return betweenText;
359
- }
360
- const insideText = scanElseIfInsideBlockComments(sourceCode, node);
361
- if (insideText) {
362
- return insideText;
363
- }
364
- return beforeText;
365
- }
366
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
367
257
  function gatherSwitchCaseCommentText(sourceCode, node) {
368
258
  const lines = sourceCode.lines;
369
259
  const startLine = node.loc.start.line;
@@ -375,7 +265,15 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
375
265
  }
376
266
  return comments.join(" ");
377
267
  }
378
- function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
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) {
379
277
  const { annotationPlacement, beforeText } = context;
380
278
  if (node.type === "SwitchCase") {
381
279
  return gatherSwitchCaseCommentText(sourceCode, node);
@@ -383,12 +281,6 @@ function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
383
281
  if (node.type === "CatchClause") {
384
282
  return gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText);
385
283
  }
386
- if (node.type === "IfStatement") {
387
- if (isElseIfBranch(node, parent)) {
388
- return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
389
- }
390
- return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
391
- }
392
284
  if (node.type === "ForStatement" ||
393
285
  node.type === "ForInStatement" ||
394
286
  node.type === "ForOfStatement" ||
@@ -398,6 +290,54 @@ function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
398
290
  }
399
291
  return null;
400
292
  }
293
+ /**
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
298
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
299
+ * @supports REQ-DUAL-POSITION-DETECTION
300
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
301
+ * @supports REQ-PLACEMENT-CONFIG
302
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
303
+ */
304
+ function gatherIfBranchCommentText(sourceCode, node, parent, context) {
305
+ const { annotationPlacement, beforeText } = context;
306
+ if (node.type !== "IfStatement") {
307
+ return null;
308
+ }
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
+ });
314
+ }
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;
331
+ }
332
+ const ifResult = gatherIfBranchCommentText(sourceCode, node, parent, context);
333
+ if (ifResult != null) {
334
+ return ifResult;
335
+ }
336
+ return null;
337
+ }
338
+ function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
339
+ return gatherBranchCommentTextByTypeInternal(sourceCode, node, parent, context);
340
+ }
401
341
  /**
402
342
  * Gather leading comment text for a branch node.
403
343
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -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
+ }
@@ -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;
@@ -501,6 +501,60 @@ try {
501
501
  catch (error) {
502
502
  // @story <story-file>.story.md
503
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();
504
558
  }`,
505
559
  errors: makeMissingAnnotationErrors("@story", "@req"),
506
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
  });
@@ -243,4 +243,72 @@ describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-AN
243
243
  expect(insideText).toContain("@req REQ-INSIDE");
244
244
  expect(insideText).not.toContain("@req REQ-BEFORE");
245
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
+ });
246
314
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.19.1",
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",