eslint-plugin-traceability 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- # [1.18.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.17.1...v1.18.0) (2025-12-18)
1
+ # [1.19.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.18.0...v1.19.0) (2025-12-18)
2
2
 
3
3
 
4
4
  ### Features
5
5
 
6
- * add annotationPlacement option for branch annotations ([9cf4189](https://github.com/voder-ai/eslint-plugin-traceability/commit/9cf41897cdbc962eb41f304847ba8a911987d0fd))
6
+ * enforce inside-brace placement mode for branch annotations ([5c2129a](https://github.com/voder-ai/eslint-plugin-traceability/commit/5c2129a7c25717dc4ce14bf55bc4541ae07e5539))
7
7
 
8
8
  # Changelog
9
9
 
@@ -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
@@ -195,6 +195,55 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
195
195
  }
196
196
  return beforeText;
197
197
  }
198
+ /**
199
+ * Gather annotation text for simple IfStatement branches, honoring the configured placement.
200
+ * When placement is "before", this helper preserves the existing behavior by returning the
201
+ * leading comment text unchanged. When placement is "inside", it switches to inside-brace
202
+ * semantics and scans for comments at the top of the consequent block.
203
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
204
+ * @supports REQ-INSIDE-BRACE-PLACEMENT
205
+ * @supports REQ-PLACEMENT-CONFIG
206
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
207
+ */
208
+ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText) {
209
+ if (annotationPlacement === "before") {
210
+ return beforeText;
211
+ }
212
+ if (annotationPlacement !== "inside") {
213
+ return beforeText;
214
+ }
215
+ if (!node.consequent || node.consequent.type !== "BlockStatement") {
216
+ return "";
217
+ }
218
+ const consequent = node.consequent;
219
+ const getCommentsInside = sourceCode.getCommentsInside;
220
+ if (typeof getCommentsInside === "function") {
221
+ try {
222
+ const insideComments = getCommentsInside(consequent) || [];
223
+ const insideText = insideComments.map(extractCommentValue).join(" ");
224
+ if (insideText) {
225
+ return insideText;
226
+ }
227
+ }
228
+ catch {
229
+ // fall through to line-based fallback
230
+ }
231
+ }
232
+ if (consequent.loc &&
233
+ consequent.loc.start &&
234
+ consequent.loc.end &&
235
+ typeof consequent.loc.start.line === "number" &&
236
+ typeof consequent.loc.end.line === "number") {
237
+ const lines = sourceCode.lines;
238
+ const startIndex = consequent.loc.start.line - 1;
239
+ const endIndex = consequent.loc.end.line - 1;
240
+ const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
241
+ if (insideText) {
242
+ return insideText;
243
+ }
244
+ }
245
+ return "";
246
+ }
198
247
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
199
248
  function scanElseIfPrecedingComments(sourceCode, node) {
200
249
  const lines = sourceCode.lines;
@@ -318,7 +367,7 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
318
367
  * @supports REQ-DUAL-POSITION-DETECTION
319
368
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
320
369
  */
321
- function gatherBranchCommentText(sourceCode, node, parent, _annotationPlacement = "before") {
370
+ function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement = "before") {
322
371
  /**
323
372
  * Conditional branch for SwitchCase nodes that may include inline comments.
324
373
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -333,13 +382,21 @@ function gatherBranchCommentText(sourceCode, node, parent, _annotationPlacement
333
382
  return gatherCatchClauseCommentText(sourceCode, node, beforeText);
334
383
  }
335
384
  /**
336
- * Conditional branch for IfStatement else-if nodes that may include inline comments
337
- * after the else-if condition but before the consequent body.
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).
338
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
339
390
  * @supports REQ-DUAL-POSITION-DETECTION
391
+ * @supports REQ-INSIDE-BRACE-PLACEMENT
392
+ * @supports REQ-PLACEMENT-CONFIG
393
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
340
394
  */
341
395
  if (node.type === "IfStatement") {
342
- return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
396
+ if (isElseIfBranch(node, parent)) {
397
+ return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
398
+ }
399
+ return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
343
400
  }
344
401
  /**
345
402
  * Conditional branch for loop nodes that may include annotations either on the loop
@@ -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,12 @@ 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)",
200
- code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
201
- // @req REQ-PLACEMENT-CONFIG
202
- if (condition) {}`,
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
+ }`,
203
205
  options: [{ annotationPlacement: "inside" }],
204
206
  },
205
207
  {
@@ -422,6 +424,22 @@ if (a) {
422
424
  }`,
423
425
  errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
424
426
  },
427
+ {
428
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-brace annotations ignored when annotationPlacement: 'inside'",
429
+ code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
430
+ // @req REQ-BEFORE-BRACE-ERROR
431
+ if (condition) {
432
+ doSomething();
433
+ }`,
434
+ options: [{ annotationPlacement: "inside" }],
435
+ output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
436
+ // @req REQ-BEFORE-BRACE-ERROR
437
+ if (condition) {
438
+ // @story <story-file>.story.md
439
+ doSomething();
440
+ }`,
441
+ errors: makeMissingAnnotationErrors("@story", "@req"),
442
+ },
425
443
  ],
426
444
  });
427
445
  runRule({
@@ -111,3 +111,55 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
111
111
  expect(loopText).toBe("@story loop branch story loop details");
112
112
  });
113
113
  });
114
+ /**
115
+ * Tests for annotationPlacement wiring at helper level
116
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
117
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
118
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-DEFAULT-BACKWARD-COMPAT
119
+ */
120
+ describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION)", () => {
121
+ it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors configured placement for simple if-statements", () => {
122
+ const sourceCode = {
123
+ lines: [
124
+ "function demo() {",
125
+ " if (condition) {",
126
+ " // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
127
+ " // @req REQ-INSIDE",
128
+ " doSomething();",
129
+ " }",
130
+ "}",
131
+ ],
132
+ getCommentsBefore: jest
133
+ .fn()
134
+ .mockReturnValue([
135
+ { value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
136
+ { value: "@req REQ-BEFORE" },
137
+ ]),
138
+ };
139
+ const ifNode = {
140
+ type: "IfStatement",
141
+ loc: {
142
+ start: { line: 2, column: 2 },
143
+ end: { line: 5, column: 3 },
144
+ },
145
+ consequent: {
146
+ type: "BlockStatement",
147
+ loc: {
148
+ start: { line: 2, column: 18 },
149
+ end: { line: 5, column: 3 },
150
+ },
151
+ },
152
+ };
153
+ const parent = {
154
+ type: "BlockStatement",
155
+ body: [ifNode],
156
+ };
157
+ const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "before");
158
+ expect(beforeText).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
159
+ expect(beforeText).toContain("@req REQ-BEFORE");
160
+ const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "inside");
161
+ expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
162
+ expect(insideText).toContain("@req REQ-INSIDE");
163
+ expect(insideText).not.toContain("@req REQ-BEFORE");
164
+ });
165
+ });
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.0",
4
4
  "description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",