eslint-plugin-traceability 1.18.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.18.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.17.1...v1.18.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
- * add annotationPlacement option for branch annotations ([9cf4189](https://github.com/voder-ai/eslint-plugin-traceability/commit/9cf41897cdbc962eb41f304847ba8a911987d0fd))
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
 
@@ -86,7 +86,14 @@ function getScopePairs(context, scopeNode, parent) {
86
86
  const sourceCode = context.getSourceCode();
87
87
  // Branch-style scope: use the branch helpers to collect comment text.
88
88
  if (branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES.includes(scopeNode.type)) {
89
- const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent);
89
+ /**
90
+ * Inside-brace annotations used as branch-level indicators (inside placement
91
+ * mode) should not be folded into scopePairs for redundancy purposes; only
92
+ * before-brace annotations define the covering scope here.
93
+ *
94
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-NON-REDUNDANT-INSIDE REQ-PLACEMENT-CONFIG
95
+ */
96
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent, "before");
90
97
  return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
91
98
  }
92
99
  const comments = getScopeCommentsFromJSDocAndLeading(sourceCode, scopeNode);
@@ -41,7 +41,7 @@ export declare function scanCommentLinesInRange(lines: string[], startIndex: num
41
41
  * @supports REQ-DUAL-POSITION-DETECTION
42
42
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
43
43
  */
44
- export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any, _annotationPlacement?: AnnotationPlacement): string;
44
+ export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any, annotationPlacement?: AnnotationPlacement): string;
45
45
  /**
46
46
  * Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
47
47
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -161,20 +161,81 @@ function isElseIfBranch(node, parent) {
161
161
  parent.type === "IfStatement" &&
162
162
  parent.alternate === node);
163
163
  }
164
+ function getInsideCatchCommentText(sourceCode, node) {
165
+ const getCommentsInside = sourceCode.getCommentsInside;
166
+ if (node.body && typeof getCommentsInside === "function") {
167
+ try {
168
+ const insideComments = getCommentsInside(node.body) || [];
169
+ const insideText = insideComments.map(extractCommentValue).join(" ");
170
+ if (insideText) {
171
+ return insideText;
172
+ }
173
+ }
174
+ catch {
175
+ // fall through to line-based fallback
176
+ }
177
+ }
178
+ if (node.body && node.body.loc && node.body.loc.start && node.body.loc.end) {
179
+ const lines = sourceCode.lines;
180
+ const startIndex = node.body.loc.start.line - 1;
181
+ const endIndex = node.body.loc.end.line - 1;
182
+ const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
183
+ if (insideText) {
184
+ return insideText;
185
+ }
186
+ }
187
+ return "";
188
+ }
164
189
  /**
165
190
  * Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
166
191
  * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
167
192
  * @req REQ-DUAL-POSITION-DETECTION
168
193
  * @req REQ-FALLBACK-LOGIC
169
194
  */
