eslint-plugin-traceability 1.12.1 → 1.13.1
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/no-redundant-annotation.d.ts +3 -0
- package/lib/src/rules/no-redundant-annotation.js +308 -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 +29 -22
- 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/plugin-default-export-and-configs.test.js +1 -0
- package/lib/tests/rules/no-redundant-annotation.test.d.ts +1 -0
- package/lib/tests/rules/no-redundant-annotation.test.js +127 -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/package.json +1 -1
- package/user-docs/api-reference.md +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
## [1.
|
|
1
|
+
## [1.13.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.13.0...v1.13.1) (2025-12-08)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* refine no-redundant-annotation rule tests and behavior ([7d72670](https://github.com/voder-ai/eslint-plugin-traceability/commit/7d726702e3ad2268778c06de3f3a9673033e3a61))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/lib/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const RULE_NAMES = [
|
|
|
19
19
|
"valid-req-reference",
|
|
20
20
|
"prefer-implements-annotation",
|
|
21
21
|
"require-test-traceability",
|
|
22
|
+
"no-redundant-annotation",
|
|
22
23
|
];
|
|
23
24
|
const rules = {};
|
|
24
25
|
exports.rules = rules;
|
|
@@ -156,6 +157,7 @@ const TRACEABILITY_RULE_SEVERITIES = {
|
|
|
156
157
|
"traceability/valid-story-reference": "error",
|
|
157
158
|
"traceability/valid-req-reference": "error",
|
|
158
159
|
"traceability/require-test-traceability": "error",
|
|
160
|
+
"traceability/no-redundant-annotation": "warn",
|
|
159
161
|
};
|
|
160
162
|
/**
|
|
161
163
|
* @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const branch_annotation_helpers_1 = require("../utils/branch-annotation-helpers");
|
|
4
|
+
const annotation_scope_analyzer_1 = require("../utils/annotation-scope-analyzer");
|
|
5
|
+
/**
|
|
6
|
+
* ESLint rule to detect redundant traceability annotations on statements
|
|
7
|
+
* that are already covered by their containing scope.
|
|
8
|
+
*
|
|
9
|
+
* This rule focuses on simple, statement-level patterns that the
|
|
10
|
+
* existing branch and function rules already treat as covered by
|
|
11
|
+
* surrounding annotations. It treats redundant annotations as
|
|
12
|
+
* maintainability concerns rather than correctness issues, and is
|
|
13
|
+
* therefore exposed as a warning-level rule by default.
|
|
14
|
+
*
|
|
15
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION REQ-STATEMENT-SIGNIFICANCE REQ-SAFE-REMOVAL REQ-DIFFERENT-REQUIREMENTS REQ-CONFIGURABLE-STRICTNESS REQ-SCOPE-INHERITANCE
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_ALWAYS_COVERED_STATEMENTS = [
|
|
18
|
+
"ReturnStatement",
|
|
19
|
+
"VariableDeclaration",
|
|
20
|
+
];
|
|
21
|
+
const DEFAULT_STRICTNESS = "moderate";
|
|
22
|
+
const DEFAULT_ALLOW_EMPHASIS_DUPLICATION = false;
|
|
23
|
+
const DEFAULT_MAX_SCOPE_DEPTH = 3;
|
|
24
|
+
function normalizeOptions(raw) {
|
|
25
|
+
const strictness = raw && typeof raw.strictness === "string"
|
|
26
|
+
? raw.strictness
|
|
27
|
+
: DEFAULT_STRICTNESS;
|
|
28
|
+
const allowEmphasisDuplication = typeof raw?.allowEmphasisDuplication === "boolean"
|
|
29
|
+
? raw.allowEmphasisDuplication
|
|
30
|
+
: DEFAULT_ALLOW_EMPHASIS_DUPLICATION;
|
|
31
|
+
const maxScopeDepth = typeof raw?.maxScopeDepth === "number" && raw.maxScopeDepth > 0
|
|
32
|
+
? raw.maxScopeDepth
|
|
33
|
+
: DEFAULT_MAX_SCOPE_DEPTH;
|
|
34
|
+
const alwaysCovered = Array.isArray(raw?.alwaysCovered)
|
|
35
|
+
? raw.alwaysCovered
|
|
36
|
+
: Array.from(DEFAULT_ALWAYS_COVERED_STATEMENTS);
|
|
37
|
+
return {
|
|
38
|
+
strictness,
|
|
39
|
+
allowEmphasisDuplication,
|
|
40
|
+
maxScopeDepth,
|
|
41
|
+
alwaysCovered,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute the story/requirement pairs for annotations that apply to the
|
|
46
|
+
* given scope node.
|
|
47
|
+
*
|
|
48
|
+
* For branch scopes we reuse the same comment-gathering helper used by
|
|
49
|
+
* the require-branch-annotation rule so that REQ-SCOPE-INHERITANCE
|
|
50
|
+
* aligns with existing behavior.
|
|
51
|
+
*
|
|
52
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-SCOPE-INHERITANCE
|
|
53
|
+
*/
|
|
54
|
+
function getScopePairs(context, scopeNode, parent) {
|
|
55
|
+
const sourceCode = context.getSourceCode();
|
|
56
|
+
// Branch-style scope: use the branch helpers to collect comment text.
|
|
57
|
+
if (branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES.includes(scopeNode.type)) {
|
|
58
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent);
|
|
59
|
+
return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
|
|
60
|
+
}
|
|
61
|
+
// Function-like scopes: collect from JSDoc and leading/before comments
|
|
62
|
+
const FUNCTION_LIKE_TYPES = new Set([
|
|
63
|
+
"FunctionDeclaration",
|
|
64
|
+
"FunctionExpression",
|
|
65
|
+
"ArrowFunctionExpression",
|
|
66
|
+
"MethodDefinition",
|
|
67
|
+
"TSDeclareFunction",
|
|
68
|
+
"TSMethodSignature",
|
|
69
|
+
]);
|
|
70
|
+
const comments = [];
|
|
71
|
+
if (FUNCTION_LIKE_TYPES.has(scopeNode.type)) {
|
|
72
|
+
const jsdoc = sourceCode.getJSDocComment
|
|
73
|
+
? sourceCode.getJSDocComment(scopeNode)
|
|
74
|
+
: null;
|
|
75
|
+
const before = sourceCode.getCommentsBefore
|
|
76
|
+
? sourceCode.getCommentsBefore(scopeNode) || []
|
|
77
|
+
: [];
|
|
78
|
+
if (jsdoc) {
|
|
79
|
+
comments.push(jsdoc);
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(scopeNode.leadingComments)) {
|
|
82
|
+
comments.push(...scopeNode.leadingComments);
|
|
83
|
+
}
|
|
84
|
+
comments.push(...before);
|
|
85
|
+
return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
|
|
86
|
+
}
|
|
87
|
+
// Fallback: inspect JSDoc and leading comments around the scope node.
|
|
88
|
+
const jsdoc = sourceCode.getJSDocComment
|
|
89
|
+
? sourceCode.getJSDocComment(scopeNode)
|
|
90
|
+
: null;
|
|
91
|
+
const before = sourceCode.getCommentsBefore
|
|
92
|
+
? sourceCode.getCommentsBefore(scopeNode) || []
|
|
93
|
+
: [];
|
|
94
|
+
if (jsdoc) {
|
|
95
|
+
comments.push(jsdoc);
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(scopeNode.leadingComments)) {
|
|
98
|
+
comments.push(...scopeNode.leadingComments);
|
|
99
|
+
}
|
|
100
|
+
comments.push(...before);
|
|
101
|
+
return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Collect the comments directly associated with a statement node.
|
|
105
|
+
*
|
|
106
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-STATEMENT-SIGNIFICANCE REQ-SCOPE-ANALYSIS
|
|
107
|
+
*/
|
|
108
|
+
function getStatementComments(context, node) {
|
|
109
|
+
const sourceCode = context.getSourceCode();
|
|
110
|
+
const comments = [];
|
|
111
|
+
if (sourceCode.getCommentsBefore) {
|
|
112
|
+
comments.push(...(sourceCode.getCommentsBefore(node) || []));
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(node.leadingComments)) {
|
|
115
|
+
comments.push(...node.leadingComments);
|
|
116
|
+
}
|
|
117
|
+
return comments;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Debug helper for logging scope-level pairs in TRACEABILITY_DEBUG mode.
|
|
121
|
+
*
|
|
122
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS
|
|
123
|
+
*/
|
|
124
|
+
function debugScopePairs(scopeNode, scopePairs) {
|
|
125
|
+
if (process.env.TRACEABILITY_DEBUG !== "1") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log("[no-redundant-annotation] Scope node type=%s pairs=%o", scopeNode && scopeNode.type, Array.from(scopePairs));
|
|
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
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Analyze a block's statements and report redundant traceability annotations.
|
|
223
|
+
*
|
|
224
|
+
* This helper encapsulates the iteration and reporting logic so that the
|
|
225
|
+
* BlockStatement visitor remains small and focused on scope setup.
|
|
226
|
+
*
|
|
227
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-STATEMENT-SIGNIFICANCE
|
|
228
|
+
*/
|
|
229
|
+
function reportRedundantAnnotationsInBlock(context, blockNode, scopePairs, options) {
|
|
230
|
+
const statements = Array.isArray(blockNode.body) ? blockNode.body : [];
|
|
231
|
+
if (statements.length === 0 || scopePairs.size === 0)
|
|
232
|
+
return;
|
|
233
|
+
const sourceCode = context.getSourceCode();
|
|
234
|
+
for (const stmt of statements) {
|
|
235
|
+
const info = getRedundantStatementContext(context, stmt, scopePairs, options);
|
|
236
|
+
if (!info) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const ranges = getRemovalRangesForAnnotationComments(info.comments, sourceCode);
|
|
240
|
+
if (ranges.length === 0) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
context.report({
|
|
244
|
+
node: stmt,
|
|
245
|
+
messageId: "redundantAnnotation",
|
|
246
|
+
fix(fixer) {
|
|
247
|
+
return ranges.map(([start, end]) => fixer.removeRange([start, end]));
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const rule = {
|
|
253
|
+
meta: {
|
|
254
|
+
type: "suggestion",
|
|
255
|
+
docs: {
|
|
256
|
+
description: "Detect and remove redundant traceability annotations already covered by containing scope",
|
|
257
|
+
recommended: false,
|
|
258
|
+
},
|
|
259
|
+
fixable: "code",
|
|
260
|
+
schema: [
|
|
261
|
+
{
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
strictness: {
|
|
265
|
+
enum: ["strict", "moderate", "permissive"],
|
|
266
|
+
},
|
|
267
|
+
allowEmphasisDuplication: {
|
|
268
|
+
type: "boolean",
|
|
269
|
+
},
|
|
270
|
+
maxScopeDepth: {
|
|
271
|
+
type: "number",
|
|
272
|
+
minimum: 1,
|
|
273
|
+
},
|
|
274
|
+
alwaysCovered: {
|
|
275
|
+
type: "array",
|
|
276
|
+
items: { type: "string" },
|
|
277
|
+
uniqueItems: true,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
messages: {
|
|
284
|
+
/**
|
|
285
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-CLEAR-MESSAGES REQ-SAFE-REMOVAL
|
|
286
|
+
*/
|
|
287
|
+
redundantAnnotation: "Annotation on this statement is redundant; it is already covered by its containing scope.",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
create(context) {
|
|
291
|
+
const options = normalizeOptions(context.options[0]);
|
|
292
|
+
return {
|
|
293
|
+
// @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL
|
|
294
|
+
BlockStatement(node) {
|
|
295
|
+
const parent = node.parent;
|
|
296
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
297
|
+
console.log("[no-redundant-annotation] BlockStatement parent=%s statements=%d", parent && parent.type, Array.isArray(node.body) ? node.body.length : 0);
|
|
298
|
+
}
|
|
299
|
+
const scopePairs = collectScopePairs(context, parent, options.maxScopeDepth);
|
|
300
|
+
debugScopePairs(parent, scopePairs);
|
|
301
|
+
if (scopePairs.size === 0)
|
|
302
|
+
return;
|
|
303
|
+
reportRedundantAnnotationsInBlock(context, node, scopePairs, options);
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
exports.default = rule;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Shared types and helpers for redundant-annotation detection.
|
|
4
|
+
*
|
|
5
|
+
* These utilities focus on parsing traceability annotations from comment
|
|
6
|
+
* text and computing relationships between "scope" coverage and
|
|
7
|
+
* statement-level annotations. They are intentionally small, pure
|
|
8
|
+
* functions so that the ESLint rule can delegate most of its logic
|
|
9
|
+
* here while keeping its own create/visitor code shallow.
|
|
10
|
+
*
|
|
11
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION
|
|
12
|
+
*/
|
|
13
|
+
export declare const EXPECTED_RANGE_LENGTH = 2;
|
|
14
|
+
export type Strictness = "strict" | "moderate" | "permissive";
|
|
15
|
+
export interface RedundancyRuleOptions {
|
|
16
|
+
strictness: Strictness;
|
|
17
|
+
allowEmphasisDuplication: boolean;
|
|
18
|
+
maxScopeDepth: number;
|
|
19
|
+
alwaysCovered: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Canonical representation of a single story+requirement pair.
|
|
23
|
+
*
|
|
24
|
+
* The key form `"<story>|<req>"` lets us compare pairs across scopes
|
|
25
|
+
* without repeatedly allocating compound objects.
|
|
26
|
+
*
|
|
27
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
|
|
28
|
+
*/
|
|
29
|
+
export type StoryReqKey = string;
|
|
30
|
+
/**
|
|
31
|
+
* Build a canonical key for a story/requirement pair.
|
|
32
|
+
*
|
|
33
|
+
* Empty story or requirement components are normalized to the empty
|
|
34
|
+
* string so that comparisons remain stable even when some annotations
|
|
35
|
+
* omit one side (for example, malformed or story-less @req lines).
|
|
36
|
+
*
|
|
37
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
|
|
38
|
+
*/
|
|
39
|
+
export declare function toStoryReqKey(storyPath: string | null, reqId: string): StoryReqKey;
|
|
40
|
+
/**
|
|
41
|
+
* Extract story/requirement pairs from a snippet of comment text.
|
|
42
|
+
*
|
|
43
|
+
* Supported patterns:
|
|
44
|
+
* - `@story <path>` followed by one or more `@req <ID>` lines.
|
|
45
|
+
* - `@supports <path> <REQ-ID-1> <REQ-ID-2> ...` where each `REQ-*`
|
|
46
|
+
* token is treated as a separate pair bound to the same story path.
|
|
47
|
+
*
|
|
48
|
+
* The parser is intentionally conservative: it only creates pairs when
|
|
49
|
+
* it can confidently associate a requirement identifier with a story
|
|
50
|
+
* path. This avoids false positives in REQ-DIFFERENT-REQUIREMENTS by
|
|
51
|
+
* ensuring we never conflate different requirement IDs.
|
|
52
|
+
*
|
|
53
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
|
|
54
|
+
*/
|
|
55
|
+
export declare function extractStoryReqPairsFromText(text: string): Set<StoryReqKey>;
|
|
56
|
+
/**
|
|
57
|
+
* Extract story/requirement pairs from a list of ESLint comment nodes.
|
|
58
|
+
*
|
|
59
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractStoryReqPairsFromComments(comments: any[]): Set<StoryReqKey>;
|
|
62
|
+
/**
|
|
63
|
+
* Determine whether all story/requirement pairs in `child` are already
|
|
64
|
+
* covered by `parent`.
|
|
65
|
+
*
|
|
66
|
+
* This implements the core notion of redundancy: if a statement-level
|
|
67
|
+
* annotation only repeats the exact same story+requirement pairs that
|
|
68
|
+
* are already declared on its containing scope, it does not add any
|
|
69
|
+
* new traceability information.
|
|
70
|
+
*
|
|
71
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS REQ-SCOPE-INHERITANCE
|
|
72
|
+
*/
|
|
73
|
+
export declare function arePairsFullyCovered(child: Set<StoryReqKey>, parent: Set<StoryReqKey>): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Decide whether a given statement node type should be considered
|
|
76
|
+
* "simple" or "significant" for redundancy detection, based on the
|
|
77
|
+
* configured strictness and alwaysCovered lists.
|
|
78
|
+
*
|
|
79
|
+
* - In `strict` mode, all non-branch statements are eligible.
|
|
80
|
+
* - In `moderate` mode (default), only statement types listed in
|
|
81
|
+
* `alwaysCovered` plus bare expression statements are treated as
|
|
82
|
+
* candidates for redundancy.
|
|
83
|
+
* - In `permissive` mode, only `alwaysCovered` types are considered.
|
|
84
|
+
*
|
|
85
|
+
* This keeps REQ-STATEMENT-SIGNIFICANCE and REQ-CONFIGURABLE-STRICTNESS
|
|
86
|
+
* aligned with the story's configuration model.
|
|
87
|
+
*
|
|
88
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-STATEMENT-SIGNIFICANCE REQ-CONFIGURABLE-STRICTNESS
|
|
89
|
+
*/
|
|
90
|
+
export declare function isStatementEligibleForRedundancy(node: any, options: RedundancyRuleOptions, branchTypes: readonly string[]): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Compute the character range that should be removed when auto-fixing a
|
|
93
|
+
* redundant annotation comment.
|
|
94
|
+
*
|
|
95
|
+
* The implementation is conservative to satisfy REQ-SAFE-REMOVAL:
|
|
96
|
+
*
|
|
97
|
+
* - When the comment occupies its own line (only whitespace before the
|
|
98
|
+
* comment token), the removal range is expanded to include that
|
|
99
|
+
* leading whitespace and the trailing newline, so the entire line is
|
|
100
|
+
* removed.
|
|
101
|
+
* - When there is other code before the comment on the same line, only
|
|
102
|
+
* the comment text itself is removed, leaving surrounding code and
|
|
103
|
+
* whitespace intact.
|
|
104
|
+
*
|
|
105
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SAFE-REMOVAL
|
|
106
|
+
*/
|
|
107
|
+
export declare function getCommentRemovalRange(comment: any, sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>): [number, number];
|
|
@@ -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
|
+
}
|
|
@@ -417,12 +417,28 @@ function reportMissingReq(context, node, options) {
|
|
|
417
417
|
* @supports REQ-ANNOTATION-PARSING
|
|
418
418
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
419
419
|
*/
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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,
|
|
424
436
|
column: 0,
|
|
425
437
|
});
|
|
438
|
+
return { indent, insertPos };
|
|
439
|
+
}
|
|
440
|
+
function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
441
|
+
let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
|
|
426
442
|
if (node.type === "CatchClause" && node.body) {
|
|
427
443
|
const bodyNode = node.body;
|
|
428
444
|
const bodyStatements = Array.isArray(bodyNode.body)
|
|
@@ -433,22 +449,16 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
|
433
449
|
: undefined;
|
|
434
450
|
if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
|
|
435
451
|
const firstLine = firstStatement.loc.start.line;
|
|
436
|
-
const
|
|
437
|
-
indent =
|
|
438
|
-
insertPos =
|
|
439
|
-
line: firstLine,
|
|
440
|
-
column: 0,
|
|
441
|
-
});
|
|
452
|
+
const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, "");
|
|
453
|
+
indent = firstLineInfo.indent;
|
|
454
|
+
insertPos = firstLineInfo.insertPos;
|
|
442
455
|
}
|
|
443
456
|
else if (bodyNode.loc && bodyNode.loc.start) {
|
|
444
457
|
const blockLine = bodyNode.loc.start.line;
|
|
445
|
-
const
|
|
446
|
-
const innerIndent = `${
|
|
458
|
+
const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, "");
|
|
459
|
+
const innerIndent = `${blockLineInfo.indent} `;
|
|
447
460
|
indent = innerIndent;
|
|
448
|
-
insertPos =
|
|
449
|
-
line: blockLine,
|
|
450
|
-
column: 0,
|
|
451
|
-
});
|
|
461
|
+
insertPos = blockLineInfo.insertPos;
|
|
452
462
|
}
|
|
453
463
|
}
|
|
454
464
|
return { indent, insertPos };
|
|
@@ -475,12 +485,9 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
|
475
485
|
// For else-if blocks, align auto-fix comments with Prettier's tendency to place comments
|
|
476
486
|
// inside the wrapped block body; non-block consequents intentionally keep the default behavior.
|
|
477
487
|
const commentLine = node.consequent.loc.start.line + 1;
|
|
478
|
-
const
|
|
479
|
-
indent =
|
|
480
|
-
insertPos =
|
|
481
|
-
line: commentLine,
|
|
482
|
-
column: 0,
|
|
483
|
-
});
|
|
488
|
+
const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
|
|
489
|
+
indent = commentLineInfo.indent;
|
|
490
|
+
insertPos = commentLineInfo.insertPos;
|
|
484
491
|
}
|
|
485
492
|
return { missingStory, missingReq, indent, insertPos };
|
|
486
493
|
}
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
* Tests for: docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md
|
|
8
|
+
* @story docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md
|
|
9
|
+
* @req REQ-SCOPE-ANALYSIS - Verify that the rule understands scope coverage for branch and block annotations
|
|
10
|
+
* @req REQ-DUPLICATION-DETECTION - Verify detection of duplicate annotations within the same scope
|
|
11
|
+
* @req REQ-STATEMENT-SIGNIFICANCE - Verify that simple statements are treated as redundant when covered by scope
|
|
12
|
+
* @req REQ-SAFE-REMOVAL - Verify that auto-fix removes only redundant annotations and preserves code
|
|
13
|
+
* @req REQ-DIFFERENT-REQUIREMENTS - Verify that annotations with different requirement IDs are preserved
|
|
14
|
+
*/
|
|
15
|
+
const eslint_1 = require("eslint");
|
|
16
|
+
const no_redundant_annotation_1 = __importDefault(require("../../src/rules/no-redundant-annotation"));
|
|
17
|
+
const ruleTester = new eslint_1.RuleTester({
|
|
18
|
+
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
19
|
+
});
|
|
20
|
+
const runRule = (tests) => ruleTester.run("no-redundant-annotation", no_redundant_annotation_1.default, tests);
|
|
21
|
+
describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DETECTION)", () => {
|
|
22
|
+
runRule({
|
|
23
|
+
valid: [
|
|
24
|
+
{
|
|
25
|
+
name: "[REQ-DIFFERENT-REQUIREMENTS] preserves child annotation with different requirement ID",
|
|
26
|
+
code: `function example() {\n // @story docs/stories/002.0-EXAMPLE.story.md\n // @req REQ-EXAMPLE-PARENT\n if (flag) {\n // @story docs/stories/002.0-EXAMPLE.story.md\n // @req REQ-EXAMPLE-CHILD\n doWork();\n }\n}`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "[REQ-STATEMENT-SIGNIFICANCE] preserves annotation on complex nested branch",
|
|
30
|
+
code: `function example() {\n // @story docs/stories/006.0-EXAMPLE.story.md\n // @req REQ-OUTER-CHECK\n if (enabled) {\n // @story docs/stories/006.0-EXAMPLE.story.md\n // @req REQ-INNER-VALIDATION\n if (validate) {\n validate(data);\n }\n }\n}`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
invalid: [
|
|
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
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const annotation_scope_analyzer_1 = require("../../src/utils/annotation-scope-analyzer");
|
|
4
|
+
describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATION-DETECTION)", () => {
|
|
5
|
+
it("[REQ-DUPLICATION-DETECTION] builds stable story/req keys", () => {
|
|
6
|
+
const key = (0, annotation_scope_analyzer_1.toStoryReqKey)("docs/stories/001.story.md", "REQ-ONE");
|
|
7
|
+
expect(key).toBe("docs/stories/001.story.md|REQ-ONE");
|
|
8
|
+
});
|
|
9
|
+
it("[REQ-DUPLICATION-DETECTION] extracts pairs from @story/@req sequences", () => {
|
|
10
|
+
const text = `// @story docs/stories/001.story.md\n// @req REQ-ONE`;
|
|
11
|
+
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
|
|
12
|
+
expect(Array.from(pairs)).toEqual([
|
|
13
|
+
"docs/stories/001.story.md|REQ-ONE",
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
it("[REQ-SCOPE-ANALYSIS] extracts pairs from @supports lines", () => {
|
|
17
|
+
const text = `// @supports docs/stories/002.story.md REQ-A REQ-B OTHER`;
|
|
18
|
+
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
|
|
19
|
+
expect(pairs.has("docs/stories/002.story.md|REQ-A")).toBe(true);
|
|
20
|
+
expect(pairs.has("docs/stories/002.story.md|REQ-B")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it("[REQ-DUPLICATION-DETECTION] aggregates pairs across comments", () => {
|
|
23
|
+
const comments = [
|
|
24
|
+
{ value: "// @story docs/stories/001.story.md\n// @req REQ-ONE" },
|
|
25
|
+
{ value: "// @supports docs/stories/002.story.md REQ-TWO" },
|
|
26
|
+
];
|
|
27
|
+
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
|
|
28
|
+
expect(pairs.size).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
it("[REQ-DUPLICATION-DETECTION] determines full coverage correctly", () => {
|
|
31
|
+
const parent = new Set([
|
|
32
|
+
"story|REQ-ONE",
|
|
33
|
+
"story|REQ-TWO",
|
|
34
|
+
]);
|
|
35
|
+
const childCovered = new Set(["story|REQ-ONE"]);
|
|
36
|
+
const childNotCovered = new Set(["story|REQ-THREE"]);
|
|
37
|
+
expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childCovered, parent)).toBe(true);
|
|
38
|
+
expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childNotCovered, parent)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it("[REQ-STATEMENT-SIGNIFICANCE] respects alwaysCovered and strictness levels", () => {
|
|
41
|
+
const base = {
|
|
42
|
+
strictness: "moderate",
|
|
43
|
+
allowEmphasisDuplication: false,
|
|
44
|
+
maxScopeDepth: 3,
|
|
45
|
+
alwaysCovered: ["ReturnStatement"],
|
|
46
|
+
};
|
|
47
|
+
const branchTypes = ["IfStatement"];
|
|
48
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ReturnStatement" }, base, branchTypes)).toBe(true);
|
|
49
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, base, branchTypes)).toBe(true);
|
|
50
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "IfStatement" }, base, branchTypes)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment", () => {
|
|
53
|
+
const source = `const x = 1;\n// @story docs/stories/001.story.md\nconst y = 2;\n`;
|
|
54
|
+
const sourceCode = {
|
|
55
|
+
getText() {
|
|
56
|
+
return source;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const start = source.indexOf("// @story");
|
|
60
|
+
const end = start + "// @story docs/stories/001.story.md".length;
|
|
61
|
+
const comment = { range: [start, end] };
|
|
62
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
63
|
+
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
64
|
+
expect(removed).toBe("const x = 1;\nconst y = 2;\n");
|
|
65
|
+
});
|
|
66
|
+
it("[REQ-SAFE-REMOVAL] returns [0, 0] for comments with invalid range length (EXPECTS EXPECTED_RANGE_LENGTH usage)", () => {
|
|
67
|
+
const source = "const x = 1;";
|
|
68
|
+
const sourceCode = {
|
|
69
|
+
getText() {
|
|
70
|
+
return source;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const comment = { range: [0] };
|
|
74
|
+
const range = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
75
|
+
expect(range).toEqual([0, 0]);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
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",
|
|
@@ -268,6 +268,42 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
|
|
|
268
268
|
});
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
+
### traceability/no-redundant-annotation
|
|
272
|
+
|
|
273
|
+
Description: Detects and optionally removes **redundant** traceability annotations on code that is already covered by an enclosing annotated scope. It focuses on simple, statement-level constructs—such as `return` statements, basic variable declarations, and other leaf statements—where repeating the same `@story` / `@req` / `@supports` information adds noise without improving coverage. When run with `--fix`, the rule offers safe auto-fixes that remove only the redundant comments while preserving all annotations that are required to maintain correct traceability.
|
|
274
|
+
|
|
275
|
+
The rule is designed to complement the core presence and validation rules: it never treats removing a redundant annotation as valid if doing so would leave the underlying requirement or story **uncovered** according to the plugin’s normal rules. It only targets comments whose traceability content is already implied by a surrounding function, method, or branch annotation.
|
|
276
|
+
|
|
277
|
+
Options:
|
|
278
|
+
|
|
279
|
+
The rule accepts an optional configuration object:
|
|
280
|
+
|
|
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
|
+
|
|
289
|
+
Behavior notes:
|
|
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. 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
|
+
- 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
|
+
- When no enclosing scope with compatible coverage is found within `maxScopeDepth`, the annotation is not considered redundant and is left unchanged.
|
|
294
|
+
|
|
295
|
+
Default Severity: `warn`
|
|
296
|
+
|
|
297
|
+
This rule is **not** enabled in the `recommended` or `strict` presets by default. To use it, add it explicitly to your ESLint configuration with an appropriate severity level:
|
|
298
|
+
|
|
299
|
+
```jsonc
|
|
300
|
+
{
|
|
301
|
+
"rules": {
|
|
302
|
+
"traceability/no-redundant-annotation": "warn",
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
271
307
|
### traceability/prefer-supports-annotation
|
|
272
308
|
|
|
273
309
|
Description: An optional, opt-in migration helper that encourages converting legacy single‑story `@story` + `@req` JSDoc blocks into the newer multi‑story `@supports` format. The rule is **disabled by default** and is **not included in any built‑in preset**; you enable it explicitly and control its behavior entirely via ESLint severity (`"off" | "warn" | "error"`). It does not change what the core rules consider valid—it only adds migration recommendations and safe auto‑fixes on top of existing validation. The legacy rule key `traceability/prefer-implements-annotation` is still recognized as a **deprecated alias** for `traceability/prefer-supports-annotation` so that existing configurations continue to work unchanged.
|