eslint-plugin-traceability 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/lib/src/index.js +2 -0
- package/lib/src/rules/helpers/require-story-core.js +61 -52
- package/lib/src/rules/no-redundant-annotation.d.ts +3 -0
- package/lib/src/rules/no-redundant-annotation.js +235 -0
- package/lib/src/utils/annotation-scope-analyzer.d.ts +107 -0
- package/lib/src/utils/annotation-scope-analyzer.js +233 -0
- package/lib/src/utils/branch-annotation-helpers.js +100 -51
- package/lib/tests/integration/dogfooding-validation.test.js +33 -1
- package/lib/tests/integration/else-if-annotation-prettier.integration.test.js +18 -79
- package/lib/tests/plugin-default-export-and-configs.test.js +1 -0
- package/lib/tests/rules/auto-fix-behavior-008.test.js +87 -1
- package/lib/tests/rules/no-redundant-annotation.test.d.ts +1 -0
- package/lib/tests/rules/no-redundant-annotation.test.js +63 -0
- package/lib/tests/rules/require-story-core.autofix.test.js +26 -0
- package/lib/tests/utils/annotation-scope-analyzer.test.d.ts +1 -0
- package/lib/tests/utils/annotation-scope-analyzer.test.js +77 -0
- package/lib/tests/utils/branch-annotation-else-if-position.test.js +38 -0
- package/lib/tests/utils/req-annotation-detection.test.js +46 -0
- package/package.json +1 -1
- package/user-docs/api-reference.md +37 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# [1.
|
|
1
|
+
# [1.13.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.12.1...v1.13.0) (2025-12-07)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* add no-redundant-annotation rule and scope analyzer utilities ([5dfc6a8](https://github.com/voder-ai/eslint-plugin-traceability/commit/5dfc6a897a986ba5999c933e67a3834d9f237c33))
|
|
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
|
|
@@ -98,6 +98,46 @@ exports.STORY_PATH = "docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md";
|
|
|
98
98
|
* Allowed values for export priority option.
|
|
99
99
|
*/
|
|
100
100
|
exports.EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
|
|
101
|
+
/**
|
|
102
|
+
* Safely execute a reporting operation, swallowing unexpected errors so that
|
|
103
|
+
* traceability rules never break ESLint runs. When TRACEABILITY_DEBUG=1 is
|
|
104
|
+
* set in the environment, a diagnostic message is logged to stderr.
|
|
105
|
+
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-RESILIENCE
|
|
106
|
+
*/
|
|
107
|
+
function withSafeReporting(label, fn) {
|
|
108
|
+
try {
|
|
109
|
+
fn();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
113
|
+
// Debug logging only when explicitly enabled for troubleshooting helper failures.
|
|
114
|
+
console.error(`[traceability] ${label} failed`, error?.message ?? error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the shared ESLint report descriptor for a missing @story annotation.
|
|
120
|
+
* This keeps the core helpers focused on computing names, targets, and
|
|
121
|
+
* templates while centralizing the diagnostic wiring.
|
|
122
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ERROR-SPECIFIC
|
|
123
|
+
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-RESILIENCE
|
|
124
|
+
*/
|
|
125
|
+
function createMissingStoryReportDescriptor(config) {
|
|
126
|
+
const { nameNode, name, resolvedTarget, effectiveTemplate, allowFix, createFix, } = config;
|
|
127
|
+
const baseFix = createFix(resolvedTarget, effectiveTemplate);
|
|
128
|
+
return {
|
|
129
|
+
node: nameNode,
|
|
130
|
+
messageId: "missingStory",
|
|
131
|
+
data: { name, functionName: name },
|
|
132
|
+
fix: allowFix ? baseFix : undefined,
|
|
133
|
+
suggest: [
|
|
134
|
+
{
|
|
135
|
+
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
|
|
136
|
+
fix: baseFix,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
101
141
|
/**
|
|
102
142
|
* Core helper to report a missing @story annotation for a function-like node.
|
|
103
143
|
* This reporting utility delegates behavior to injected dependencies so that
|
|
@@ -111,7 +151,7 @@ exports.EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
|
|
|
111
151
|
*/
|
|
112
152
|
function coreReportMissing(deps, context, sourceCode, config) {
|
|
113
153
|
const { node, target: passedTarget, options = {} } = config;
|
|
114
|
-
|
|
154
|
+
withSafeReporting("coreReportMissing", () => {
|
|
115
155
|
if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
116
156
|
return;
|
|
117
157
|
}
|
|
@@ -120,30 +160,15 @@ function coreReportMissing(deps, context, sourceCode, config) {
|
|
|
120
160
|
const nameNode = deps.getNameNodeForReport(node);
|
|
121
161
|
const { effectiveTemplate, allowFix } = deps.buildTemplateConfig(options);
|
|
122
162
|
const name = functionName;
|
|
123
|
-
context.report({
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
|
|
133
|
-
fix: deps.createAddStoryFix(resolvedTarget, effectiveTemplate),
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
// Intentionally swallow unexpected helper errors so traceability checks never
|
|
140
|
-
// break lint runs. When TRACEABILITY_DEBUG=1 is set, log a debug message to
|
|
141
|
-
// help diagnose misbehaving helpers in local development without affecting
|
|
142
|
-
// normal CI or production usage.
|
|
143
|
-
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
144
|
-
console.error("[traceability] coreReportMissing failed for node", error?.message ?? error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
163
|
+
context.report(createMissingStoryReportDescriptor({
|
|
164
|
+
nameNode,
|
|
165
|
+
name,
|
|
166
|
+
resolvedTarget,
|
|
167
|
+
effectiveTemplate,
|
|
168
|
+
allowFix,
|
|
169
|
+
createFix: deps.createAddStoryFix,
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
147
172
|
}
|
|
148
173
|
/**
|
|
149
174
|
* Core helper to report a missing @story annotation for a method-like node.
|
|
@@ -158,37 +183,21 @@ function coreReportMissing(deps, context, sourceCode, config) {
|
|
|
158
183
|
*/
|
|
159
184
|
function coreReportMethod(deps, context, sourceCode, config) {
|
|
160
185
|
const { node, target: passedTarget, options = {} } = config;
|
|
161
|
-
|
|
186
|
+
withSafeReporting("coreReportMethod", () => {
|
|
162
187
|
if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
163
188
|
return;
|
|
164
189
|
}
|
|
165
190
|
const resolvedTarget = passedTarget ?? deps.resolveAnnotationTargetNode(sourceCode, node, null);
|
|
166
191
|
const name = deps.extractName(node);
|
|
167
192
|
const nameNode = (node.key && node.key.type === "Identifier" && node.key) || node;
|
|
168
|
-
const effectiveTemplate = deps.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{
|
|
179
|
-
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
|
|
180
|
-
fix: deps.createMethodFix(resolvedTarget, effectiveTemplate),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
// Intentionally swallow unexpected helper errors so traceability checks never
|
|
187
|
-
// break lint runs. When TRACEABILITY_DEBUG=1 is set, log a debug message to
|
|
188
|
-
// help diagnose misbehaving helpers in local development without affecting
|
|
189
|
-
// normal CI or production usage.
|
|
190
|
-
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
191
|
-
console.error("[traceability] coreReportMethod failed for node", error?.message ?? error);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
193
|
+
const { effectiveTemplate, allowFix } = deps.buildTemplateConfig(options);
|
|
194
|
+
context.report(createMissingStoryReportDescriptor({
|
|
195
|
+
nameNode,
|
|
196
|
+
name,
|
|
197
|
+
resolvedTarget,
|
|
198
|
+
effectiveTemplate,
|
|
199
|
+
allowFix,
|
|
200
|
+
createFix: deps.createMethodFix,
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
194
203
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
* Analyze a block's statements and report redundant traceability annotations.
|
|
132
|
+
*
|
|
133
|
+
* This helper encapsulates the iteration and reporting logic so that the
|
|
134
|
+
* BlockStatement visitor remains small and focused on scope setup.
|
|
135
|
+
*
|
|
136
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-STATEMENT-SIGNIFICANCE
|
|
137
|
+
*/
|
|
138
|
+
function reportRedundantAnnotationsInBlock(context, blockNode, scopePairs, options) {
|
|
139
|
+
const statements = Array.isArray(blockNode.body) ? blockNode.body : [];
|
|
140
|
+
if (statements.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
for (const stmt of statements) {
|
|
143
|
+
if (!(0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)(stmt, options, branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const stmtComments = getStatementComments(context, stmt);
|
|
147
|
+
if (stmtComments.length === 0) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const stmtPairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(stmtComments);
|
|
151
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
152
|
+
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));
|
|
153
|
+
}
|
|
154
|
+
if (stmtPairs.size === 0) {
|
|
155
|
+
continue;
|
|
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
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const rule = {
|
|
179
|
+
meta: {
|
|
180
|
+
type: "suggestion",
|
|
181
|
+
docs: {
|
|
182
|
+
description: "Detect and remove redundant traceability annotations already covered by containing scope",
|
|
183
|
+
recommended: false,
|
|
184
|
+
},
|
|
185
|
+
fixable: "code",
|
|
186
|
+
schema: [
|
|
187
|
+
{
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
strictness: {
|
|
191
|
+
enum: ["strict", "moderate", "permissive"],
|
|
192
|
+
},
|
|
193
|
+
allowEmphasisDuplication: {
|
|
194
|
+
type: "boolean",
|
|
195
|
+
},
|
|
196
|
+
maxScopeDepth: {
|
|
197
|
+
type: "number",
|
|
198
|
+
minimum: 1,
|
|
199
|
+
},
|
|
200
|
+
alwaysCovered: {
|
|
201
|
+
type: "array",
|
|
202
|
+
items: { type: "string" },
|
|
203
|
+
uniqueItems: true,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
messages: {
|
|
210
|
+
/**
|
|
211
|
+
* @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-CLEAR-MESSAGES REQ-SAFE-REMOVAL
|
|
212
|
+
*/
|
|
213
|
+
redundantAnnotation: "Annotation on this statement is redundant; it is already covered by its containing scope.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
create(context) {
|
|
217
|
+
const options = normalizeOptions(context.options[0]);
|
|
218
|
+
return {
|
|
219
|
+
// @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL
|
|
220
|
+
BlockStatement(node) {
|
|
221
|
+
const parent = node.parent;
|
|
222
|
+
const scopeNode = parent;
|
|
223
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
224
|
+
console.log("[no-redundant-annotation] BlockStatement parent=%s statements=%d", parent && parent.type, Array.isArray(node.body) ? node.body.length : 0);
|
|
225
|
+
}
|
|
226
|
+
const scopePairs = getScopePairs(context, scopeNode, scopeNode?.parent);
|
|
227
|
+
debugScopePairs(scopeNode, scopePairs);
|
|
228
|
+
if (scopePairs.size === 0)
|
|
229
|
+
return;
|
|
230
|
+
reportRedundantAnnotationsInBlock(context, node, scopePairs, options);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
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];
|