eslint-plugin-traceability 1.12.0 → 1.13.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/index.js +2 -0
- package/lib/src/rules/helpers/require-story-core.js +61 -52
- package/lib/src/rules/no-redundant-annotation.d.ts +3 -0
- package/lib/src/rules/no-redundant-annotation.js +235 -0
- package/lib/src/utils/annotation-scope-analyzer.d.ts +107 -0
- package/lib/src/utils/annotation-scope-analyzer.js +233 -0
- package/lib/src/utils/branch-annotation-helpers.js +100 -51
- package/lib/tests/integration/dogfooding-validation.test.js +33 -1
- package/lib/tests/integration/else-if-annotation-prettier.integration.test.js +18 -79
- package/lib/tests/plugin-default-export-and-configs.test.js +1 -0
- package/lib/tests/rules/auto-fix-behavior-008.test.js +87 -1
- package/lib/tests/rules/no-redundant-annotation.test.d.ts +1 -0
- package/lib/tests/rules/no-redundant-annotation.test.js +63 -0
- package/lib/tests/rules/require-story-core.autofix.test.js +26 -0
- package/lib/tests/utils/annotation-scope-analyzer.test.d.ts +1 -0
- package/lib/tests/utils/annotation-scope-analyzer.test.js +77 -0
- package/lib/tests/utils/branch-annotation-else-if-position.test.js +38 -0
- package/lib/tests/utils/req-annotation-detection.test.js +46 -0
- package/package.json +1 -1
- package/user-docs/api-reference.md +37 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXPECTED_RANGE_LENGTH = void 0;
|
|
4
|
+
exports.toStoryReqKey = toStoryReqKey;
|
|
5
|
+
exports.extractStoryReqPairsFromText = extractStoryReqPairsFromText;
|
|
6
|
+
exports.extractStoryReqPairsFromComments = extractStoryReqPairsFromComments;
|
|
7
|
+
exports.arePairsFullyCovered = arePairsFullyCovered;
|
|
8
|
+
exports.isStatementEligibleForRedundancy = isStatementEligibleForRedundancy;
|
|
9
|
+
exports.getCommentRemovalRange = getCommentRemovalRange;
|
|
10
|
+
/**
|
|
11
|
+
* Shared types and helpers for redundant-annotation detection.
|
|
12
|
+
*
|
|
13
|
+
* These utilities focus on parsing traceability annotations from comment
|
|
14
|
+
* text and computing relationships between "scope" coverage and
|
|
15
|
+
* statement-level annotations. They are intentionally small, pure
|
|
16
|
+
* functions so that the ESLint rule can delegate most of its logic
|
|
17
|
+
* here while keeping its own create/visitor code shallow.
|
|
18
|
+
*
|
|
19
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION
|
|
20
|
+
*/
|
|
21
|
+
exports.EXPECTED_RANGE_LENGTH = 2; // [start, end]
|
|
22
|
+
/**
|
|
23
|
+
* Build a canonical key for a story/requirement pair.
|
|
24
|
+
*
|
|
25
|
+
* Empty story or requirement components are normalized to the empty
|
|
26
|
+
* string so that comparisons remain stable even when some annotations
|
|
27
|
+
* omit one side (for example, malformed or story-less @req lines).
|
|
28
|
+
*
|
|
29
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
|
|
30
|
+
*/
|
|
31
|
+
function toStoryReqKey(storyPath, reqId) {
|
|
32
|
+
const story = storyPath ?? "";
|
|
33
|
+
const req = reqId ?? "";
|
|
34
|
+
return `${story}|${req}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract story/requirement pairs from a snippet of comment text.
|
|
38
|
+
*
|
|
39
|
+
* Supported patterns:
|
|
40
|
+
* - `@story <path>` followed by one or more `@req <ID>` lines.
|
|
41
|
+
* - `@supports <path> <REQ-ID-1> <REQ-ID-2> ...` where each `REQ-*`
|
|
42
|
+
* token is treated as a separate pair bound to the same story path.
|
|
43
|
+
*
|
|
44
|
+
* The parser is intentionally conservative: it only creates pairs when
|
|
45
|
+
* it can confidently associate a requirement identifier with a story
|
|
46
|
+
* path. This avoids false positives in REQ-DIFFERENT-REQUIREMENTS by
|
|
47
|
+
* ensuring we never conflate different requirement IDs.
|
|
48
|
+
*
|
|
49
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
|
|
50
|
+
*/
|
|
51
|
+
function extractStoryReqPairsFromText(text) {
|
|
52
|
+
const pairs = new Set();
|
|
53
|
+
if (!text)
|
|
54
|
+
return pairs;
|
|
55
|
+
const lines = text.split(/\r?\n/);
|
|
56
|
+
let currentStory = null;
|
|
57
|
+
for (const rawLine of lines) {
|
|
58
|
+
const line = rawLine.trim();
|
|
59
|
+
if (!line)
|
|
60
|
+
continue;
|
|
61
|
+
// @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS
|
|
62
|
+
const storyMatch = line.match(/@story\s+(\S+)/);
|
|
63
|
+
if (storyMatch) {
|
|
64
|
+
currentStory = storyMatch[1];
|
|
65
|
+
}
|
|
66
|
+
// Handle explicit @req lines that follow the most recent @story.
|
|
67
|
+
const reqMatch = line.match(/@req\s+(\S+)/);
|
|
68
|
+
if (reqMatch && currentStory) {
|
|
69
|
+
pairs.add(toStoryReqKey(currentStory, reqMatch[1]));
|
|
70
|
+
}
|
|
71
|
+
// Handle consolidated @supports lines that encode both story and
|
|
72
|
+
// requirement identifiers on a single line.
|
|
73
|
+
const supportsMatch = line.match(/@supports\s+(\S+)\s+(.+)/);
|
|
74
|
+
if (supportsMatch) {
|
|
75
|
+
const storyPath = supportsMatch[1];
|
|
76
|
+
const tail = supportsMatch[2];
|
|
77
|
+
const tokens = tail
|
|
78
|
+
.split(/\s+/)
|
|
79
|
+
.filter((t) => /^REQ-[A-Z0-9-]+$/.test(t));
|
|
80
|
+
for (const reqId of tokens) {
|
|
81
|
+
pairs.add(toStoryReqKey(storyPath, reqId));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return pairs;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extract story/requirement pairs from a list of ESLint comment nodes.
|
|
89
|
+
*
|
|
90
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION
|
|
91
|
+
*/
|
|
92
|
+
function extractStoryReqPairsFromComments(comments) {
|
|
93
|
+
const pairs = new Set();
|
|
94
|
+
if (!Array.isArray(comments) || comments.length === 0) {
|
|
95
|
+
return pairs;
|
|
96
|
+
}
|
|
97
|
+
const combinedText = comments
|
|
98
|
+
.filter((comment) => comment && typeof comment.value === "string")
|
|
99
|
+
.map((comment) => comment.value)
|
|
100
|
+
.join("\n");
|
|
101
|
+
const fromComments = extractStoryReqPairsFromText(combinedText);
|
|
102
|
+
for (const key of fromComments) {
|
|
103
|
+
pairs.add(key);
|
|
104
|
+
}
|
|
105
|
+
return pairs;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Determine whether all story/requirement pairs in `child` are already
|
|
109
|
+
* covered by `parent`.
|
|
110
|
+
*
|
|
111
|
+
* This implements the core notion of redundancy: if a statement-level
|
|
112
|
+
* annotation only repeats the exact same story+requirement pairs that
|
|
113
|
+
* are already declared on its containing scope, it does not add any
|
|
114
|
+
* new traceability information.
|
|
115
|
+
*
|
|
116
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS REQ-SCOPE-INHERITANCE
|
|
117
|
+
*/
|
|
118
|
+
function arePairsFullyCovered(child, parent) {
|
|
119
|
+
if (child.size === 0)
|
|
120
|
+
return false;
|
|
121
|
+
if (parent.size === 0)
|
|
122
|
+
return false;
|
|
123
|
+
for (const key of child) {
|
|
124
|
+
if (!parent.has(key)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Decide whether a given statement node type should be considered
|
|
132
|
+
* "simple" or "significant" for redundancy detection, based on the
|
|
133
|
+
* configured strictness and alwaysCovered lists.
|
|
134
|
+
*
|
|
135
|
+
* - In `strict` mode, all non-branch statements are eligible.
|
|
136
|
+
* - In `moderate` mode (default), only statement types listed in
|
|
137
|
+
* `alwaysCovered` plus bare expression statements are treated as
|
|
138
|
+
* candidates for redundancy.
|
|
139
|
+
* - In `permissive` mode, only `alwaysCovered` types are considered.
|
|
140
|
+
*
|
|
141
|
+
* This keeps REQ-STATEMENT-SIGNIFICANCE and REQ-CONFIGURABLE-STRICTNESS
|
|
142
|
+
* aligned with the story's configuration model.
|
|
143
|
+
*
|
|
144
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-STATEMENT-SIGNIFICANCE REQ-CONFIGURABLE-STRICTNESS
|
|
145
|
+
*/
|
|
146
|
+
function isStatementEligibleForRedundancy(node, options, branchTypes) {
|
|
147
|
+
if (!node || typeof node.type !== "string") {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
// Never treat branch nodes themselves as "simple" statements; their
|
|
151
|
+
// annotations are typically intentional and should be preserved.
|
|
152
|
+
if (branchTypes.includes(node.type)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const alwaysCoveredSet = new Set(options.alwaysCovered);
|
|
156
|
+
if (alwaysCoveredSet.has(node.type)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (options.strictness === "permissive") {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (options.strictness === "moderate") {
|
|
163
|
+
// Treat side-effecting expression statements (e.g. assignments or
|
|
164
|
+
// simple calls) as eligible while still excluding more complex
|
|
165
|
+
// control-flow constructs.
|
|
166
|
+
return node.type === "ExpressionStatement";
|
|
167
|
+
}
|
|
168
|
+
// strict: any non-branch statement may be considered.
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compute the character range that should be removed when auto-fixing a
|
|
173
|
+
* redundant annotation comment.
|
|
174
|
+
*
|
|
175
|
+
* The implementation is conservative to satisfy REQ-SAFE-REMOVAL:
|
|
176
|
+
*
|
|
177
|
+
* - When the comment occupies its own line (only whitespace before the
|
|
178
|
+
* comment token), the removal range is expanded to include that
|
|
179
|
+
* leading whitespace and the trailing newline, so the entire line is
|
|
180
|
+
* removed.
|
|
181
|
+
* - When there is other code before the comment on the same line, only
|
|
182
|
+
* the comment text itself is removed, leaving surrounding code and
|
|
183
|
+
* whitespace intact.
|
|
184
|
+
*
|
|
185
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SAFE-REMOVAL
|
|
186
|
+
*/
|
|
187
|
+
function getCommentRemovalRange(comment, sourceCode) {
|
|
188
|
+
const fullText = sourceCode.getText();
|
|
189
|
+
const range = comment && comment.range;
|
|
190
|
+
if (!Array.isArray(range) || range.length !== exports.EXPECTED_RANGE_LENGTH) {
|
|
191
|
+
return [0, 0];
|
|
192
|
+
}
|
|
193
|
+
let [start, end] = range;
|
|
194
|
+
// Find the start of the current line.
|
|
195
|
+
let lineStart = start;
|
|
196
|
+
while (lineStart > 0) {
|
|
197
|
+
const ch = fullText.charAt(lineStart - 1);
|
|
198
|
+
if (ch === "\n" || ch === "\r")
|
|
199
|
+
break;
|
|
200
|
+
lineStart -= 1;
|
|
201
|
+
}
|
|
202
|
+
const leadingText = fullText.slice(lineStart, start);
|
|
203
|
+
const onlyWhitespaceBeforeComment = leadingText.trim().length === 0;
|
|
204
|
+
let removalStart = start;
|
|
205
|
+
let removalEnd = end;
|
|
206
|
+
if (onlyWhitespaceBeforeComment) {
|
|
207
|
+
removalStart = lineStart;
|
|
208
|
+
}
|
|
209
|
+
// Expand to consume trailing whitespace after the comment.
|
|
210
|
+
while (removalEnd < fullText.length) {
|
|
211
|
+
const ch = fullText.charAt(removalEnd);
|
|
212
|
+
if (ch === " " || ch === " ") {
|
|
213
|
+
removalEnd += 1;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Optionally include the newline when the comment owns the line.
|
|
220
|
+
if (onlyWhitespaceBeforeComment && removalEnd < fullText.length) {
|
|
221
|
+
const ch = fullText.charAt(removalEnd);
|
|
222
|
+
if (ch === "\r") {
|
|
223
|
+
removalEnd += 1;
|
|
224
|
+
if (fullText.charAt(removalEnd) === "\n") {
|
|
225
|
+
removalEnd += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (ch === "\n") {
|
|
229
|
+
removalEnd += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return [removalStart, removalEnd];
|
|
233
|
+
}
|
|
@@ -87,24 +87,69 @@ function extractCommentValue(_c) {
|
|
|
87
87
|
return _c.value;
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Extract trimmed comment text for a given source line index or return null
|
|
91
|
+
* when the line is blank or not a comment. This helper centralizes the
|
|
92
|
+
* formatter-aware rules used by branch helpers when scanning for contiguous
|
|
93
|
+
* comment lines around branches.
|
|
93
94
|
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
94
95
|
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
|
|
95
96
|
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
|
|
96
97
|
*/
|
|
97
|
-
function
|
|
98
|
+
function getCommentTextAtLine(lines, index) {
|
|
98
99
|
const line = lines[index];
|
|
99
100
|
if (!line || !line.trim()) {
|
|
100
|
-
return
|
|
101
|
+
return null;
|
|
101
102
|
}
|
|
102
103
|
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return line.trim();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Collect a single contiguous comment line at the given index, appending its
|
|
110
|
+
* trimmed text to the accumulator. Returns true when a valid comment was
|
|
111
|
+
* collected and false when scanning should stop (blank or non-comment line).
|
|
112
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
113
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
|
|
114
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
|
|
115
|
+
*/
|
|
116
|
+
function collectCommentLine(lines, index, comments) {
|
|
117
|
+
const commentText = getCommentTextAtLine(lines, index);
|
|
118
|
+
if (!commentText) {
|
|
103
119
|
return false;
|
|
104
120
|
}
|
|
105
|
-
comments.push(
|
|
121
|
+
comments.push(commentText);
|
|
106
122
|
return true;
|
|
107
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Scan contiguous formatter-aware comment lines between the provided 0-based
|
|
126
|
+
* start and end indices (inclusive), stopping when a non-comment or blank line
|
|
127
|
+
* is encountered. This helper is used as a line-based fallback when
|
|
128
|
+
* structured comment APIs are not available for branch bodies.
|
|
129
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
130
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC
|
|
131
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC-ELSE-IF
|
|
132
|
+
*/
|
|
133
|
+
function scanCommentLinesInRange(lines, startIndex, endIndexInclusive) {
|
|
134
|
+
if (!Array.isArray(lines) || lines.length === 0) {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
if (startIndex < 0 ||
|
|
138
|
+
startIndex >= lines.length ||
|
|
139
|
+
startIndex > endIndexInclusive) {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
const comments = [];
|
|
143
|
+
const lastIndex = Math.min(endIndexInclusive, lines.length - 1);
|
|
144
|
+
let i = startIndex;
|
|
145
|
+
while (i <= lastIndex) {
|
|
146
|
+
if (!collectCommentLine(lines, i, comments)) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
return comments.join(" ");
|
|
152
|
+
}
|
|
108
153
|
function isElseIfBranch(node, parent) {
|
|
109
154
|
return (node &&
|
|
110
155
|
node.type === "IfStatement" &&
|
|
@@ -139,15 +184,7 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
|
|
|
139
184
|
const lines = sourceCode.lines;
|
|
140
185
|
const startIndex = node.body.loc.start.line - 1;
|
|
141
186
|
const endIndex = node.body.loc.end.line - 1;
|
|
142
|
-
const
|
|
143
|
-
let i = startIndex + 1;
|
|
144
|
-
while (i <= endIndex) {
|
|
145
|
-
if (!collectCommentLine(lines, i, comments)) {
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
i++;
|
|
149
|
-
}
|
|
150
|
-
const insideText = comments.join(" ");
|
|
187
|
+
const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
|
|
151
188
|
if (insideText) {
|
|
152
189
|
return insideText;
|
|
153
190
|
}
|
|
@@ -165,14 +202,11 @@ function scanElseIfPrecedingComments(sourceCode, node) {
|
|
|
165
202
|
let i = startLine - 1;
|
|
166
203
|
let scanned = 0;
|
|
167
204
|
while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
205
|
+
const commentText = getCommentTextAtLine(lines, i);
|
|
206
|
+
if (!commentText) {
|
|
170
207
|
break;
|
|
171
208
|
}
|
|
172
|
-
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
comments.unshift(line.trim());
|
|
209
|
+
comments.unshift(commentText);
|
|
176
210
|
i--;
|
|
177
211
|
scanned++;
|
|
178
212
|
}
|
|
@@ -194,13 +228,16 @@ function scanElseIfBetweenConditionAndBody(sourceCode, node) {
|
|
|
194
228
|
const lines = sourceCode.lines;
|
|
195
229
|
const conditionEndLine = node.test.loc.end.line;
|
|
196
230
|
const consequentStartLine = node.consequent.loc.start.line;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
231
|
+
// Lines in sourceCode are 0-based indexes, but loc.line values are 1-based.
|
|
232
|
+
// We want to scan comments strictly between the condition and the
|
|
233
|
+
// consequent body, so we start at the line after the condition's end and
|
|
234
|
+
// stop at the line immediately before the consequent's starting line.
|
|
235
|
+
const startIndex = conditionEndLine; // already the next logical line index when 0-based
|
|
236
|
+
const endIndexExclusive = consequentStartLine - 1;
|
|
237
|
+
if (endIndexExclusive <= startIndex) {
|
|
238
|
+
return "";
|
|
202
239
|
}
|
|
203
|
-
return
|
|
240
|
+
return scanCommentLinesInRange(lines, startIndex, endIndexExclusive - 1);
|
|
204
241
|
}
|
|
205
242
|
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
206
243
|
function scanElseIfInsideBlockComments(sourceCode, node) {
|
|
@@ -228,7 +265,10 @@ function scanElseIfInsideBlockComments(sourceCode, node) {
|
|
|
228
265
|
* @supports REQ-FALLBACK-LOGIC
|
|
229
266
|
*/
|
|
230
267
|
function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
|
|
231
|
-
if (
|
|
268
|
+
if (beforeText &&
|
|
269
|
+
(/@story\b/.test(beforeText) ||
|
|
270
|
+
/@req\b/.test(beforeText) ||
|
|
271
|
+
/@supports\b/.test(beforeText))) {
|
|
232
272
|
return beforeText;
|
|
233
273
|
}
|
|
234
274
|
if (!isElseIfBranch(node, parent)) {
|
|
@@ -236,7 +276,9 @@ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
|
|
|
236
276
|
}
|
|
237
277
|
const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
|
|
238
278
|
if (beforeElseText &&
|
|
239
|
-
(/@story\b/.test(beforeElseText) ||
|
|
279
|
+
(/@story\b/.test(beforeElseText) ||
|
|
280
|
+
/@req\b/.test(beforeElseText) ||
|
|
281
|
+
/@supports\b/.test(beforeElseText))) {
|
|
240
282
|
return beforeElseText;
|
|
241
283
|
}
|
|
242
284
|
if (!hasValidElseIfBlockLoc(node)) {
|
|
@@ -375,12 +417,28 @@ function reportMissingReq(context, node, options) {
|
|
|
375
417
|
* @supports REQ-ANNOTATION-PARSING
|
|
376
418
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
377
419
|
*/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Compute indentation and insert position for the start of a given 1-based line
|
|
422
|
+
* number. This keeps indentation and fixer insert positions consistent across
|
|
423
|
+
* branch helpers that need to align auto-inserted comments with existing
|
|
424
|
+
* source formatting.
|
|
425
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-ANNOTATION-PARSING
|
|
426
|
+
*/
|
|
427
|
+
function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
|
|
428
|
+
const lines = sourceCode.lines;
|
|
429
|
+
let indent = fallbackIndent;
|
|
430
|
+
if (line >= 1 && line <= lines.length) {
|
|
431
|
+
const rawLine = lines[line - 1];
|
|
432
|
+
indent = rawLine.match(/^(\s*)/)?.[1] || fallbackIndent;
|
|
433
|
+
}
|
|
434
|
+
const insertPos = sourceCode.getIndexFromLoc({
|
|
435
|
+
line,
|
|
382
436
|
column: 0,
|
|
383
437
|
});
|
|
438
|
+
return { indent, insertPos };
|
|
439
|
+
}
|
|
440
|
+
function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
441
|
+
let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
|
|
384
442
|
if (node.type === "CatchClause" && node.body) {
|
|
385
443
|
const bodyNode = node.body;
|
|
386
444
|
const bodyStatements = Array.isArray(bodyNode.body)
|
|
@@ -391,22 +449,16 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
|
391
449
|
: undefined;
|
|
392
450
|
if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
|
|
393
451
|
const firstLine = firstStatement.loc.start.line;
|
|
394
|
-
const
|
|
395
|
-
indent =
|
|
396
|
-
insertPos =
|
|
397
|
-
line: firstLine,
|
|
398
|
-
column: 0,
|
|
399
|
-
});
|
|
452
|
+
const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, "");
|
|
453
|
+
indent = firstLineInfo.indent;
|
|
454
|
+
insertPos = firstLineInfo.insertPos;
|
|
400
455
|
}
|
|
401
456
|
else if (bodyNode.loc && bodyNode.loc.start) {
|
|
402
457
|
const blockLine = bodyNode.loc.start.line;
|
|
403
|
-
const
|
|
404
|
-
const innerIndent = `${
|
|
458
|
+
const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, "");
|
|
459
|
+
const innerIndent = `${blockLineInfo.indent} `;
|
|
405
460
|
indent = innerIndent;
|
|
406
|
-
insertPos =
|
|
407
|
-
line: blockLine,
|
|
408
|
-
column: 0,
|
|
409
|
-
});
|
|
461
|
+
insertPos = blockLineInfo.insertPos;
|
|
410
462
|
}
|
|
411
463
|
}
|
|
412
464
|
return { indent, insertPos };
|
|
@@ -433,12 +485,9 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
|
433
485
|
// For else-if blocks, align auto-fix comments with Prettier's tendency to place comments
|
|
434
486
|
// inside the wrapped block body; non-block consequents intentionally keep the default behavior.
|
|
435
487
|
const commentLine = node.consequent.loc.start.line + 1;
|
|
436
|
-
const
|
|
437
|
-
indent =
|
|
438
|
-
insertPos =
|
|
439
|
-
line: commentLine,
|
|
440
|
-
column: 0,
|
|
441
|
-
});
|
|
488
|
+
const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
|
|
489
|
+
indent = commentLineInfo.indent;
|
|
490
|
+
insertPos = commentLineInfo.insertPos;
|
|
442
491
|
}
|
|
443
492
|
return { missingStory, missingReq, indent, insertPos };
|
|
444
493
|
}
|
|
@@ -35,10 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
/**
|
|
37
37
|
* Dogfooding validation integration tests
|
|
38
|
-
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI
|
|
38
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI REQ-DOGFOODING-VERIFY REQ-DOGFOODING-PRESET
|
|
39
39
|
*/
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const child_process_1 = require("child_process");
|
|
42
|
+
const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
|
|
43
|
+
const index_1 = __importStar(require("../../src/index"));
|
|
42
44
|
/**
|
|
43
45
|
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
|
|
44
46
|
*/
|
|
@@ -91,4 +93,34 @@ describe("Dogfooding Validation (Story 023.0-MAINT-DOGFOODING-VALIDATION)", () =
|
|
|
91
93
|
expect(result.stdout).toContain("error");
|
|
92
94
|
expect(result.stdout).toContain("src/dogfood.ts");
|
|
93
95
|
});
|
|
96
|
+
it("[REQ-DOGFOODING-VERIFY] should report at least one traceability rule active for TS sources", () => {
|
|
97
|
+
/**
|
|
98
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-VERIFY
|
|
99
|
+
*/
|
|
100
|
+
const eslintConfig = require("../../eslint.config.js");
|
|
101
|
+
const tsConfig = getTsConfigFromEslintConfig(eslintConfig);
|
|
102
|
+
expect(tsConfig).toBeDefined();
|
|
103
|
+
const rules = tsConfig.rules || {};
|
|
104
|
+
const hasTraceabilityRule = Object.keys(rules).some((key) => key.startsWith("traceability/"));
|
|
105
|
+
expect(hasTraceabilityRule).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it("[REQ-DOGFOODING-PRESET] should be compatible with recommended preset usage without throwing", async () => {
|
|
108
|
+
/**
|
|
109
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-PRESET
|
|
110
|
+
*/
|
|
111
|
+
const config = [
|
|
112
|
+
{ plugins: { traceability: index_1.default }, rules: {} },
|
|
113
|
+
...index_1.configs.recommended,
|
|
114
|
+
];
|
|
115
|
+
const eslint = new use_at_your_own_risk_1.FlatESLint({
|
|
116
|
+
overrideConfig: config,
|
|
117
|
+
overrideConfigFile: true,
|
|
118
|
+
ignore: false,
|
|
119
|
+
});
|
|
120
|
+
const results = await eslint.lintText("function foo() {}", {
|
|
121
|
+
filePath: "example.ts",
|
|
122
|
+
});
|
|
123
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
124
|
+
expect(Array.isArray(results[0].messages)).toBe(true);
|
|
125
|
+
});
|
|
94
126
|
});
|
|
@@ -50,9 +50,8 @@ describe("Else-if annotations with Prettier (Story 026.0-DEV-ELSE-IF-ANNOTATION-
|
|
|
50
50
|
}
|
|
51
51
|
return result.stdout;
|
|
52
52
|
}
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
const original = `
|
|
53
|
+
it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
|
|
54
|
+
const original = `
|
|
56
55
|
function doA() {
|
|
57
56
|
return 1;
|
|
58
57
|
}
|
|
@@ -72,16 +71,16 @@ else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
|
72
71
|
doB();
|
|
73
72
|
}
|
|
74
73
|
`;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const formatted = formatWithPrettier(original);
|
|
75
|
+
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
76
|
+
// but the exact layout and comment movement may vary between versions.
|
|
77
|
+
expect(formatted).toContain("else if");
|
|
78
|
+
expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
79
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
80
|
+
expect(result.status).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
|
|
83
|
+
const original = `
|
|
85
84
|
function doA() {
|
|
86
85
|
return 1;
|
|
87
86
|
}
|
|
@@ -102,70 +101,10 @@ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherConditio
|
|
|
102
101
|
doB();
|
|
103
102
|
}
|
|
104
103
|
`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
|
|
114
|
-
const original = `
|
|
115
|
-
function doA() {
|
|
116
|
-
return 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function doB() {
|
|
120
|
-
return 2;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
124
|
-
// @req REQ-BRANCH-DETECTION
|
|
125
|
-
if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
|
|
126
|
-
doA();
|
|
127
|
-
}
|
|
128
|
-
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
129
|
-
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
130
|
-
else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
131
|
-
doB();
|
|
132
|
-
}
|
|
133
|
-
`;
|
|
134
|
-
const formatted = formatWithPrettier(original);
|
|
135
|
-
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
136
|
-
// but the exact layout and comment movement may vary between versions.
|
|
137
|
-
expect(formatted).toContain("else if");
|
|
138
|
-
expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
139
|
-
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
140
|
-
expect(result.status).toBe(0);
|
|
141
|
-
});
|
|
142
|
-
it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
|
|
143
|
-
const original = `
|
|
144
|
-
function doA() {
|
|
145
|
-
return 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function doB() {
|
|
149
|
-
return 2;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
153
|
-
// @req REQ-BRANCH-DETECTION
|
|
154
|
-
if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
|
|
155
|
-
doA();
|
|
156
|
-
} else if (
|
|
157
|
-
anotherVeryLongConditionThatForcesWrapping && someOtherCondition
|
|
158
|
-
) {
|
|
159
|
-
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
160
|
-
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
161
|
-
doB();
|
|
162
|
-
}
|
|
163
|
-
`;
|
|
164
|
-
const formatted = formatWithPrettier(original);
|
|
165
|
-
// Note: Prettier's exact layout of the else-if and its comments may differ between versions;
|
|
166
|
-
// the rule should accept any of the supported annotation positions regardless of formatting.
|
|
167
|
-
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
168
|
-
expect(result.status).toBe(0);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
104
|
+
const formatted = formatWithPrettier(original);
|
|
105
|
+
// Note: Prettier's exact layout of the else-if and its comments may differ between versions;
|
|
106
|
+
// the rule should accept any of the supported annotation positions regardless of formatting.
|
|
107
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
108
|
+
expect(result.status).toBe(0);
|
|
109
|
+
});
|
|
171
110
|
});
|
|
@@ -59,6 +59,7 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
|
|
|
59
59
|
"valid-req-reference",
|
|
60
60
|
"prefer-implements-annotation",
|
|
61
61
|
"require-test-traceability",
|
|
62
|
+
"no-redundant-annotation",
|
|
62
63
|
"prefer-supports-annotation",
|
|
63
64
|
];
|
|
64
65
|
// Act: get actual rule names from plugin
|