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 +2 -2
- package/lib/src/rules/no-redundant-annotation.js +8 -1
- package/lib/src/utils/branch-annotation-helpers.d.ts +1 -1
- package/lib/src/utils/branch-annotation-helpers.js +61 -4
- package/lib/src/utils/branch-annotation-report-helpers.js +49 -13
- package/lib/tests/rules/require-branch-annotation.test.js +22 -4
- package/lib/tests/utils/branch-annotation-helpers.test.js +52 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# [1.
|
|
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
|
-
*
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
337
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
if (node.type === "IfStatement"
|
|
75
|
-
|
|
76
|
-
parent
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
200
|
-
code:
|
|
201
|
-
// @
|
|
202
|
-
|
|
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.
|
|
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",
|