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.
Files changed (48) hide show
  1. package/README.md +39 -1
  2. package/lib/src/index.d.ts +30 -27
  3. package/lib/src/index.js +51 -31
  4. package/lib/src/maintenance/cli.d.ts +12 -0
  5. package/lib/src/maintenance/cli.js +279 -0
  6. package/lib/src/maintenance/detect.js +27 -12
  7. package/lib/src/maintenance/update.js +42 -34
  8. package/lib/src/maintenance/utils.js +30 -30
  9. package/lib/src/rules/helpers/require-story-io.js +51 -15
  10. package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
  11. package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
  12. package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
  13. package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
  14. package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
  15. package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
  16. package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
  17. package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
  18. package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
  19. package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
  20. package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
  21. package/lib/src/rules/prefer-implements-annotation.js +276 -0
  22. package/lib/src/rules/valid-annotation-format.js +255 -208
  23. package/lib/src/rules/valid-req-reference.js +210 -29
  24. package/lib/src/rules/valid-story-reference.d.ts +7 -0
  25. package/lib/src/rules/valid-story-reference.js +38 -80
  26. package/lib/src/utils/annotation-checker.js +2 -145
  27. package/lib/src/utils/branch-annotation-helpers.js +12 -3
  28. package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
  29. package/lib/src/utils/reqAnnotationDetection.js +152 -0
  30. package/lib/tests/maintenance/cli.test.d.ts +1 -0
  31. package/lib/tests/maintenance/cli.test.js +172 -0
  32. package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
  33. package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
  34. package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
  35. package/lib/tests/rules/require-branch-annotation.test.js +3 -2
  36. package/lib/tests/rules/require-req-annotation.test.js +57 -68
  37. package/lib/tests/rules/require-story-annotation.test.js +13 -28
  38. package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
  39. package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
  40. package/lib/tests/rules/valid-annotation-format.test.js +395 -40
  41. package/lib/tests/rules/valid-req-reference.test.js +34 -0
  42. package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
  43. package/lib/tests/utils/annotation-checker.test.js +24 -17
  44. package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
  45. package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
  46. package/lib/tests/utils/ts-language-options.d.ts +22 -0
  47. package/lib/tests/utils/ts-language-options.js +27 -0
  48. 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;