eslint-plugin-traceability 1.13.0 → 1.14.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 +106 -33
- package/lib/src/rules/prefer-implements-annotation.js +137 -5
- package/lib/tests/integration/no-redundant-annotation.integration.test.d.ts +1 -0
- package/lib/tests/integration/no-redundant-annotation.integration.test.js +98 -0
- package/lib/tests/rules/no-redundant-annotation.test.js +91 -27
- package/lib/tests/rules/prefer-implements-annotation.test.js +23 -0
- package/package.json +1 -1
- package/user-docs/api-reference.md +13 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# [1.
|
|
1
|
+
# [1.14.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.13.1...v1.14.0) (2025-12-08)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* support inline [@supports](https://github.com/supports) migration in prefer-supports-annotation rule ([32b064d](https://github.com/voder-ai/eslint-plugin-traceability/commit/32b064daee35d867d74c3f4f9041c4b780918559))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
@@ -127,6 +127,97 @@ function debugScopePairs(scopeNode, scopePairs) {
|
|
|
127
127
|
}
|
|
128
128
|
console.log("[no-redundant-annotation] Scope node type=%s pairs=%o", scopeNode && scopeNode.type, Array.from(scopePairs));
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Walk up enclosing scopes starting from the given scope node and
|
|
132
|
+
* accumulate all story/requirement pairs, limited by maxScopeDepth.
|
|
133
|
+
*
|
|
134
|
+
* This keeps REQ-SCOPE-INHERITANCE and REQ-CONFIGURABLE-STRICTNESS
|
|
135
|
+
* aligned with the story's configuration model while delegating the
|
|
136
|
+
* actual comment parsing to getScopePairs.
|
|
137
|
+
*
|
|
138
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-SCOPE-INHERITANCE REQ-CONFIGURABLE-STRICTNESS
|
|
139
|
+
*/
|
|
140
|
+
function collectScopePairs(context, startingScopeNode, maxScopeDepth) {
|
|
141
|
+
const result = new Set();
|
|
142
|
+
if (!startingScopeNode || maxScopeDepth <= 0) {
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
let current = startingScopeNode;
|
|
146
|
+
let depth = 0;
|
|
147
|
+
while (current && depth < maxScopeDepth) {
|
|
148
|
+
const parent = current.parent;
|
|
149
|
+
const pairs = getScopePairs(context, current, parent);
|
|
150
|
+
for (const key of pairs) {
|
|
151
|
+
result.add(key);
|
|
152
|
+
}
|
|
153
|
+
current = parent;
|
|
154
|
+
depth += 1;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Determine whether a statement is redundant relative to the provided
|
|
160
|
+
* scopePairs and options, and when so return the associated annotation
|
|
161
|
+
* comments. Returns null when the statement should not be treated as
|
|
162
|
+
* redundant.
|
|
163
|
+
*
|
|
164
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-STATEMENT-SIGNIFICANCE REQ-CONFIGURABLE-STRICTNESS
|
|
165
|
+
*/
|
|
166
|
+
function getRedundantStatementContext(context, stmt, scopePairs, options) {
|
|
167
|
+
if (scopePairs.size === 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (!(0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)(stmt, options, branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const stmtComments = getStatementComments(context, stmt);
|
|
174
|
+
if (stmtComments.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const stmtPairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(stmtComments);
|
|
178
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
179
|
+
console.log("[no-redundant-annotation] Statement type=%s eligible=%s commentCount=%d pairs=%o", stmt && stmt.type, (0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)(stmt, options, branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES), stmtComments.length, Array.from(stmtPairs));
|
|
180
|
+
}
|
|
181
|
+
if (stmtPairs.size === 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
// When emphasis duplication is allowed, treat a single fully-covered
|
|
185
|
+
// pair as intentional emphasis and skip reporting.
|
|
186
|
+
if (options.allowEmphasisDuplication && stmtPairs.size === 1) {
|
|
187
|
+
if ((0, annotation_scope_analyzer_1.arePairsFullyCovered)(stmtPairs, scopePairs)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!(0, annotation_scope_analyzer_1.arePairsFullyCovered)(stmtPairs, scopePairs)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
// At this point the statement-level annotations are fully
|
|
195
|
+
// covered by the parent/ancestor scopes and therefore redundant.
|
|
196
|
+
const annotationComments = stmtComments.filter((comment) => {
|
|
197
|
+
const commentText = typeof comment.value === "string" ? comment.value : "";
|
|
198
|
+
return /@story\b|@req\b|@supports\b/.test(commentText);
|
|
199
|
+
});
|
|
200
|
+
if (annotationComments.length === 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return { comments: annotationComments };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Compute unique removal ranges for the given annotation comments.
|
|
207
|
+
*
|
|
208
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SAFE-REMOVAL
|
|
209
|
+
*/
|
|
210
|
+
function getRemovalRangesForAnnotationComments(comments, sourceCode) {
|
|
211
|
+
const rangeMap = new Map();
|
|
212
|
+
for (const comment of comments) {
|
|
213
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
214
|
+
const key = `${removalStart}:${removalEnd}`;
|
|
215
|
+
if (!rangeMap.has(key)) {
|
|
216
|
+
rangeMap.set(key, [removalStart, removalEnd]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return Array.from(rangeMap.values()).sort((a, b) => b[0] - a[0]);
|
|
220
|
+
}
|
|
130
221
|
/**
|
|
131
222
|
* Analyze a block's statements and report redundant traceability annotations.
|
|
132
223
|
*
|
|
@@ -137,42 +228,25 @@ function debugScopePairs(scopeNode, scopePairs) {
|
|
|
137
228
|
*/
|
|
138
229
|
function reportRedundantAnnotationsInBlock(context, blockNode, scopePairs, options) {
|
|
139
230
|
const statements = Array.isArray(blockNode.body) ? blockNode.body : [];
|
|
140
|
-
if (statements.length === 0)
|
|
231
|
+
if (statements.length === 0 || scopePairs.size === 0)
|
|
141
232
|
return;
|
|
233
|
+
const sourceCode = context.getSourceCode();
|
|
142
234
|
for (const stmt of statements) {
|
|
143
|
-
|
|
235
|
+
const info = getRedundantStatementContext(context, stmt, scopePairs, options);
|
|
236
|
+
if (!info) {
|
|
144
237
|
continue;
|
|
145
238
|
}
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
239
|
+
const ranges = getRemovalRangesForAnnotationComments(info.comments, sourceCode);
|
|
240
|
+
if (ranges.length === 0) {
|
|
148
241
|
continue;
|
|
149
242
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
if (!(0, annotation_scope_analyzer_1.arePairsFullyCovered)(stmtPairs, scopePairs)) {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
// At this point the statement-level annotations are fully
|
|
161
|
-
// covered by the parent scope and therefore redundant.
|
|
162
|
-
for (const comment of stmtComments) {
|
|
163
|
-
const commentText = typeof comment.value === "string" ? comment.value : "";
|
|
164
|
-
if (!/@story\b|@req\b|@supports\b/.test(commentText)) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, context.getSourceCode());
|
|
168
|
-
context.report({
|
|
169
|
-
node: stmt,
|
|
170
|
-
messageId: "redundantAnnotation",
|
|
171
|
-
fix(fixer) {
|
|
172
|
-
return fixer.removeRange([removalStart, removalEnd]);
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
}
|
|
243
|
+
context.report({
|
|
244
|
+
node: stmt,
|
|
245
|
+
messageId: "redundantAnnotation",
|
|
246
|
+
fix(fixer) {
|
|
247
|
+
return ranges.map(([start, end]) => fixer.removeRange([start, end]));
|
|
248
|
+
},
|
|
249
|
+
});
|
|
176
250
|
}
|
|
177
251
|
}
|
|
178
252
|
const rule = {
|
|
@@ -219,12 +293,11 @@ const rule = {
|
|
|
219
293
|
// @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL
|
|
220
294
|
BlockStatement(node) {
|
|
221
295
|
const parent = node.parent;
|
|
222
|
-
const scopeNode = parent;
|
|
223
296
|
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
224
297
|
console.log("[no-redundant-annotation] BlockStatement parent=%s statements=%d", parent && parent.type, Array.isArray(node.body) ? node.body.length : 0);
|
|
225
298
|
}
|
|
226
|
-
const scopePairs =
|
|
227
|
-
debugScopePairs(
|
|
299
|
+
const scopePairs = collectScopePairs(context, parent, options.maxScopeDepth);
|
|
300
|
+
debugScopePairs(parent, scopePairs);
|
|
228
301
|
if (scopePairs.size === 0)
|
|
229
302
|
return;
|
|
230
303
|
reportRedundantAnnotationsInBlock(context, node, scopePairs, options);
|
|
@@ -186,7 +186,7 @@ function hasMultipleStories(storyPaths) {
|
|
|
186
186
|
*
|
|
187
187
|
* @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-OPTIONAL-WARNING REQ-MULTI-STORY-DETECT REQ-AUTO-FIX REQ-VALID-OUTPUT
|
|
188
188
|
*/
|
|
189
|
-
function
|
|
189
|
+
function processBlockComment(comment, context) {
|
|
190
190
|
const { hasStory, hasReq, hasImplements, storyPaths } = analyzeComment(comment);
|
|
191
191
|
if (!hasStory || !hasReq) {
|
|
192
192
|
return;
|
|
@@ -215,6 +215,137 @@ function processComment(comment, context) {
|
|
|
215
215
|
fix: fix ?? undefined,
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
|
+
function getLinePrefixFromText(fullText) {
|
|
219
|
+
const match = fullText.match(/^(\s*\/\/\s*)/);
|
|
220
|
+
return match ? match[1] : "";
|
|
221
|
+
}
|
|
222
|
+
function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
|
|
223
|
+
const sourceCode = context.getSourceCode();
|
|
224
|
+
const storyComment = comments[storyIndex];
|
|
225
|
+
const storyNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(storyComment.value || "");
|
|
226
|
+
if (!storyNormalized || !/^@story\b/.test(storyNormalized)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const storyParts = storyNormalized.split(/\s+/);
|
|
230
|
+
if (storyParts.length !== MIN_STORY_TOKENS) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const storyPath = storyParts[1];
|
|
234
|
+
const reqIds = [];
|
|
235
|
+
for (const idx of reqIndices) {
|
|
236
|
+
const reqComment = comments[idx];
|
|
237
|
+
const reqNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(reqComment.value || "");
|
|
238
|
+
if (!reqNormalized || !/^@req\b/.test(reqNormalized)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const reqParts = reqNormalized.split(/\s+/);
|
|
242
|
+
if (reqParts.length !== MIN_REQ_TOKENS) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
reqIds.push(reqParts[1]);
|
|
246
|
+
}
|
|
247
|
+
if (!reqIds.length) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const fullText = sourceCode.text.slice(storyComment.range[0], storyComment.range[1]);
|
|
251
|
+
const linePrefix = getLinePrefixFromText(fullText);
|
|
252
|
+
const implAnnotation = `@supports ${storyPath} ${reqIds.join(" ")}`;
|
|
253
|
+
const implLine = `${linePrefix}${implAnnotation}`;
|
|
254
|
+
const start = storyComment.range[0];
|
|
255
|
+
const end = comments[reqIndices[reqIndices.length - 1]].range[1];
|
|
256
|
+
return (fixer) => fixer.replaceTextRange([start, end], implLine);
|
|
257
|
+
}
|
|
258
|
+
function handleInlineStorySequence(context, group, startIndex) {
|
|
259
|
+
const n = group.length;
|
|
260
|
+
const current = group[startIndex];
|
|
261
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
262
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
263
|
+
return startIndex + 1;
|
|
264
|
+
}
|
|
265
|
+
if (/^@supports\b/.test(normalized)) {
|
|
266
|
+
return startIndex + 1;
|
|
267
|
+
}
|
|
268
|
+
const storyIndex = startIndex;
|
|
269
|
+
const reqIndices = [];
|
|
270
|
+
let j = startIndex + 1;
|
|
271
|
+
while (j < n) {
|
|
272
|
+
const next = group[j];
|
|
273
|
+
const nextNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(next.value || "");
|
|
274
|
+
if (!nextNormalized || /^@supports\b/.test(nextNormalized)) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (/^@req\b/.test(nextNormalized)) {
|
|
278
|
+
reqIndices.push(j);
|
|
279
|
+
j += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
if (reqIndices.length === 0) {
|
|
285
|
+
context.report({
|
|
286
|
+
node: current,
|
|
287
|
+
messageId: "preferImplements",
|
|
288
|
+
});
|
|
289
|
+
return startIndex + 1;
|
|
290
|
+
}
|
|
291
|
+
const fix = tryBuildInlineAutoFix(context, group, storyIndex, reqIndices);
|
|
292
|
+
if (fix) {
|
|
293
|
+
context.report({
|
|
294
|
+
node: current,
|
|
295
|
+
messageId: "preferImplements",
|
|
296
|
+
fix,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
context.report({
|
|
301
|
+
node: current,
|
|
302
|
+
messageId: "preferImplements",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return reqIndices[reqIndices.length - 1] + 1;
|
|
306
|
+
}
|
|
307
|
+
function processInlineGroup(context, group) {
|
|
308
|
+
if (group.length === 0)
|
|
309
|
+
return;
|
|
310
|
+
const n = group.length;
|
|
311
|
+
let i = 0;
|
|
312
|
+
while (i < n) {
|
|
313
|
+
const current = group[i];
|
|
314
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
315
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
316
|
+
i += 1;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
i = handleInlineStorySequence(context, group, i);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Scan sequences of Line comments for inline legacy @story/@req patterns and
|
|
324
|
+
* report diagnostics and optional auto-fixes.
|
|
325
|
+
*/
|
|
326
|
+
function processInlineComments(context, lineComments) {
|
|
327
|
+
if (!lineComments.length)
|
|
328
|
+
return;
|
|
329
|
+
// Group by contiguous line numbers
|
|
330
|
+
let group = [lineComments[0]];
|
|
331
|
+
const flushGroup = () => {
|
|
332
|
+
processInlineGroup(context, group);
|
|
333
|
+
group = [];
|
|
334
|
+
};
|
|
335
|
+
for (let idx = 1; idx < lineComments.length; idx++) {
|
|
336
|
+
const prev = lineComments[idx - 1];
|
|
337
|
+
const curr = lineComments[idx];
|
|
338
|
+
if (curr.loc.start.line === prev.loc.start.line + 1 &&
|
|
339
|
+
curr.loc.start.column === prev.loc.start.column) {
|
|
340
|
+
group.push(curr);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
flushGroup();
|
|
344
|
+
group.push(curr);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
flushGroup();
|
|
348
|
+
}
|
|
218
349
|
/**
|
|
219
350
|
* ESLint rule: prefer-implements-annotation
|
|
220
351
|
*
|
|
@@ -292,11 +423,12 @@ const preferImplementsAnnotationRule = {
|
|
|
292
423
|
*/
|
|
293
424
|
Program() {
|
|
294
425
|
const comments = sourceCode.getAllComments() || [];
|
|
295
|
-
comments
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
processComment(comment, context);
|
|
426
|
+
const blockComments = comments.filter((comment) => comment.type === "Block");
|
|
427
|
+
blockComments.forEach((comment) => {
|
|
428
|
+
processBlockComment(comment, context);
|
|
299
429
|
});
|
|
430
|
+
const lineComments = comments.filter((comment) => comment.type === "Line");
|
|
431
|
+
processInlineComments(context, lineComments);
|
|
300
432
|
},
|
|
301
433
|
};
|
|
302
434
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* Integration tests for no-redundant-annotation rule across multiple files
|
|
8
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-SCOPE-INHERITANCE
|
|
9
|
+
*/
|
|
10
|
+
const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
|
|
11
|
+
const index_1 = __importDefault(require("../../src/index"));
|
|
12
|
+
async function lintTextWithConfig(text, filename, extraConfig = {}) {
|
|
13
|
+
const baseConfig = {
|
|
14
|
+
plugins: {
|
|
15
|
+
traceability: index_1.default,
|
|
16
|
+
},
|
|
17
|
+
rules: {},
|
|
18
|
+
};
|
|
19
|
+
const eslint = new use_at_your_own_risk_1.FlatESLint({
|
|
20
|
+
overrideConfig: [baseConfig, extraConfig],
|
|
21
|
+
overrideConfigFile: true,
|
|
22
|
+
ignore: false,
|
|
23
|
+
});
|
|
24
|
+
const [result] = await eslint.lintText(text, { filePath: filename });
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
describe("no-redundant-annotation integration (Story 027.0-DEV-REDUNDANT-ANNOTATION-DETECTION)", () => {
|
|
28
|
+
it("[REQ-REDUNDANCY-PATTERNS] cleans up redundant annotations in multiple files while preserving required ones", async () => {
|
|
29
|
+
const codeA = `// @story docs/stories/003.0-EXAMPLE.story.md
|
|
30
|
+
// @req REQ-INIT
|
|
31
|
+
function init() {
|
|
32
|
+
// @story docs/stories/003.0-EXAMPLE.story.md
|
|
33
|
+
// @req REQ-INIT
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
const validator = new Validator(config);
|
|
36
|
+
}`;
|
|
37
|
+
const codeB = `/**
|
|
38
|
+
* @story docs/stories/004.0-EXAMPLE.story.md
|
|
39
|
+
* @req REQ-PROCESS
|
|
40
|
+
*/
|
|
41
|
+
function process(value) {
|
|
42
|
+
if (value) {
|
|
43
|
+
/* @story docs/stories/004.0-EXAMPLE.story.md
|
|
44
|
+
* @req REQ-PROCESS
|
|
45
|
+
*/
|
|
46
|
+
return handle(value);
|
|
47
|
+
}
|
|
48
|
+
}`;
|
|
49
|
+
const config = {
|
|
50
|
+
rules: {
|
|
51
|
+
"traceability/no-redundant-annotation": ["warn"],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const [resultA, resultB] = await Promise.all([
|
|
55
|
+
lintTextWithConfig(codeA, "file-a.js", config),
|
|
56
|
+
lintTextWithConfig(codeB, "file-b.js", config),
|
|
57
|
+
]);
|
|
58
|
+
expect(resultA.messages.map((m) => m.ruleId)).toContain("traceability/no-redundant-annotation");
|
|
59
|
+
expect(resultB.messages.map((m) => m.ruleId)).toContain("traceability/no-redundant-annotation");
|
|
60
|
+
const fixerConfig = {
|
|
61
|
+
rules: {
|
|
62
|
+
"traceability/no-redundant-annotation": ["warn"],
|
|
63
|
+
},
|
|
64
|
+
fix: true,
|
|
65
|
+
};
|
|
66
|
+
const eslintFix = new use_at_your_own_risk_1.FlatESLint({
|
|
67
|
+
overrideConfig: [
|
|
68
|
+
{
|
|
69
|
+
plugins: { traceability: index_1.default },
|
|
70
|
+
rules: fixerConfig.rules,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
overrideConfigFile: true,
|
|
74
|
+
ignore: false,
|
|
75
|
+
fix: true,
|
|
76
|
+
});
|
|
77
|
+
const [fixedA, fixedB] = await Promise.all([
|
|
78
|
+
(async () => {
|
|
79
|
+
const [result] = await eslintFix.lintText(codeA, {
|
|
80
|
+
filePath: "file-a.js",
|
|
81
|
+
});
|
|
82
|
+
return result;
|
|
83
|
+
})(),
|
|
84
|
+
(async () => {
|
|
85
|
+
const [result] = await eslintFix.lintText(codeB, {
|
|
86
|
+
filePath: "file-b.js",
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
})(),
|
|
90
|
+
]);
|
|
91
|
+
expect(fixedA.output).toContain("// @story docs/stories/003.0-EXAMPLE.story.md");
|
|
92
|
+
expect(fixedA.output).toContain("// @req REQ-INIT");
|
|
93
|
+
expect(fixedA.output).not.toContain("// @req REQ-INIT\n const config");
|
|
94
|
+
expect(fixedB.output).toContain("@story docs/stories/004.0-EXAMPLE.story.md");
|
|
95
|
+
expect(fixedB.output).toContain("@req REQ-PROCESS");
|
|
96
|
+
expect(fixedB.output).not.toContain("@req REQ-PROCESS\n */\n return");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -31,33 +31,97 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
|
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
33
|
invalid: [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
{
|
|
35
|
+
name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation on simple return inside annotated if",
|
|
36
|
+
code: `function example() {
|
|
37
|
+
// @story docs/stories/004.0-EXAMPLE.story.md
|
|
38
|
+
// @req REQ-PROCESS
|
|
39
|
+
if (condition) {
|
|
40
|
+
/* @story docs/stories/004.0-EXAMPLE.story.md\n * @req REQ-PROCESS
|
|
41
|
+
*/
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}`,
|
|
45
|
+
output: `function example() {
|
|
46
|
+
// @story docs/stories/004.0-EXAMPLE.story.md
|
|
47
|
+
// @req REQ-PROCESS
|
|
48
|
+
if (condition) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
}`,
|
|
52
|
+
errors: [
|
|
53
|
+
{
|
|
54
|
+
messageId: "redundantAnnotation",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "[REQ-DUPLICATION-DETECTION] flags redundant annotations on sequential simple statements in same scope",
|
|
60
|
+
code: `// @story docs/stories/003.0-EXAMPLE.story.md\n// @req REQ-INIT\nfunction init() {\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n const config = loadConfig();\n const validator = new Validator(config);\n}`,
|
|
61
|
+
output: `// @story docs/stories/003.0-EXAMPLE.story.md\n// @req REQ-INIT\nfunction init() {\n const config = loadConfig();\n const validator = new Validator(config);\n}`,
|
|
62
|
+
errors: [{ messageId: "redundantAnnotation" }],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "[REQ-SAFE-REMOVAL] removes full-line redundant comment without touching code on same line above",
|
|
66
|
+
code: `function example() {\n const keep = 1;\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n if (flag) {\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n const value = 1;\n }\n}`,
|
|
67
|
+
output: `function example() {\n const keep = 1;\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n if (flag) {\n const value = 1;\n }\n}`,
|
|
68
|
+
errors: [{ messageId: "redundantAnnotation" }],
|
|
69
|
+
},
|
|
70
|
+
// TODO: rule implementation exists; full invalid-case behavior tests pending refinement
|
|
71
|
+
// {
|
|
72
|
+
// name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation on simple return inside annotated if",
|
|
73
|
+
// code: `function example() {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n if (condition) {\n // @req REQ-PROCESS\n return value;\n }\n}`,
|
|
74
|
+
// output: `function example() {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n if (condition) {\n return value;\n }\n}`,
|
|
75
|
+
// errors: [
|
|
76
|
+
// {
|
|
77
|
+
// messageId: "redundantAnnotation",
|
|
78
|
+
// },
|
|
79
|
+
// ],
|
|
80
|
+
// },
|
|
81
|
+
// {
|
|
82
|
+
// name: "[REQ-DUPLICATION-DETECTION] flags redundant annotations on sequential simple statements in same scope",
|
|
83
|
+
// code: `// @story docs/stories/003.0-EXAMPLE.story.md\n// @req REQ-INIT\nfunction init() {\n // @req REQ-INIT\n const config = loadConfig();\n const validator = new Validator(config);\n}`,
|
|
84
|
+
// output: `// @story docs/stories/003.0-EXAMPLE.story.md\n// @req REQ-INIT\nfunction init() {\n const config = loadConfig();\n const validator = new Validator(config);\n}`,
|
|
85
|
+
// errors: [
|
|
86
|
+
// { messageId: "redundantAnnotation" },
|
|
87
|
+
// ],
|
|
88
|
+
// },
|
|
89
|
+
// {
|
|
90
|
+
// name: "[REQ-SAFE-REMOVAL] removes full-line redundant comment without touching code on same line above",
|
|
91
|
+
// code: `function example() {\n const keep = 1;\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n if (flag) {\n // @req REQ-INIT\n const value = 1;\n }\n}`,
|
|
92
|
+
// output: `function example() {\n const keep = 1;\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n if (flag) {\n const value = 1;\n }\n}`,
|
|
93
|
+
// errors: [
|
|
94
|
+
// { messageId: "redundantAnnotation" },
|
|
95
|
+
// ],
|
|
96
|
+
// },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
runRule({
|
|
100
|
+
valid: [
|
|
101
|
+
{
|
|
102
|
+
name: "[REQ-CONFIGURABLE-STRICTNESS] permissive mode does not flag expression statements as redundant",
|
|
103
|
+
options: [{ strictness: "permissive" }],
|
|
104
|
+
code: `function example() {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n if (condition) {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n doSomething();\n }\n}`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "[REQ-CONFIGURABLE-STRICTNESS] allowEmphasisDuplication skips single covered pair",
|
|
108
|
+
options: [{ allowEmphasisDuplication: true }],
|
|
109
|
+
code: `function example() {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n if (condition) {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n return value;\n }\n}`,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "[REQ-SCOPE-INHERITANCE] maxScopeDepth=1 does not treat grandparent function annotations as covering nested block",
|
|
113
|
+
options: [{ maxScopeDepth: 1 }],
|
|
114
|
+
code: `/**\n * @story docs/stories/004.0-EXAMPLE.story.md\n * @req REQ-PROCESS\n */\nfunction example() {\n if (outer) {\n {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n const value = compute();\n }\n }\n}`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
invalid: [
|
|
118
|
+
{
|
|
119
|
+
name: "[REQ-SCOPE-INHERITANCE] maxScopeDepth>1 treats function-level annotations as covering nested block statements",
|
|
120
|
+
options: [{ maxScopeDepth: 4 }],
|
|
121
|
+
code: `/**\n * @story docs/stories/004.0-EXAMPLE.story.md\n * @req REQ-PROCESS\n */\nfunction example() {\n if (outer) {\n {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n const value = compute();\n }\n }\n}`,
|
|
122
|
+
output: `/**\n * @story docs/stories/004.0-EXAMPLE.story.md\n * @req REQ-PROCESS\n */\nfunction example() {\n if (outer) {\n {\n const value = compute();\n }\n }\n}`,
|
|
123
|
+
errors: [{ messageId: "redundantAnnotation" }],
|
|
124
|
+
},
|
|
61
125
|
],
|
|
62
126
|
});
|
|
63
127
|
});
|
|
@@ -87,6 +87,29 @@ describe("prefer-supports-annotation / prefer-implements-annotation aliasing (St
|
|
|
87
87
|
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
|
|
88
88
|
errors: [{ messageId: "preferImplements" }],
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] single inline // @story + // @req auto-fixes to single // @supports line above function",
|
|
92
|
+
code: `// @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n// @req REQ-INLINE-COMMENT-SUPPORT\nfunction inlineLegacy() {}`,
|
|
93
|
+
output: `// @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-INLINE-COMMENT-SUPPORT\nfunction inlineLegacy() {}`,
|
|
94
|
+
errors: [{ messageId: "preferImplements" }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] single inline // @story with multiple // @req lines auto-fixes to single // @supports containing all REQ IDs",
|
|
98
|
+
code: `// @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n// @req REQ-INLINE-COMMENT-SUPPORT\n// @req REQ-BRANCH-POSITION-PRESERVE\nfunction inlineMultiReq() {}`,
|
|
99
|
+
output: `// @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-INLINE-COMMENT-SUPPORT REQ-BRANCH-POSITION-PRESERVE\nfunction inlineMultiReq() {}`,
|
|
100
|
+
errors: [{ messageId: "preferImplements" }],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] inline // @story + // @req above statement is auto-fixed preserving branch position (REQ-BRANCH-POSITION-PRESERVE)",
|
|
104
|
+
code: `if (flag) {\n // @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n // @req REQ-BRANCH-POSITION-PRESERVE\n doSomething();\n}`,
|
|
105
|
+
output: `if (flag) {\n // @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-BRANCH-POSITION-PRESERVE\n doSomething();\n}`,
|
|
106
|
+
errors: [{ messageId: "preferImplements" }],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] complex inline // @req content is not safely auto-fixable but still reports preferImplements",
|
|
110
|
+
code: `// @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n// @req REQ-INLINE-COMMENT-SUPPORT extra description inline\nfunction inlineComplexReqNoAutoFix() {}`,
|
|
111
|
+
errors: [{ messageId: "preferImplements" }],
|
|
112
|
+
},
|
|
90
113
|
];
|
|
91
114
|
ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
|
|
92
115
|
valid,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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",
|
|
@@ -278,17 +278,17 @@ Options:
|
|
|
278
278
|
|
|
279
279
|
The rule accepts an optional configuration object:
|
|
280
280
|
|
|
281
|
-
- `strictness` (`"
|
|
282
|
-
- `"
|
|
283
|
-
- `"
|
|
284
|
-
- `"
|
|
285
|
-
- `allowEmphasisDuplication` (boolean, optional)
|
|
286
|
-
- `maxScopeDepth` (number, optional)
|
|
287
|
-
- `alwaysCovered` (string[], optional)
|
|
281
|
+
- `strictness` (`"strict" | "moderate" | "permissive"`, optional) Controls how broadly statements are considered eligible for redundancy.
|
|
282
|
+
- `"strict"` Treats any non-branch statement as a candidate for redundancy once it is covered by a containing annotated scope. This is the most aggressive mode and is useful in codebases that want to push almost all traceability down to function/branch level only.
|
|
283
|
+
- `"moderate"` (default) Focuses on obviously leaf-like statements: anything in `alwaysCovered` **plus** bare `ExpressionStatement` nodes (for example, simple calls or assignments) that are not themselves branches. This mode balances redundancy cleanup with readability.
|
|
284
|
+
- `"permissive"` Only treats AST node types listed in `alwaysCovered` as candidates. Other statements are ignored even when they are technically covered by an enclosing scope, which is useful when you prefer more explicit, local annotations.
|
|
285
|
+
- `allowEmphasisDuplication` (boolean, optional) When `true`, allows a statement-level annotation that repeats a **single** fully-covered story/requirement pair from its parent scope purely for emphasis (for example, a guard clause with its own comment) and **does not** report it as redundant. When omitted or `false` (the default), even emphasis-only duplicates are treated as redundant when they add no new coverage.
|
|
286
|
+
- `maxScopeDepth` (number, optional) Limits how far up the ancestor chain the rule searches for covering scopes when deciding whether a statements annotations are redundant. A value of `1` restricts checks to the immediate parent scope; larger values allow the rule to consider annotations on enclosing branches and functions further up the tree. The default is `3`, which is suitable for most common function and branch nesting patterns, but you can increase it (for example, to `4` or higher) in projects that use additional nested blocks inside annotated functions.
|
|
287
|
+
- `alwaysCovered` (string[], optional) List of AST statement `node.type` strings that your project treats as "always covered" by their containing scope when that scope is annotated. By default, the rule treats `ReturnStatement` and `VariableDeclaration` as always-covered leaf statements. You can extend or override this list to tune which statement types are considered trivial enough to inherit coverage from their parent scopes.
|
|
288
288
|
|
|
289
289
|
Behavior notes:
|
|
290
290
|
|
|
291
|
-
- The rule only inspects comments that contain recognized traceability annotations (`@story`, `@req`, `@supports`) and are attached to simple statements (returns, expression statements, variable declarations, and similar leaf nodes). It intentionally does **not** attempt to de-duplicate annotations on functions, classes, or major branches, which remain the responsibility of the core rules.
|
|
291
|
+
- The rule only inspects comments that contain recognized traceability annotations (`@story`, `@req`, `@supports`) and are attached to simple statements (returns, expression statements, variable declarations, and similar leaf nodes). It intentionally does **not** attempt to de-duplicate annotations on functions, classes, or major branches, which remain the responsibility of the core rules. When a statement has multiple redundant traceability comments (for example, a small comment block that repeats both @story and @req lines), the rule reports a **single** diagnostic for that statement and, in fix mode, removes all of the redundant annotation comments associated with it in a single grouped fix.
|
|
292
292
|
- Auto-fix removes only the redundant traceability lines (and any now-empty comment delimiters when safe) while preserving surrounding non-traceability text in the same comment where possible.
|
|
293
293
|
- When no enclosing scope with compatible coverage is found within `maxScopeDepth`, the annotation is not considered redundant and is left unchanged.
|
|
294
294
|
|
|
@@ -299,8 +299,8 @@ This rule is **not** enabled in the `recommended` or `strict` presets by default
|
|
|
299
299
|
```jsonc
|
|
300
300
|
{
|
|
301
301
|
"rules": {
|
|
302
|
-
"traceability/no-redundant-annotation": "warn"
|
|
303
|
-
}
|
|
302
|
+
"traceability/no-redundant-annotation": "warn",
|
|
303
|
+
},
|
|
304
304
|
}
|
|
305
305
|
```
|
|
306
306
|
|
|
@@ -349,7 +349,7 @@ Main behaviors:
|
|
|
349
349
|
Deliberate non‑targets and ignored comments:
|
|
350
350
|
|
|
351
351
|
- JSDoc blocks that contain **only** `@story`, **only** `@req`, or **only** `@supports` are **not** modified by this rule. They remain valid and continue to be governed solely by the core rules such as `require-story-annotation`, `require-req-annotation`, and `valid-annotation-format`.
|
|
352
|
-
- Inline or line comments like `// @story ...`, `// @req ...`, or `// @supports ...` are
|
|
352
|
+
- Inline or line comments like `// @story ...`, `// @req ...`, or `// @supports ...` are also supported in a limited, migration‑oriented way: when the rule detects a simple, consecutive pair or small run of `// @story ...` and `// @req ...` lines that are directly attached to a function or branch, it can, in `--fix` mode, consolidate them into a single `// @supports ...` line while preserving indentation and the comment’s relative position next to the code. More complex inline patterns (such as mixed traceability and non‑traceability content, multiple distinct stories, or interleaved unrelated comments) are still reported without auto‑fix for safety. As with JSDoc migration, this behavior is opt‑in: the rule remains disabled by default and must be explicitly enabled with your desired severity when you are ready to start migrating inline annotations.
|
|
353
353
|
- Any block that does not match the “single story + simple requirements, no supports” shape is treated conservatively: the rule may report a diagnostic to flag the legacy/mixed pattern, but it will not rewrite comments unless it is clearly safe.
|
|
354
354
|
|
|
355
355
|
Interaction with other rules:
|
|
@@ -741,4 +741,5 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
|
|
|
741
741
|
In CI:
|
|
742
742
|
|
|
743
743
|
```bash
|
|
744
|
-
npm run traceability:verify
|
|
744
|
+
npm run traceability:verify
|
|
745
|
+
```
|