eslint-plugin-traceability 1.6.5 → 1.7.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/README.md +39 -1
- package/lib/src/index.d.ts +30 -27
- package/lib/src/index.js +51 -31
- package/lib/src/maintenance/cli.d.ts +12 -0
- package/lib/src/maintenance/cli.js +279 -0
- package/lib/src/maintenance/detect.js +27 -12
- package/lib/src/maintenance/update.js +42 -34
- package/lib/src/maintenance/utils.js +30 -30
- package/lib/src/rules/helpers/require-story-io.js +51 -15
- package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
- package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
- package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
- package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
- package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
- package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
- package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
- package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
- package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
- package/lib/src/rules/prefer-implements-annotation.js +276 -0
- package/lib/src/rules/valid-annotation-format.js +255 -208
- package/lib/src/rules/valid-req-reference.js +210 -29
- package/lib/src/rules/valid-story-reference.d.ts +7 -0
- package/lib/src/rules/valid-story-reference.js +38 -80
- package/lib/src/utils/annotation-checker.js +2 -145
- package/lib/src/utils/branch-annotation-helpers.js +12 -3
- package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
- package/lib/src/utils/reqAnnotationDetection.js +152 -0
- package/lib/tests/maintenance/cli.test.d.ts +1 -0
- package/lib/tests/maintenance/cli.test.js +172 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
- package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
- package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
- package/lib/tests/rules/require-branch-annotation.test.js +3 -2
- package/lib/tests/rules/require-req-annotation.test.js +57 -68
- package/lib/tests/rules/require-story-annotation.test.js +13 -28
- package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
- package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
- package/lib/tests/rules/valid-annotation-format.test.js +395 -40
- package/lib/tests/rules/valid-req-reference.test.js +34 -0
- package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
- package/lib/tests/utils/annotation-checker.test.js +24 -17
- package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
- package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
- package/lib/tests/utils/ts-language-options.d.ts +22 -0
- package/lib/tests/utils/ts-language-options.js +27 -0
- package/package.json +12 -3
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
|
|
4
|
+
// Maximum number of distinct @story paths allowed before treating as "multi-story".
|
|
5
|
+
// @req REQ-MULTI-STORY-DETECT - Centralized threshold constant for detecting multi-story patterns
|
|
6
|
+
const MULTI_STORY_THRESHOLD = 1;
|
|
7
|
+
// Minimum number of tokens required for a valid @story annotation line.
|
|
8
|
+
// @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
9
|
+
// @req REQ-MULTI-STORY-DETECT
|
|
10
|
+
const MIN_STORY_TOKENS = 2;
|
|
11
|
+
// Minimum number of tokens required for a valid @req annotation line, aligned with story tokens.
|
|
12
|
+
const MIN_REQ_TOKENS = MIN_STORY_TOKENS;
|
|
13
|
+
// Length of the opening "/*" portion of a block comment prefix.
|
|
14
|
+
const COMMENT_PREFIX_LENGTH = 2;
|
|
15
|
+
function collectStoryAndReqMetadata(comment) {
|
|
16
|
+
const rawValue = comment.value || "";
|
|
17
|
+
const rawLines = rawValue.split(/\r?\n/);
|
|
18
|
+
const storyLineIndices = [];
|
|
19
|
+
const reqLineIndices = [];
|
|
20
|
+
const reqIds = [];
|
|
21
|
+
let storyPath = null;
|
|
22
|
+
rawLines.forEach((rawLine, index) => {
|
|
23
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(rawLine);
|
|
24
|
+
if (!normalized)
|
|
25
|
+
return;
|
|
26
|
+
if (/^@implements\b/.test(normalized)) {
|
|
27
|
+
// Mixed @implements usage should have been filtered out earlier
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (/^@story\b/.test(normalized)) {
|
|
31
|
+
const parts = normalized.split(/\s+/);
|
|
32
|
+
if (parts.length === MIN_STORY_TOKENS) {
|
|
33
|
+
storyLineIndices.push(index);
|
|
34
|
+
storyPath = parts[1];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
storyPath = null;
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (/^@req\b/.test(normalized)) {
|
|
42
|
+
const parts = normalized.split(/\s+/);
|
|
43
|
+
if (parts.length === MIN_REQ_TOKENS) {
|
|
44
|
+
reqLineIndices.push(index);
|
|
45
|
+
reqIds.push(parts[1]);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Complex @req form; bail out entirely.
|
|
49
|
+
storyPath = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return { storyLineIndices, reqLineIndices, reqIds, storyPath };
|
|
54
|
+
}
|
|
55
|
+
function applyImplementsReplacement(context, comment, details) {
|
|
56
|
+
const { storyIdx, allIndicesToRemove, storyPath, reqIds } = details;
|
|
57
|
+
const rawValue = comment.value || "";
|
|
58
|
+
const rawLines = rawValue.split(/\r?\n/);
|
|
59
|
+
const implAnnotation = `@implements ${storyPath} ${reqIds.join(" ")}`;
|
|
60
|
+
// Determine the leading prefix (indentation and `*`) from the original @story line
|
|
61
|
+
const storyRawLine = rawLines[storyIdx];
|
|
62
|
+
const prefixMatch = storyRawLine.match(/^(\s*\*?\s*)/);
|
|
63
|
+
const linePrefix = prefixMatch ? prefixMatch[1] : "";
|
|
64
|
+
const implementsLine = `${linePrefix}${implAnnotation}`;
|
|
65
|
+
const fixedLines = [];
|
|
66
|
+
rawLines.forEach((line, index) => {
|
|
67
|
+
if (index === storyIdx) {
|
|
68
|
+
fixedLines.push(implementsLine);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (allIndicesToRemove.has(index)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
fixedLines.push(line);
|
|
75
|
+
});
|
|
76
|
+
const fixedValue = fixedLines.join("\n");
|
|
77
|
+
const sourceCode = context.getSourceCode();
|
|
78
|
+
return (fixer) => fixer.replaceTextRange([comment.range[0], comment.range[1]], sourceCode.text.slice(comment.range[0], comment.range[0] + COMMENT_PREFIX_LENGTH) +
|
|
79
|
+
fixedValue +
|
|
80
|
+
"*/");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build an ESLint auto-fix for simple single-story `@story` + `@req` JSDoc
|
|
84
|
+
* blocks, converting them to a single `@implements` annotation while
|
|
85
|
+
* preserving the original comment formatting.
|
|
86
|
+
*
|
|
87
|
+
* The fixer is intentionally conservative and only activates when:
|
|
88
|
+
* - There is exactly one distinct `@story` path.
|
|
89
|
+
* - Exactly one `@story` line is present.
|
|
90
|
+
* - At least one `@req` line is present.
|
|
91
|
+
* - Each `@req` line has the simple form `@req <REQ-ID>` (no extra tokens).
|
|
92
|
+
*
|
|
93
|
+
* When applicable, the fix:
|
|
94
|
+
* - Removes the original `@story` and `@req` lines.
|
|
95
|
+
* - Inserts a single `@implements` line in their place, preserving the
|
|
96
|
+
* original leading comment prefix (indentation and `*` markers).
|
|
97
|
+
*
|
|
98
|
+
* More complex patterns remain diagnostics-only with no fix to avoid
|
|
99
|
+
* producing invalid or ambiguous output.
|
|
100
|
+
*
|
|
101
|
+
* @implements docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
102
|
+
* @req REQ-AUTO-FIX - Provide safe, opt-in auto-fix for simple legacy patterns
|
|
103
|
+
* @req REQ-SINGLE-STORY-FIX - Restrict auto-fix to single-story, single-path cases
|
|
104
|
+
* @req REQ-PRESERVE-FORMAT - Preserve original JSDoc indentation and prefix formatting
|
|
105
|
+
* @req REQ-VALID-OUTPUT - Avoid emitting auto-fixes for complex or ambiguous patterns
|
|
106
|
+
*/
|
|
107
|
+
function buildImplementsAutoFix(context, comment, storyPaths) {
|
|
108
|
+
if (storyPaths.size !== 1)
|
|
109
|
+
return null;
|
|
110
|
+
const { storyLineIndices, reqLineIndices, reqIds, storyPath } = collectStoryAndReqMetadata(comment);
|
|
111
|
+
if (storyPaths.size !== 1 ||
|
|
112
|
+
storyLineIndices.length !== 1 ||
|
|
113
|
+
reqLineIndices.length < 1 ||
|
|
114
|
+
storyPath === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const storyIdx = storyLineIndices[0];
|
|
118
|
+
const allIndicesToRemove = new Set([
|
|
119
|
+
...storyLineIndices,
|
|
120
|
+
...reqLineIndices,
|
|
121
|
+
]);
|
|
122
|
+
return applyImplementsReplacement(context, comment, {
|
|
123
|
+
storyIdx,
|
|
124
|
+
allIndicesToRemove,
|
|
125
|
+
storyPath,
|
|
126
|
+
reqIds,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function analyzeComment(comment) {
|
|
130
|
+
const rawLines = (comment.value || "").split(/\r?\n/);
|
|
131
|
+
let hasStory = false;
|
|
132
|
+
let hasReq = false;
|
|
133
|
+
let hasImplements = false;
|
|
134
|
+
const storyPaths = new Set();
|
|
135
|
+
rawLines.forEach((rawLine) => {
|
|
136
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(rawLine);
|
|
137
|
+
if (!normalized)
|
|
138
|
+
return;
|
|
139
|
+
if (/^@implements\b/.test(normalized)) {
|
|
140
|
+
hasImplements = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (/^@story\b/.test(normalized)) {
|
|
144
|
+
hasStory = true;
|
|
145
|
+
const parts = normalized.split(/\s+/);
|
|
146
|
+
if (parts.length >= MIN_STORY_TOKENS) {
|
|
147
|
+
storyPaths.add(parts[1]);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (/^@req\b/.test(normalized)) {
|
|
152
|
+
hasReq = true;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return { hasStory, hasReq, hasImplements, storyPaths };
|
|
156
|
+
}
|
|
157
|
+
function hasMultipleStories(storyPaths) {
|
|
158
|
+
// @req REQ-MULTI-STORY-DETECT - Use named threshold constant instead of a magic number
|
|
159
|
+
return storyPaths.size > MULTI_STORY_THRESHOLD;
|
|
160
|
+
}
|
|
161
|
+
function processComment(comment, context) {
|
|
162
|
+
const { hasStory, hasReq, hasImplements, storyPaths } = analyzeComment(comment);
|
|
163
|
+
if (!hasStory || !hasReq) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (hasImplements) {
|
|
167
|
+
context.report({
|
|
168
|
+
node: comment,
|
|
169
|
+
messageId: "cannotAutoFix",
|
|
170
|
+
data: {
|
|
171
|
+
reason: "comment mixes @story/@req with existing @implements annotations",
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (hasMultipleStories(storyPaths)) {
|
|
177
|
+
context.report({
|
|
178
|
+
node: comment,
|
|
179
|
+
messageId: "multiStoryDetected",
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const fix = buildImplementsAutoFix(context, comment, storyPaths);
|
|
184
|
+
context.report({
|
|
185
|
+
node: comment,
|
|
186
|
+
messageId: "preferImplements",
|
|
187
|
+
fix: fix ?? undefined,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* ESLint rule: prefer-implements-annotation
|
|
192
|
+
*
|
|
193
|
+
* Recommend migrating from legacy `@story` + `@req` annotations to the
|
|
194
|
+
* newer `@implements` format. This rule is **disabled by default** and
|
|
195
|
+
* is intended as an optional, opt-in migration aid.
|
|
196
|
+
*
|
|
197
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
198
|
+
* @req REQ-OPTIONAL-WARNING - Emit configurable recommendation diagnostics for legacy @story/@req usage
|
|
199
|
+
* @req REQ-MULTI-STORY-DETECT - Detect multi-story patterns that cannot be auto-fixed
|
|
200
|
+
* @req REQ-BACKWARD-COMP-VALIDATION - Keep legacy @story/@req annotations valid when the rule is disabled
|
|
201
|
+
*/
|
|
202
|
+
const preferImplementsAnnotationRule = {
|
|
203
|
+
meta: {
|
|
204
|
+
type: "suggestion",
|
|
205
|
+
docs: {
|
|
206
|
+
description: "Recommend using @implements instead of legacy @story + @req annotations (optional migration rule)",
|
|
207
|
+
recommended: false,
|
|
208
|
+
},
|
|
209
|
+
// Auto-fix support will be wired in a later iteration; the rule starts as
|
|
210
|
+
// a recommendation-only warning with no code modifications.
|
|
211
|
+
fixable: "code",
|
|
212
|
+
messages: {
|
|
213
|
+
/**
|
|
214
|
+
* Recommend migrating simple, single-story @story + @req blocks to a
|
|
215
|
+
* single @implements line. Auto-fix is provided where safe in a
|
|
216
|
+
* follow-up iteration.
|
|
217
|
+
*
|
|
218
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
219
|
+
* @req REQ-OPTIONAL-WARNING
|
|
220
|
+
*/
|
|
221
|
+
preferImplements: "Consider using @implements instead of @story + @req for clearer traceability. Run ESLint with --fix to auto-convert.",
|
|
222
|
+
/**
|
|
223
|
+
* Report situations where the rule detects a legacy annotation pattern
|
|
224
|
+
* but cannot safely provide an automatic fix. The `reason` field gives
|
|
225
|
+
* a short, human-readable explanation to guide manual migration.
|
|
226
|
+
*
|
|
227
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
228
|
+
* @req REQ-MULTI-STORY-DETECT
|
|
229
|
+
*/
|
|
230
|
+
cannotAutoFix: "Cannot auto-fix: {{reason}}. Manual migration to @implements required.",
|
|
231
|
+
/**
|
|
232
|
+
* Specialized message for the most common non-fixable case where more
|
|
233
|
+
* than one @story annotation appears in the same block, indicating a
|
|
234
|
+
* likely multi-story integration that must be converted manually.
|
|
235
|
+
*
|
|
236
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
237
|
+
* @req REQ-MULTI-STORY-DETECT
|
|
238
|
+
*/
|
|
239
|
+
multiStoryDetected: "Multiple @story annotations detected in the same comment block. Manually convert to separate @implements lines.",
|
|
240
|
+
},
|
|
241
|
+
schema: [],
|
|
242
|
+
},
|
|
243
|
+
/**
|
|
244
|
+
* Rule entrypoint.
|
|
245
|
+
*
|
|
246
|
+
* This initial implementation focuses on **detection and messaging only**:
|
|
247
|
+
* it surfaces recommendations when legacy `@story` + `@req` combinations are
|
|
248
|
+
* present but does not yet perform automatic code modifications.
|
|
249
|
+
*
|
|
250
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
251
|
+
* @req REQ-OPTIONAL-WARNING
|
|
252
|
+
* @req REQ-MULTI-STORY-DETECT
|
|
253
|
+
*/
|
|
254
|
+
create(context) {
|
|
255
|
+
const sourceCode = context.getSourceCode();
|
|
256
|
+
return {
|
|
257
|
+
/**
|
|
258
|
+
* Program-level visitor that scans all comments for legacy
|
|
259
|
+
* `@story` + `@req` usage and emits recommendation diagnostics.
|
|
260
|
+
*
|
|
261
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
|
|
262
|
+
* @req REQ-OPTIONAL-WARNING - Emit recommendations when legacy annotations are detected
|
|
263
|
+
* @req REQ-MULTI-STORY-DETECT - Detect multi-story and mixed annotation patterns
|
|
264
|
+
*/
|
|
265
|
+
Program() {
|
|
266
|
+
const comments = sourceCode.getAllComments() || [];
|
|
267
|
+
comments
|
|
268
|
+
.filter((comment) => comment.type === "Block")
|
|
269
|
+
.forEach((comment) => {
|
|
270
|
+
processComment(comment, context);
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
exports.default = preferImplementsAnnotationRule;
|