170
- function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
171
- if (/@story\b/.test(beforeText) || /@req\b/.test(beforeText)) {
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
+ }
212
+ return beforeText;
213
+ }
214
+ /**
215
+ * Gather annotation text for simple IfStatement branches, honoring the configured placement.
216
+ * When placement is "before", this helper preserves the existing behavior by returning the
217
+ * leading comment text unchanged. When placement is "inside", it switches to inside-brace
218
+ * semantics and scans for comments at the top of the consequent block.
219
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
220
+ * @supports REQ-INSIDE-BRACE-PLACEMENT
221
+ * @supports REQ-PLACEMENT-CONFIG
222
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
223
+ */
224
+ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText) {
225
+ if (annotationPlacement === "before") {
226
+ return beforeText;
227
+ }
228
+ if (annotationPlacement !== "inside") {
172
229
  return beforeText;
173
230
  }
231
+ if (!node.consequent || node.consequent.type !== "BlockStatement") {
232
+ return "";
233
+ }
234
+ const consequent = node.consequent;
174
235
  const getCommentsInside = sourceCode.getCommentsInside;
175
- if (node.body && typeof getCommentsInside === "function") {
236
+ if (typeof getCommentsInside === "function") {
176
237
  try {
177
- const insideComments = getCommentsInside(node.body) || [];
238
+ const insideComments = getCommentsInside(consequent) || [];
178
239
  const insideText = insideComments.map(extractCommentValue).join(" ");
179
240
  if (insideText) {
180
241
  return insideText;
@@ -184,16 +245,20 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
184
245
  // fall through to line-based fallback
185
246
  }
186
247
  }
187
- if (node.body && node.body.loc && node.body.loc.start && node.body.loc.end) {
248
+ if (consequent.loc &&
249
+ consequent.loc.start &&
250
+ consequent.loc.end &&
251
+ typeof consequent.loc.start.line === "number" &&
252
+ typeof consequent.loc.end.line === "number") {
188
253
  const lines = sourceCode.lines;
189
- const startIndex = node.body.loc.start.line - 1;
190
- const endIndex = node.body.loc.end.line - 1;
254
+ const startIndex = consequent.loc.start.line - 1;
255
+ const endIndex = consequent.loc.end.line - 1;
191
256
  const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
192
257
  if (insideText) {
193
258
  return insideText;
194
259
  }
195
260
  }
196
- return beforeText;
261
+ return "";
197
262
  }
198
263
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
199
264
  function scanElseIfPrecedingComments(sourceCode, node) {
@@ -310,48 +375,46 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
310
375
  }
311
376
  return comments.join(" ");
312
377
  }
313
- /**
314
- * Gather leading comment text for a branch node.
315
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
316
- * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
317
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
318
- * @supports REQ-DUAL-POSITION-DETECTION
319
- * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
320
- */
321
- function gatherBranchCommentText(sourceCode, node, parent, _annotationPlacement = "before") {
322
- /**
323
- * Conditional branch for SwitchCase nodes that may include inline comments.
324
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
325
- * @req REQ-TRACEABILITY-SWITCHCASE-COMMENTS - Trace collection of preceding comments for SwitchCase
326
- */
378
+ function gatherBranchCommentTextByType(sourceCode, node, parent, context) {
379
+ const { annotationPlacement, beforeText } = context;
327
380
  if (node.type === "SwitchCase") {
328
381
  return gatherSwitchCaseCommentText(sourceCode, node);
329
382
  }
330
- const beforeComments = sourceCode.getCommentsBefore(node) || [];
331
- const beforeText = beforeComments.map(extractCommentValue).join(" ");
332
383
  if (node.type === "CatchClause") {
333
- return gatherCatchClauseCommentText(sourceCode, node, beforeText);
384
+ return gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText);
334
385
  }
335
- /**
336
- * Conditional branch for IfStatement else-if nodes that may include inline comments
337
- * after the else-if condition but before the consequent body.
338
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
339
- * @supports REQ-DUAL-POSITION-DETECTION
340
- */
341
386
  if (node.type === "IfStatement") {
342
- return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
387
+ if (isElseIfBranch(node, parent)) {
388
+ return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
389
+ }
390
+ return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
343
391
  }
344
- /**
345
- * Conditional branch for loop nodes that may include annotations either on the loop
346
- * statement itself or at the top of the loop body, allowing flexible placement.
347
- * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-LOOP-ANNOTATION REQ-LOOP-PLACEMENT-FLEXIBLE
348
- */
349
392
  if (node.type === "ForStatement" ||
350
393
  node.type === "ForInStatement" ||
351
394
  node.type === "ForOfStatement" ||
352
395
  node.type === "WhileStatement" ||
353
396
  node.type === "DoWhileStatement") {
354
- 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;
355
418
  }
356
419
  return beforeText;
357
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
  }
@@ -52,6 +52,47 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement)
52
52
  }
53
53
  return { indent, insertPos };
54
54
  }
55
+ /**
56
+ * Determine whether a node represents an else-if branch that should be used for
57
+ * determining comment insertion position.
58
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
59
+ */
60
+ function isElseIfBranchForInsert(node, parent) {
61
+ return (node &&
62
+ node.type === "IfStatement" &&
63
+ parent &&
64
+ parent.type === "IfStatement" &&
65
+ parent.alternate === node);
66
+ }
67
+ /**
68
+ * Compute indentation and insert position for IfStatement branches, handling
69
+ * both simple if and else-if cases, respecting the configured annotation
70
+ * placement and indentation rules.
71
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
72
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG REQ-INDENTATION-CORRECT
73
+ */
74
+ function getIfStatementIndentAndInsertPos(sourceCode, node, options, context) {
75
+ const { parent, annotationPlacement } = options;
76
+ let { indent, insertPos } = context;
77
+ const hasBlockConsequent = node.consequent &&
78
+ node.consequent.type === "BlockStatement" &&
79
+ node.consequent.loc &&
80
+ node.consequent.loc.start;
81
+ if (!hasBlockConsequent) {
82
+ return context;
83
+ }
84
+ const isElseIf = isElseIfBranchForInsert(node, parent);
85
+ const isSimpleIfInsidePlacement = annotationPlacement === "inside" && !isElseIf;
86
+ if (isSimpleIfInsidePlacement || isElseIf) {
87
+ const commentLine = node.consequent.loc.start.line + 1;
88
+ const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
89
+ indent = commentLineInfo.indent;
90
+ insertPos = commentLineInfo.insertPos;
91
+ context.indent = indent;
92
+ context.insertPos = insertPos;
93
+ }
94
+ return context;
95
+ }
55
96
  /**
56
97
  * Compute which annotations are missing for a branch based on its gathered comment text.
57
98
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -70,19 +111,14 @@ function getBranchMissingFlags(sourceCode, node, parent, annotationPlacement) {
70
111
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
71
112
  */
