eslint-plugin-traceability 1.19.0 → 1.19.1

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,10 @@
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.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.0...v1.19.1) (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
+ * 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))
7
8
 
8
9
  # Changelog
9
10
 
@@ -161,16 +161,7 @@ function isElseIfBranch(node, parent) {
161
161
  parent.type === "IfStatement" &&
162
162
  parent.alternate === node);
163
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
- }
164
+ function getInsideCatchCommentText(sourceCode, node) {
174
165
  const getCommentsInside = sourceCode.getCommentsInside;
175
166
  if (node.body && typeof getCommentsInside === "function") {
176
167
  try {
@@ -193,6 +184,31 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
193
184
  return insideText;
194
185
  }
195
186
  }
187
+ return "";
188
+ }
189
+ /**
190
+ * Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
191
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
192
+ * @req REQ-DUAL-POSITION-DETECTION
193
+ * @req REQ-FALLBACK-LOGIC
194
+ */
195
+ function gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText) {
196
+ if (annotationPlacement === "inside") {
197
+ const insideText = getInsideCatchCommentText(sourceCode, node);
198
+ if (insideText) {
199
+ return insideText;
200
+ }
201
+ return "";
202
+ }
203
+ if (/@story\b/.test(beforeText) ||
204
+ /@req\b/.test(beforeText) ||
205
+ /@supports\b/.test(beforeText)) {
206
+ return beforeText;
207
+ }
208
+ const insideText = getInsideCatchCommentText(sourceCode, node);
209
+ if (insideText) {
210
+ return insideText;
211
+ }
196
212
  return beforeText;
197
213
  }
198
214
  /**
@@ -359,56 +375,46 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
359
375
  }
360
376
  return comments.join(" ");
361
377
  }
362
- /**
363
- * Gather leading comment text for a branch node.
364
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
365
- * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
366
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
367
- * @supports REQ-DUAL-POSITION-DETECTION
368
- * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
369
- */
370
- 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
- */
378
+ function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
379
+ const { annotationPlacement, beforeText } = context;
376
380
  if (node.type === "SwitchCase") {
377
381
  return gatherSwitchCaseCommentText(sourceCode, node);
378
382
  }
379
- const beforeComments = sourceCode.getCommentsBefore(node) || [];
380
- const beforeText = beforeComments.map(extractCommentValue).join(" ");
381
383
  if (node.type === "CatchClause") {
382
- return gatherCatchClauseCommentText(sourceCode, node, beforeText);
384
+ return gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText);
383
385
  }
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
386
  if (node.type === "IfStatement") {
396
387
  if (isElseIfBranch(node, parent)) {
397
388
  return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
398
389
  }
399
390
  return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
400
391
  }
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
392
  if (node.type === "ForStatement" ||
407
393
  node.type === "ForInStatement" ||
408
394
  node.type === "ForOfStatement" ||
409
395
  node.type === "WhileStatement" ||
410
396
  node.type === "DoWhileStatement") {
411
- return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, beforeText);
397
+ return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, annotationPlacement, beforeText);
398
+ }
399
+ return null;
400
+ }
401
+ /**
402
+ * Gather leading comment text for a branch node.
403
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
404
+ * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
405
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
406
+ * @supports REQ-DUAL-POSITION-DETECTION
407
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
408
+ */
409
+ function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement = "before") {
410
+ const beforeComments = sourceCode.getCommentsBefore(node) || [];
411
+ const beforeText = beforeComments.map(extractCommentValue).join(" ");
412
+ const handled = gatherBranchCommentTextByType(sourceCode, node, parent, {
413
+ annotationPlacement,
414
+ beforeText,
415
+ });
416
+ if (handled != null) {
417
+ return handled;
412
418
  }
413
419
  return beforeText;
414
420
  }
@@ -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
  }
@@ -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,48 @@ 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);
440
504
  }`,
441
505
  errors: makeMissingAnnotationErrors("@story", "@req"),
442
506
  },
@@ -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
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.1",
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",