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 +2 -3
- package/lib/src/utils/branch-annotation-helpers.js +58 -118
- package/lib/src/utils/branch-annotation-if-helpers.d.ts +12 -0
- package/lib/src/utils/branch-annotation-if-helpers.js +137 -0
- package/lib/src/utils/branch-annotation-report-helpers.js +2 -1
- package/lib/tests/rules/require-branch-annotation.test.js +54 -0
- package/lib/tests/utils/branch-annotation-else-if-insert-position.test.js +6 -5
- package/lib/tests/utils/branch-annotation-helpers.test.js +68 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
## [1.19.
|
|
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
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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:
|
|
76
|
+
.getIndexFromLoc({ line: 4, column: 0 });
|
|
76
77
|
expect(range).toEqual([expectedIndex, expectedIndex]);
|
|
77
|
-
// and that the inserted text is prefixed with the
|
|
78
|
-
expect(text.startsWith("
|
|
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.
|
|
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",
|