72
113
  function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement) {
73
- let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
74
- if (node.type === "IfStatement" &&
75
- parent &&
76
- parent.type === "IfStatement" &&
77
- parent.alternate === node &&
78
- node.consequent &&
79
- node.consequent.type === "BlockStatement" &&
80
- node.consequent.loc &&
81
- node.consequent.loc.start) {
82
- const commentLine = node.consequent.loc.start.line + 1;
83
- const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
84
- indent = commentLineInfo.indent;
85
- insertPos = commentLineInfo.insertPos;
114
+ const { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
115
+ if (node.type === "IfStatement") {
116
+ const context = { indent, insertPos };
117
+ const updatedContext = getIfStatementIndentAndInsertPos(sourceCode, node, { parent, annotationPlacement }, context);
118
+ return {
119
+ indent: updatedContext.indent,
120
+ insertPos: updatedContext.insertPos,
121
+ };
86
122
  }
87
123
  return { indent, insertPos };
88
124
  }
@@ -196,10 +196,34 @@ if (condition) {}`,
196
196
  options: [{ annotationPlacement: "before" }],
197
197
  },
198
198
  {
199
- name: "[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] if-statement with before-brace annotations using annotationPlacement: 'inside' (temporary backward-compatible behavior)",
199
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] if-statement annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
200
+ code: `if (condition) {
201
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
202
+ // @req REQ-INSIDE-BRACE-PLACEMENT
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)",
200
209
  code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
201
- // @req REQ-PLACEMENT-CONFIG
202
- if (condition) {}`,
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);
226
+ }`,
203
227
  options: [{ annotationPlacement: "inside" }],
204
228
  },
205
229
  {
@@ -422,6 +446,64 @@ if (a) {
422
446
  }`,
423
447
  errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
424
448
  },
449
+ {
450
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-brace annotations ignored when annotationPlacement: 'inside'",
451
+ code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
452
+ // @req REQ-BEFORE-BRACE-ERROR
453
+ if (condition) {
454
+ doSomething();
455
+ }`,
456
+ options: [{ annotationPlacement: "inside" }],
457
+ output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
458
+ // @req REQ-BEFORE-BRACE-ERROR
459
+ if (condition) {
460
+ // @story <story-file>.story.md
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
+ },
425
507
  ],
426
508
  });
427
509
  runRule({
@@ -110,4 +110,137 @@ 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
+ });
194
+ });
195
+ /**
196
+ * Tests for annotationPlacement wiring at helper level
197
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
198
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
199
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-DEFAULT-BACKWARD-COMPAT
200
+ */
201
+ describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION)", () => {
202
+ it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors configured placement for simple if-statements", () => {
203
+ const sourceCode = {
204
+ lines: [
205
+ "function demo() {",
206
+ " if (condition) {",
207
+ " // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
208
+ " // @req REQ-INSIDE",
209
+ " doSomething();",
210
+ " }",
211
+ "}",
212
+ ],
213
+ getCommentsBefore: jest
214
+ .fn()
215
+ .mockReturnValue([
216
+ { value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
217
+ { value: "@req REQ-BEFORE" },
218
+ ]),
219
+ };
220
+ const ifNode = {
221
+ type: "IfStatement",
222
+ loc: {
223
+ start: { line: 2, column: 2 },
224
+ end: { line: 5, column: 3 },
225
+ },
226
+ consequent: {
227
+ type: "BlockStatement",
228
+ loc: {
229
+ start: { line: 2, column: 18 },
230
+ end: { line: 5, column: 3 },
231
+ },
232
+ },
233
+ };
234
+ const parent = {
235
+ type: "BlockStatement",
236
+ body: [ifNode],
237
+ };
238
+ const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "before");
239
+ expect(beforeText).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
240
+ expect(beforeText).toContain("@req REQ-BEFORE");
241
+ const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "inside");
242
+ expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
243
+ expect(insideText).toContain("@req REQ-INSIDE");
244
+ expect(insideText).not.toContain("@req REQ-BEFORE");
245
+ });
113
246
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.18.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",