eslint-plugin-traceability 1.11.2 → 1.11.3
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/README.md +2 -2
- package/lib/src/index.d.ts +1 -7
- package/lib/src/index.js +28 -0
- package/lib/src/utils/branch-annotation-helpers.js +92 -13
- package/lib/tests/integration/catch-annotation-prettier.integration.test.d.ts +1 -0
- package/lib/tests/integration/catch-annotation-prettier.integration.test.js +131 -0
- package/lib/tests/perf/valid-annotation-format-large-file.test.d.ts +1 -0
- package/lib/tests/perf/valid-annotation-format-large-file.test.js +74 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +1 -0
- package/lib/tests/rules/prefer-implements-annotation.test.js +84 -70
- package/lib/tests/utils/branch-annotation-catch-insert-position.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-catch-insert-position.test.js +68 -0
- package/lib/tests/utils/branch-annotation-catch-position.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-catch-position.test.js +115 -0
- package/lib/tests/utils/req-annotation-detection.test.d.ts +1 -0
- package/lib/tests/utils/req-annotation-detection.test.js +247 -0
- package/package.json +3 -3
- package/user-docs/api-reference.md +16 -8
- package/user-docs/migration-guide.md +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
## [1.11.
|
|
1
|
+
## [1.11.3](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.11.2...v1.11.3) (2025-12-06)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* ensure catch clause annotations remain valid after prettier formatting ([ca38772](https://github.com/voder-ai/eslint-plugin-traceability/commit/ca3877248d344d7b94ed0059eca9b80b14a04772))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Created autonomously by [voder.ai](https://voder.ai).
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
Prerequisites: Node.js
|
|
11
|
+
Prerequisites: Node.js 18.18.x, 20.x, 22.14.x, or 24.x and ESLint v9+.
|
|
12
12
|
|
|
13
13
|
1. Using npm
|
|
14
14
|
npm install --save-dev eslint-plugin-traceability
|
|
@@ -55,7 +55,7 @@ export default [
|
|
|
55
55
|
- `traceability/valid-story-reference` Validates that `@story` references point to existing story files. (See the rule documentation in the plugin's user guide.)
|
|
56
56
|
- `traceability/valid-req-reference` Validates that `@req` references point to existing requirement IDs. (See the rule documentation in the plugin's user guide.)
|
|
57
57
|
- `traceability/require-test-traceability` Enforces traceability conventions in test files by requiring file-level `@supports` annotations, story references in `describe` blocks, and `[REQ-...]` prefixes in `it`/`test` names. (See the rule documentation in the plugin's user guide.)
|
|
58
|
-
- `traceability/prefer-
|
|
58
|
+
- `traceability/prefer-supports-annotation` Recommends migration from legacy `@story`/`@req` annotations to `@supports` (opt-in; disabled by default in the presets and must be explicitly enabled). The legacy rule name `traceability/prefer-implements-annotation` remains available as a deprecated alias. (See the rule documentation in the plugin's user guide.)
|
|
59
59
|
|
|
60
60
|
Configuration options: For detailed per-rule options (such as scopes, branch types, and story directory settings), see the individual rule docs in the plugin's user guide and the consolidated [API Reference](user-docs/api-reference.md).
|
|
61
61
|
|
package/lib/src/index.d.ts
CHANGED
|
@@ -10,13 +10,7 @@ import type { Rule } from "eslint";
|
|
|
10
10
|
* @req REQ-MAINTENANCE-API-EXPORT - Expose maintenance utilities alongside core plugin exports
|
|
11
11
|
*/
|
|
12
12
|
import { detectStaleAnnotations, updateAnnotationReferences, batchUpdateAnnotations, verifyAnnotations, generateMaintenanceReport } from "./maintenance";
|
|
13
|
-
|
|
14
|
-
* @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md
|
|
15
|
-
* @req REQ-RULE-LIST - Enumerate supported rule file names for plugin discovery
|
|
16
|
-
*/
|
|
17
|
-
declare const RULE_NAMES: readonly ["require-story-annotation", "require-req-annotation", "require-branch-annotation", "valid-annotation-format", "valid-story-reference", "valid-req-reference", "prefer-implements-annotation", "require-test-traceability"];
|
|
18
|
-
type RuleName = (typeof RULE_NAMES)[number];
|
|
19
|
-
declare const rules: Record<RuleName, Rule.RuleModule>;
|
|
13
|
+
declare const rules: Record<string, Rule.RuleModule>;
|
|
20
14
|
/**
|
|
21
15
|
* Plugin metadata used by ESLint for debugging and caching.
|
|
22
16
|
*
|
package/lib/src/index.js
CHANGED
|
@@ -74,6 +74,34 @@ RULE_NAMES.forEach(
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
|
+
/**
|
|
78
|
+
* @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-RULE-NAME
|
|
79
|
+
* Wire up traceability/prefer-supports-annotation as the primary rule name and
|
|
80
|
+
* traceability/prefer-implements-annotation as its deprecated alias.
|
|
81
|
+
*/
|
|
82
|
+
{
|
|
83
|
+
const implementsRule = rules["prefer-implements-annotation"];
|
|
84
|
+
if (implementsRule) {
|
|
85
|
+
const originalMeta = implementsRule.meta ?? {};
|
|
86
|
+
const preferSupportsRule = {
|
|
87
|
+
...implementsRule,
|
|
88
|
+
meta: {
|
|
89
|
+
...originalMeta,
|
|
90
|
+
deprecated: false,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
rules["prefer-supports-annotation"] = preferSupportsRule;
|
|
94
|
+
const implementsMeta = (implementsRule.meta =
|
|
95
|
+
implementsRule.meta ?? {});
|
|
96
|
+
implementsMeta.deprecated = true;
|
|
97
|
+
implementsMeta.replacedBy = ["prefer-supports-annotation"];
|
|
98
|
+
if (implementsMeta.docs &&
|
|
99
|
+
typeof implementsMeta.docs.description === "string") {
|
|
100
|
+
implementsMeta.docs.description +=
|
|
101
|
+
" (deprecated alias: use traceability/prefer-supports-annotation instead)";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
77
105
|
/**
|
|
78
106
|
* Plugin metadata used by ESLint for debugging and caching.
|
|
79
107
|
*
|
|
@@ -78,6 +78,61 @@ function validateBranchTypes(context) {
|
|
|
78
78
|
? options.branchTypes
|
|
79
79
|
: Array.from(exports.DEFAULT_BRANCH_TYPES);
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Extract the raw value from a comment node.
|
|
83
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
84
|
+
* @req REQ-TRACEABILITY-MAP-CALLBACK - Trace mapping of comment nodes to their text values
|
|
85
|
+
*/
|
|
86
|
+
function extractCommentValue(_c) {
|
|
87
|
+
return _c.value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
|
|
91
|
+
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
92
|
+
* @req REQ-DUAL-POSITION-DETECTION
|
|
93
|
+
* @req REQ-FALLBACK-LOGIC
|
|
94
|
+
*/
|
|
95
|
+
function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
|
|
96
|
+
if (/@story\b/.test(beforeText) || /@req\b/.test(beforeText)) {
|
|
97
|
+
return beforeText;
|
|
98
|
+
}
|
|
99
|
+
const getCommentsInside = sourceCode.getCommentsInside;
|
|
100
|
+
if (node.body && typeof getCommentsInside === "function") {
|
|
101
|
+
try {
|
|
102
|
+
const insideComments = getCommentsInside(node.body) || [];
|
|
103
|
+
const insideText = insideComments.map(extractCommentValue).join(" ");
|
|
104
|
+
if (insideText) {
|
|
105
|
+
return insideText;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// fall through to line-based fallback
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (node.body && node.body.loc && node.body.loc.start && node.body.loc.end) {
|
|
113
|
+
const lines = sourceCode.lines;
|
|
114
|
+
const startIndex = node.body.loc.start.line - 1;
|
|
115
|
+
const endIndex = node.body.loc.end.line - 1;
|
|
116
|
+
const comments = [];
|
|
117
|
+
let i = startIndex + 1;
|
|
118
|
+
while (i <= endIndex) {
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
if (!line || !line.trim()) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
comments.push(line.trim());
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
const insideText = comments.join(" ");
|
|
130
|
+
if (insideText) {
|
|
131
|
+
return insideText;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return beforeText;
|
|
135
|
+
}
|
|
81
136
|
/**
|
|
82
137
|
* Gather leading comment text for a branch node.
|
|
83
138
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -102,16 +157,12 @@ function gatherBranchCommentText(sourceCode, node) {
|
|
|
102
157
|
}
|
|
103
158
|
return comments.join(" ");
|
|
104
159
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* @req REQ-TRACEABILITY-MAP-CALLBACK - Trace mapping of comment nodes to their text values
|
|
110
|
-
*/
|
|
111
|
-
function commentToValue(c) {
|
|
112
|
-
return c.value;
|
|
160
|
+
const beforeComments = sourceCode.getCommentsBefore(node) || [];
|
|
161
|
+
const beforeText = beforeComments.map(extractCommentValue).join(" ");
|
|
162
|
+
if (node.type === "CatchClause") {
|
|
163
|
+
return gatherCatchClauseCommentText(sourceCode, node, beforeText);
|
|
113
164
|
}
|
|
114
|
-
return
|
|
165
|
+
return beforeText;
|
|
115
166
|
}
|
|
116
167
|
/**
|
|
117
168
|
* Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
|
|
@@ -168,8 +219,8 @@ function reportMissingReq(context, node, options) {
|
|
|
168
219
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
169
220
|
* @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer function used to insert missing @req
|
|
170
221
|
*/
|
|
171
|
-
function insertReqFixer(
|
|
172
|
-
return
|
|
222
|
+
function insertReqFixer(fxer) {
|
|
223
|
+
return fxer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`);
|
|
173
224
|
}
|
|
174
225
|
context.report({
|
|
175
226
|
node,
|
|
@@ -195,11 +246,39 @@ function getBranchAnnotationInfo(sourceCode, node) {
|
|
|
195
246
|
const text = gatherBranchCommentText(sourceCode, node);
|
|
196
247
|
const missingStory = !/@story\b/.test(text);
|
|
197
248
|
const missingReq = !/@req\b/.test(text);
|
|
198
|
-
|
|
199
|
-
|
|
249
|
+
let indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
|
|
250
|
+
let insertPos = sourceCode.getIndexFromLoc({
|
|
200
251
|
line: node.loc.start.line,
|
|
201
252
|
column: 0,
|
|
202
253
|
});
|
|
254
|
+
if (node.type === "CatchClause" && node.body) {
|
|
255
|
+
const bodyNode = node.body;
|
|
256
|
+
const bodyStatements = Array.isArray(bodyNode.body)
|
|
257
|
+
? bodyNode.body
|
|
258
|
+
: undefined;
|
|
259
|
+
const firstStatement = bodyStatements && bodyStatements.length > 0
|
|
260
|
+
? bodyStatements[0]
|
|
261
|
+
: undefined;
|
|
262
|
+
if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
|
|
263
|
+
const firstLine = firstStatement.loc.start.line;
|
|
264
|
+
const innerIndent = sourceCode.lines[firstLine - 1].match(/^(\s*)/)?.[1] || "";
|
|
265
|
+
indent = innerIndent;
|
|
266
|
+
insertPos = sourceCode.getIndexFromLoc({
|
|
267
|
+
line: firstLine,
|
|
268
|
+
column: 0,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else if (bodyNode.loc && bodyNode.loc.start) {
|
|
272
|
+
const blockLine = bodyNode.loc.start.line;
|
|
273
|
+
const blockIndent = sourceCode.lines[blockLine - 1].match(/^(\s*)/)?.[1] || "";
|
|
274
|
+
const innerIndent = `${blockIndent} `;
|
|
275
|
+
indent = innerIndent;
|
|
276
|
+
insertPos = sourceCode.getIndexFromLoc({
|
|
277
|
+
line: blockLine,
|
|
278
|
+
column: 0,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
203
282
|
return { missingStory, missingReq, indent, insertPos };
|
|
204
283
|
}
|
|
205
284
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
* Prettier integration tests for CatchClause annotation positions.
|
|
8
|
+
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
9
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-COMPATIBILITY
|
|
10
|
+
*/
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
describe("CatchClause annotations with Prettier (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
|
|
14
|
+
const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
|
|
15
|
+
const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
|
|
16
|
+
const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
|
|
17
|
+
const prettierPackageJson = require.resolve("prettier/package.json");
|
|
18
|
+
const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
|
|
19
|
+
function runEslintWithRequireBranchAnnotation(code) {
|
|
20
|
+
const args = [
|
|
21
|
+
"--no-config-lookup",
|
|
22
|
+
"--config",
|
|
23
|
+
configPath,
|
|
24
|
+
"--stdin",
|
|
25
|
+
"--stdin-filename",
|
|
26
|
+
"catch.js",
|
|
27
|
+
"--rule",
|
|
28
|
+
"no-unused-vars:off",
|
|
29
|
+
"--rule",
|
|
30
|
+
"no-magic-numbers:off",
|
|
31
|
+
"--rule",
|
|
32
|
+
"no-undef:off",
|
|
33
|
+
"--rule",
|
|
34
|
+
"no-console:off",
|
|
35
|
+
"--rule",
|
|
36
|
+
"traceability/require-branch-annotation:error",
|
|
37
|
+
];
|
|
38
|
+
return (0, child_process_1.spawnSync)(process.execPath, [eslintCliPath, ...args], {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
input: code,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function formatWithPrettier(source) {
|
|
44
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
input: source,
|
|
47
|
+
});
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
|
|
50
|
+
}
|
|
51
|
+
return result.stdout;
|
|
52
|
+
}
|
|
53
|
+
it("[REQ-PRETTIER-COMPATIBILITY-BEFORE] accepts code where annotations start before catch but are moved inside by Prettier", () => {
|
|
54
|
+
const original = `
|
|
55
|
+
function doSomething() {
|
|
56
|
+
return 42;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleError(error) {
|
|
60
|
+
console.error(error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
64
|
+
// @req REQ-BRANCH-TRY
|
|
65
|
+
try {
|
|
66
|
+
doSomething();
|
|
67
|
+
}
|
|
68
|
+
// @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
69
|
+
// @req REQ-CATCH-PATH
|
|
70
|
+
catch (error) {
|
|
71
|
+
handleError(error);
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
const formatted = formatWithPrettier(original);
|
|
75
|
+
// Sanity check: Prettier should move the branch annotations inside the catch body.
|
|
76
|
+
expect(formatted).toContain("catch (error) {");
|
|
77
|
+
const catchIndex = formatted.indexOf("catch (error) {");
|
|
78
|
+
const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
|
|
79
|
+
expect(storyIndex).toBeGreaterThan(catchIndex);
|
|
80
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
81
|
+
expect(result.status).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
it("[REQ-PRETTIER-COMPATIBILITY-INSIDE] accepts code where annotations start inside the catch body and are preserved by Prettier", () => {
|
|
84
|
+
const original = `
|
|
85
|
+
function doSomething() {
|
|
86
|
+
return 42;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleError(error) {
|
|
90
|
+
console.error(error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
94
|
+
// @req REQ-BRANCH-TRY
|
|
95
|
+
try {
|
|
96
|
+
doSomething();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
99
|
+
// @req REQ-CATCH-INSIDE
|
|
100
|
+
handleError(error);
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
const formatted = formatWithPrettier(original);
|
|
104
|
+
// Sanity: annotations should still be associated with the catch body after formatting.
|
|
105
|
+
expect(formatted).toContain("catch (error) {");
|
|
106
|
+
const catchIndex = formatted.indexOf("catch (error) {");
|
|
107
|
+
const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
|
|
108
|
+
expect(storyIndex).toBeGreaterThan(catchIndex);
|
|
109
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
110
|
+
expect(result.status).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
it("[REQ-PRETTIER-COMPATIBILITY-EMPTY] accepts empty catch blocks with inside-catch annotations after Prettier formatting", () => {
|
|
113
|
+
const original = `
|
|
114
|
+
function doSomething() {
|
|
115
|
+
return 42;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
119
|
+
// @req REQ-BRANCH-TRY
|
|
120
|
+
try {
|
|
121
|
+
doSomething();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
124
|
+
// @req REQ-CATCH-EMPTY
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
const formatted = formatWithPrettier(original);
|
|
128
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
129
|
+
expect(result.status).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
* Performance tests for valid-annotation-format on large annotated files.
|
|
8
|
+
*
|
|
9
|
+
* @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-MULTILINE-SUPPORT REQ-FLEXIBLE-PARSING REQ-SYNTAX-VALIDATION
|
|
10
|
+
*/
|
|
11
|
+
const eslint_1 = require("eslint");
|
|
12
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
13
|
+
const valid_annotation_format_1 = __importDefault(require("../../src/rules/valid-annotation-format"));
|
|
14
|
+
/**
|
|
15
|
+
* Build a large source file containing many functions with traceability
|
|
16
|
+
* annotations in both line and block comments.
|
|
17
|
+
*
|
|
18
|
+
* The generated code mixes valid and invalid annotation formats to exercise
|
|
19
|
+
* parsing, multi-line handling, and error-reporting paths at scale without
|
|
20
|
+
* relying on auto-fix.
|
|
21
|
+
*/
|
|
22
|
+
function buildLargeAnnotatedSource(functionCount, annotationsPerFunction) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
for (let i = 0; i < functionCount; i += 1) {
|
|
25
|
+
// JSDoc-style block comment with multi-line @story/@req values.
|
|
26
|
+
lines.push("/**");
|
|
27
|
+
lines.push(" * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md");
|
|
28
|
+
lines.push(" * @req REQ-FORMAT-SPECIFICATION");
|
|
29
|
+
lines.push(" */");
|
|
30
|
+
// Additional line comments with a mix of valid and intentionally
|
|
31
|
+
// invalid formats (missing extensions, traversal, malformed IDs).
|
|
32
|
+
for (let j = 0; j < annotationsPerFunction; j += 1) {
|
|
33
|
+
const selector = (i + j) % 4;
|
|
34
|
+
if (selector === 0) {
|
|
35
|
+
lines.push("// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story");
|
|
36
|
+
}
|
|
37
|
+
else if (selector === 1) {
|
|
38
|
+
lines.push("// @req REQ-EXAMPLE-" + i.toString(10));
|
|
39
|
+
}
|
|
40
|
+
else if (selector === 2) {
|
|
41
|
+
lines.push("// @story ../outside-project.story.md");
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lines.push("// @req invalid-format-id");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
lines.push(`function annotated_fn_${i}() {`);
|
|
48
|
+
lines.push(' return "ok";\n}');
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
describe("valid-annotation-format performance on large annotated files (Story 005.0-DEV-ANNOTATION-VALIDATION)", () => {
|
|
53
|
+
const ruleName = "traceability/valid-annotation-format";
|
|
54
|
+
it("[REQ-MULTILINE-SUPPORT][REQ-FLEXIBLE-PARSING] analyzes a large annotated file within a generous time budget", () => {
|
|
55
|
+
const linter = new eslint_1.Linter({ configType: "eslintrc" });
|
|
56
|
+
linter.defineRule(ruleName, valid_annotation_format_1.default);
|
|
57
|
+
// 150 functions each with several annotations provides a substantial
|
|
58
|
+
// volume of comments and annotation patterns without being extreme.
|
|
59
|
+
const source = buildLargeAnnotatedSource(150, 3);
|
|
60
|
+
const start = perf_hooks_1.performance.now();
|
|
61
|
+
const messages = linter.verify(source, {
|
|
62
|
+
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
|
|
63
|
+
rules: {
|
|
64
|
+
[ruleName]: "error",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
68
|
+
// Sanity check: we expect diagnostics for some invalid annotations so the
|
|
69
|
+
// rule is definitely executing its validation logic.
|
|
70
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
71
|
+
// Guardrail: keep analysis comfortably under ~5 seconds on CI hardware.
|
|
72
|
+
expect(durationMs).toBeLessThan(5000);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -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
|
+
"prefer-supports-annotation",
|
|
62
63
|
];
|
|
63
64
|
// Act: get actual rule names from plugin
|
|
64
65
|
const actual = Object.keys(index_1.rules);
|
|
@@ -18,80 +18,87 @@ const ruleTester = new eslint_1.RuleTester({
|
|
|
18
18
|
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
|
-
describe("prefer-implements-annotation
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
reason: "comment mixes @story/@req with existing @supports annotations",
|
|
60
|
-
},
|
|
21
|
+
describe("prefer-supports-annotation / prefer-implements-annotation aliasing (Story 010.3-DEV-MIGRATE-TO-SUPPORTS)", () => {
|
|
22
|
+
const valid = [
|
|
23
|
+
{
|
|
24
|
+
name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @story is ignored",
|
|
25
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction onlyStory() {}`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @req is ignored",
|
|
29
|
+
code: `/**\n * @req REQ-ONLY\n */\nfunction onlyReq() {}`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @supports only is ignored",
|
|
33
|
+
code: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction alreadyImplements() {}`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @story and @supports but no @req is ignored",
|
|
37
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction storyAndSupportsNoReq() {}`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @req and @supports but no @story is ignored",
|
|
41
|
+
code: `/**\n * @req REQ-ANNOTATION-REQUIRED\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction reqAndSupportsNoStory() {}`,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const invalid = [
|
|
45
|
+
{
|
|
46
|
+
name: "[REQ-OPTIONAL-WARNING] single-story @story + @req block triggers preferImplements message",
|
|
47
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
|
|
48
|
+
output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
|
|
49
|
+
errors: [{ messageId: "preferImplements" }],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "[REQ-MULTI-STORY-DETECT] mixed @story/@req and @supports triggers cannotAutoFix",
|
|
53
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction mixed() {}`,
|
|
54
|
+
errors: [
|
|
55
|
+
{
|
|
56
|
+
messageId: "cannotAutoFix",
|
|
57
|
+
data: {
|
|
58
|
+
reason: "comment mixes @story/@req with existing @supports annotations",
|
|
61
59
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
91
|
-
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "[REQ-MULTI-STORY-DETECT] multiple @story paths in same block trigger multiStoryDetected",
|
|
65
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md\n * @req REQ-BRANCH-DETECTION\n */\nfunction multiStory() {}`,
|
|
66
|
+
errors: [{ messageId: "multiStoryDetected" }],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "[REQ-AUTO-FIX] single @story + single @req auto-fixes to single @supports line",
|
|
70
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
|
|
71
|
+
output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
|
|
72
|
+
errors: [{ messageId: "preferImplements" }],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "[REQ-SINGLE-STORY-FIX] single @story with multiple @req lines auto-fixes to single @supports line containing all REQ IDs",
|
|
76
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ONE\n * @req REQ-TWO\n * @req REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
|
|
77
|
+
output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ONE REQ-TWO REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
|
|
78
|
+
errors: [{ messageId: "preferImplements" }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "[REQ-AUTO-FIX] complex @req content (extra description) does not auto-fix but still warns",
|
|
82
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED must handle extra description\n */\nfunction complexReqNoAutoFix() {}`,
|
|
83
|
+
errors: [{ messageId: "preferImplements" }],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "[REQ-AUTO-FIX] complex @story content (extra description) does not auto-fix but still warns",
|
|
87
|
+
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
|
|
88
|
+
errors: [{ messageId: "preferImplements" }],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
|
|
92
|
+
valid,
|
|
93
|
+
invalid,
|
|
94
|
+
});
|
|
95
|
+
ruleTester.run("prefer-supports-annotation", prefer_implements_annotation_1.default, {
|
|
96
|
+
valid,
|
|
97
|
+
invalid,
|
|
92
98
|
});
|
|
93
99
|
});
|
|
94
100
|
describe("prefer-implements-annotation configuration severity (REQ-CONFIG-SEVERITY)", () => {
|
|
101
|
+
// Story 010.3 / REQ-RULE-NAME: verify aliasing semantics for new primary rule name and deprecated alias
|
|
95
102
|
test("rule is disabled by default in recommended and strict presets (not present in preset rule maps)", () => {
|
|
96
103
|
const recommended = src_1.configs.recommended;
|
|
97
104
|
expect(Array.isArray(recommended)).toBe(true);
|
|
@@ -99,27 +106,34 @@ describe("prefer-implements-annotation configuration severity (REQ-CONFIG-SEVERI
|
|
|
99
106
|
expect(firstConfig).toBeDefined();
|
|
100
107
|
const rules = firstConfig.rules || {};
|
|
101
108
|
expect(rules["traceability/prefer-implements-annotation"]).toBeUndefined();
|
|
109
|
+
expect(rules["traceability/prefer-supports-annotation"]).toBeUndefined();
|
|
102
110
|
const strict = src_1.configs.strict;
|
|
103
111
|
expect(Array.isArray(strict)).toBe(true);
|
|
104
112
|
const strictFirstConfig = strict[0];
|
|
105
113
|
expect(strictFirstConfig).toBeDefined();
|
|
106
114
|
const strictRules = strictFirstConfig.rules || {};
|
|
107
115
|
expect(strictRules["traceability/prefer-implements-annotation"]).toBeUndefined();
|
|
116
|
+
expect(strictRules["traceability/prefer-supports-annotation"]).toBeUndefined();
|
|
108
117
|
});
|
|
109
118
|
test("rule can be configured with severity 'warn' or 'error' in flat config", () => {
|
|
119
|
+
// Story 010.3 / REQ-RULE-NAME: both primary and alias rule keys must be accepted in flat config
|
|
110
120
|
const flatWarnConfig = {
|
|
111
121
|
files: ["**/*.ts"],
|
|
112
122
|
rules: {
|
|
113
123
|
"traceability/prefer-implements-annotation": "warn",
|
|
124
|
+
"traceability/prefer-supports-annotation": "warn",
|
|
114
125
|
},
|
|
115
126
|
};
|
|
116
127
|
expect(flatWarnConfig.rules["traceability/prefer-implements-annotation"]).toBe("warn");
|
|
128
|
+
expect(flatWarnConfig.rules["traceability/prefer-supports-annotation"]).toBe("warn");
|
|
117
129
|
const flatErrorConfig = {
|
|
118
130
|
files: ["**/*.ts"],
|
|
119
131
|
rules: {
|
|
120
132
|
"traceability/prefer-implements-annotation": "error",
|
|
133
|
+
"traceability/prefer-supports-annotation": "error",
|
|
121
134
|
},
|
|
122
135
|
};
|
|
123
136
|
expect(flatErrorConfig.rules["traceability/prefer-implements-annotation"]).toBe("error");
|
|
137
|
+
expect(flatErrorConfig.rules["traceability/prefer-supports-annotation"]).toBe("error");
|
|
124
138
|
});
|
|
125
139
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for CatchClause insert position calculation.
|
|
5
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
6
|
+
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
7
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-AUTOFIX
|
|
8
|
+
*/
|
|
9
|
+
const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
|
|
10
|
+
describe("CatchClause insert position (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
|
|
11
|
+
it("[REQ-PRETTIER-AUTOFIX] inserts annotations at the first statement inside the catch body", () => {
|
|
12
|
+
const lines = [
|
|
13
|
+
"try {",
|
|
14
|
+
" doSomething();",
|
|
15
|
+
"}",
|
|
16
|
+
"catch (error) {",
|
|
17
|
+
" handleError(error);",
|
|
18
|
+
"}",
|
|
19
|
+
];
|
|
20
|
+
const fixer = {
|
|
21
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
22
|
+
};
|
|
23
|
+
const context = {
|
|
24
|
+
getSourceCode() {
|
|
25
|
+
return {
|
|
26
|
+
lines,
|
|
27
|
+
getCommentsBefore() {
|
|
28
|
+
return [];
|
|
29
|
+
},
|
|
30
|
+
getIndexFromLoc({ line, column }) {
|
|
31
|
+
// simple line/column to index mapping for the test: assume each line ends with "\n"
|
|
32
|
+
const prefix = lines.slice(0, line - 1).join("\n");
|
|
33
|
+
return prefix.length + (line > 1 ? 1 : 0) + column;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
report({ fix }) {
|
|
38
|
+
// immediately invoke the fixer to exercise the insert position
|
|
39
|
+
if (typeof fix === "function") {
|
|
40
|
+
fix(fixer);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const node = {
|
|
45
|
+
type: "CatchClause",
|
|
46
|
+
loc: { start: { line: 4 } },
|
|
47
|
+
body: {
|
|
48
|
+
type: "BlockStatement",
|
|
49
|
+
loc: { start: { line: 4 } },
|
|
50
|
+
body: [
|
|
51
|
+
{
|
|
52
|
+
type: "ExpressionStatement",
|
|
53
|
+
loc: { start: { line: 5 } },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const storyFixCountRef = { count: 0 };
|
|
59
|
+
(0, branch_annotation_helpers_1.reportMissingAnnotations)(context, node, storyFixCountRef);
|
|
60
|
+
expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
|
|
61
|
+
const [range, text] = fixer.insertTextBeforeRange.mock.calls[0];
|
|
62
|
+
// ensure we are inserting before the first statement in the catch body (line 5)
|
|
63
|
+
const expectedIndex = context.getSourceCode().getIndexFromLoc({ line: 5, column: 0 });
|
|
64
|
+
expect(range).toEqual([expectedIndex, expectedIndex]);
|
|
65
|
+
// and that the inserted text is prefixed with the inner indentation from line 5
|
|
66
|
+
expect(text.startsWith(" ")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
|
|
4
|
+
function createMockSourceCode(options) {
|
|
5
|
+
const { lines = [], commentsBefore = [], commentsInside = [] } = options;
|
|
6
|
+
return {
|
|
7
|
+
lines,
|
|
8
|
+
getCommentsBefore() {
|
|
9
|
+
return commentsBefore;
|
|
10
|
+
},
|
|
11
|
+
getCommentsInside(node) {
|
|
12
|
+
// exercise the code path that passes node.body into getCommentsInside
|
|
13
|
+
if (node && node.type === "BlockStatement") {
|
|
14
|
+
return commentsInside;
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe("gatherBranchCommentText CatchClause behavior (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
|
|
21
|
+
it("[REQ-DUAL-POSITION-DETECTION] prefers before-catch annotations when present", () => {
|
|
22
|
+
const sourceCode = createMockSourceCode({
|
|
23
|
+
commentsBefore: [
|
|
24
|
+
{ value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
|
|
25
|
+
{ value: "@req REQ-BRANCH-DETECTION" },
|
|
26
|
+
],
|
|
27
|
+
commentsInside: [
|
|
28
|
+
{ value: "@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md" },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
const node = {
|
|
32
|
+
type: "CatchClause",
|
|
33
|
+
loc: { start: { line: 5 } },
|
|
34
|
+
body: { type: "BlockStatement" },
|
|
35
|
+
};
|
|
36
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
|
|
37
|
+
expect(text).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
|
|
38
|
+
expect(text).toContain("@req REQ-BRANCH-DETECTION");
|
|
39
|
+
});
|
|
40
|
+
it("[REQ-FALLBACK-LOGIC] falls back to inside-catch annotations when before-catch is missing", () => {
|
|
41
|
+
const sourceCode = createMockSourceCode({
|
|
42
|
+
commentsBefore: [],
|
|
43
|
+
commentsInside: [
|
|
44
|
+
{ value: "@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md" },
|
|
45
|
+
{ value: "@req REQ-CATCH-PATH" },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
const node = {
|
|
49
|
+
type: "CatchClause",
|
|
50
|
+
loc: { start: { line: 10 } },
|
|
51
|
+
body: { type: "BlockStatement" },
|
|
52
|
+
};
|
|
53
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
|
|
54
|
+
expect(text).toContain("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
|
|
55
|
+
expect(text).toContain("@req REQ-CATCH-PATH");
|
|
56
|
+
});
|
|
57
|
+
it("[REQ-FALLBACK-LOGIC] returns before-catch text when getCommentsInside is not available", () => {
|
|
58
|
+
const lines = [
|
|
59
|
+
"try {",
|
|
60
|
+
" doSomething();",
|
|
61
|
+
"}",
|
|
62
|
+
"catch (error) {",
|
|
63
|
+
" // body",
|
|
64
|
+
"}",
|
|
65
|
+
];
|
|
66
|
+
const sourceCode = {
|
|
67
|
+
lines,
|
|
68
|
+
getCommentsBefore() {
|
|
69
|
+
return [
|
|
70
|
+
{ value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
|
|
71
|
+
{ value: "@req REQ-BRANCH-DETECTION" },
|
|
72
|
+
];
|
|
73
|
+
},
|
|
74
|
+
// intentionally omit getCommentsInside so that the CatchClause path
|
|
75
|
+
// falls back to the before-catch comments.
|
|
76
|
+
};
|
|
77
|
+
const node = {
|
|
78
|
+
type: "CatchClause",
|
|
79
|
+
loc: { start: { line: 4 } },
|
|
80
|
+
body: { type: "BlockStatement" },
|
|
81
|
+
};
|
|
82
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
|
|
83
|
+
expect(text).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
|
|
84
|
+
expect(text).toContain("@req REQ-BRANCH-DETECTION");
|
|
85
|
+
});
|
|
86
|
+
it("[REQ-FALLBACK-LOGIC] collects inside-catch comments using line-based fallback when getCommentsInside is unavailable", () => {
|
|
87
|
+
const lines = [
|
|
88
|
+
"try {",
|
|
89
|
+
" doSomething();",
|
|
90
|
+
"} catch (error) {",
|
|
91
|
+
" // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md",
|
|
92
|
+
" // @req REQ-CATCH-LINE-FALLBACK",
|
|
93
|
+
" handleError(error);",
|
|
94
|
+
"}",
|
|
95
|
+
];
|
|
96
|
+
const sourceCode = {
|
|
97
|
+
lines,
|
|
98
|
+
getCommentsBefore() {
|
|
99
|
+
return [];
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const node = {
|
|
103
|
+
type: "CatchClause",
|
|
104
|
+
loc: { start: { line: 3 } },
|
|
105
|
+
body: {
|
|
106
|
+
type: "BlockStatement",
|
|
107
|
+
loc: { start: { line: 3 }, end: { line: 7 } },
|
|
108
|
+
body: [],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
|
|
112
|
+
expect(text).toContain("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
|
|
113
|
+
expect(text).toContain("@req REQ-CATCH-LINE-FALLBACK");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const reqAnnotationDetection_1 = require("../../src/utils/reqAnnotationDetection");
|
|
4
|
+
// Small helper to construct a minimal SourceCode-like object for the detection helpers.
|
|
5
|
+
function createMockSourceCode(options = {}) {
|
|
6
|
+
const { lines = null, text = "", commentsBefore = [] } = options;
|
|
7
|
+
return {
|
|
8
|
+
lines: lines ?? undefined,
|
|
9
|
+
getText() {
|
|
10
|
+
return text;
|
|
11
|
+
},
|
|
12
|
+
getCommentsBefore() {
|
|
13
|
+
return commentsBefore;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe("reqAnnotationDetection advanced heuristics (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
|
|
18
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] returns false when sourceCode is missing", () => {
|
|
19
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], undefined, {
|
|
20
|
+
loc: null,
|
|
21
|
+
});
|
|
22
|
+
expect(has).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] returns false when node is missing", () => {
|
|
25
|
+
const context = {
|
|
26
|
+
getSourceCode() {
|
|
27
|
+
return createMockSourceCode({ lines: ["/** @req REQ-TEST */"] });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, undefined);
|
|
31
|
+
expect(has).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] inspects jsdoc and comments when advanced heuristics throw", () => {
|
|
34
|
+
const context = {
|
|
35
|
+
getSourceCode() {
|
|
36
|
+
// This object intentionally causes hasReqInAdvancedHeuristics to throw by
|
|
37
|
+
// providing a getCommentsBefore implementation that throws on access.
|
|
38
|
+
return {
|
|
39
|
+
getCommentsBefore() {
|
|
40
|
+
throw new Error("boom");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const jsdoc = { value: "/** @req REQ-FROM-JSDOC */" };
|
|
46
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, {
|
|
47
|
+
// Minimal shape – the helper will call into the mock sourceCode and trigger the throw
|
|
48
|
+
parent: {},
|
|
49
|
+
});
|
|
50
|
+
expect(has).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] treats @supports in comments as satisfying requirement", () => {
|
|
53
|
+
const context = {
|
|
54
|
+
getSourceCode() {
|
|
55
|
+
return createMockSourceCode();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const comments = [{ value: "// @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-X" }];
|
|
59
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, comments, context, {
|
|
60
|
+
parent: {},
|
|
61
|
+
});
|
|
62
|
+
expect(has).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] linesBeforeHasReq returns false when lines is not an array", () => {
|
|
65
|
+
const context = {
|
|
66
|
+
getSourceCode() {
|
|
67
|
+
// lines is null here, causing the helper to see a non-array and return false
|
|
68
|
+
return createMockSourceCode({ lines: null });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, {
|
|
72
|
+
// Provide a minimal location so advanced heuristics try to use line info
|
|
73
|
+
loc: { start: { line: 5 } },
|
|
74
|
+
parent: {},
|
|
75
|
+
});
|
|
76
|
+
expect(has).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] linesBeforeHasReq returns false when startLine is not a number", () => {
|
|
79
|
+
const sourceCode = createMockSourceCode({ lines: ["// @req REQ-SHOULD-NOT-BE-SEEN"] });
|
|
80
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], { getSourceCode: () => sourceCode }, {
|
|
81
|
+
// loc is missing/undefined; startLine will not be a valid number
|
|
82
|
+
loc: undefined,
|
|
83
|
+
parent: {},
|
|
84
|
+
});
|
|
85
|
+
expect(has).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] parentChainHasReq returns false when getCommentsBefore is not a function and no leadingComments/parents have req", () => {
|
|
88
|
+
const context = {
|
|
89
|
+
getSourceCode() {
|
|
90
|
+
return {
|
|
91
|
+
// getCommentsBefore is not a function here
|
|
92
|
+
getCommentsBefore: 123,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const node = {
|
|
97
|
+
parent: {
|
|
98
|
+
leadingComments: [{ value: "no req here" }],
|
|
99
|
+
parent: {
|
|
100
|
+
leadingComments: [{ value: "still nothing" }],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
105
|
+
expect(has).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] parentChainHasReq returns true when getCommentsBefore returns comments containing @req", () => {
|
|
108
|
+
const sourceCode = {
|
|
109
|
+
getCommentsBefore(n) {
|
|
110
|
+
if (n && n.isTargetParent) {
|
|
111
|
+
return [{ value: "/* @req REQ-FROM-PARENT */" }];
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const context = {
|
|
117
|
+
getSourceCode() {
|
|
118
|
+
return sourceCode;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const node = {
|
|
122
|
+
parent: {
|
|
123
|
+
isTargetParent: true,
|
|
124
|
+
parent: {},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
128
|
+
expect(has).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when getText is not a function", () => {
|
|
131
|
+
const context = {
|
|
132
|
+
getSourceCode() {
|
|
133
|
+
return {
|
|
134
|
+
// getText is not a function
|
|
135
|
+
getText: "not-a-function",
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const node = {
|
|
140
|
+
range: [0, 10],
|
|
141
|
+
parent: {},
|
|
142
|
+
};
|
|
143
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
144
|
+
expect(has).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when node.range is not an array", () => {
|
|
147
|
+
const context = {
|
|
148
|
+
getSourceCode() {
|
|
149
|
+
return createMockSourceCode({ text: "/* @req REQ-IN-TEXT */" });
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const node = {
|
|
153
|
+
// range is missing; helper should see non-array range and return false
|
|
154
|
+
range: null,
|
|
155
|
+
parent: {},
|
|
156
|
+
};
|
|
157
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
158
|
+
expect(has).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns true when text window contains @req", () => {
|
|
161
|
+
const fullText = `
|
|
162
|
+
// some header
|
|
163
|
+
/** @req REQ-IN-TEXT-WINDOW */
|
|
164
|
+
function foo() {}
|
|
165
|
+
`;
|
|
166
|
+
const context = {
|
|
167
|
+
getSourceCode() {
|
|
168
|
+
return createMockSourceCode({ text: fullText });
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
// Choose a range that starts after the @req comment so the "text before"
|
|
172
|
+
// window that the helper inspects includes the annotation.
|
|
173
|
+
const startIndex = fullText.indexOf("function foo");
|
|
174
|
+
const node = {
|
|
175
|
+
range: [startIndex, startIndex + 5],
|
|
176
|
+
parent: {},
|
|
177
|
+
};
|
|
178
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
179
|
+
expect(has).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when getText throws", () => {
|
|
182
|
+
const context = {
|
|
183
|
+
getSourceCode() {
|
|
184
|
+
return {
|
|
185
|
+
getText() {
|
|
186
|
+
throw new Error("boom from getText");
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const node = {
|
|
192
|
+
range: [0, 10],
|
|
193
|
+
parent: {},
|
|
194
|
+
};
|
|
195
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
196
|
+
expect(has).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] hasReqInAdvancedHeuristics short-circuits and returns false when sourceCode is missing", () => {
|
|
199
|
+
const context = {
|
|
200
|
+
// No getSourceCode method at all – internal advanced heuristics
|
|
201
|
+
// should immediately return false and not throw.
|
|
202
|
+
};
|
|
203
|
+
const node = {
|
|
204
|
+
loc: { start: { line: 3 } },
|
|
205
|
+
range: [0, 10],
|
|
206
|
+
parent: {},
|
|
207
|
+
};
|
|
208
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
|
|
209
|
+
expect(has).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] hasReqInAdvancedHeuristics short-circuits and returns false when node is missing", () => {
|
|
212
|
+
const context = {
|
|
213
|
+
getSourceCode() {
|
|
214
|
+
return createMockSourceCode({ text: "@req REQ-SHOULD-NOT-BE-SEEN" });
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, undefined);
|
|
218
|
+
expect(has).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] hasReqAnnotation returns true when jsdoc contains @supports and advanced heuristics are false", () => {
|
|
221
|
+
const context = {
|
|
222
|
+
getSourceCode() {
|
|
223
|
+
// Returning a sourceCode that will not satisfy any advanced heuristic
|
|
224
|
+
// (no lines, no comments, empty text).
|
|
225
|
+
return createMockSourceCode({ lines: [], text: "" });
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
const jsdoc = {
|
|
229
|
+
value: "/** @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-JSDOC-SUPPORTS */",
|
|
230
|
+
};
|
|
231
|
+
const node = {
|
|
232
|
+
parent: {},
|
|
233
|
+
};
|
|
234
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, node);
|
|
235
|
+
expect(has).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] falls back to jsdoc/comments when context.getSourceCode throws", () => {
|
|
238
|
+
const context = {
|
|
239
|
+
getSourceCode() {
|
|
240
|
+
throw new Error("boom from getSourceCode");
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
const jsdoc = { value: "/** @req REQ-FROM-GETSOURCECODE */" };
|
|
244
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, { parent: {} });
|
|
245
|
+
expect(has).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.3",
|
|
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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"safety:deps": "node scripts/ci-safety-deps.js",
|
|
42
42
|
"audit:ci": "node scripts/ci-audit.js",
|
|
43
43
|
"check:ci-artifacts": "node scripts/check-no-tracked-ci-artifacts.js",
|
|
44
|
-
"security:secrets": "secretlint \"**/*\"
|
|
44
|
+
"security:secrets": "secretlint \"**/*\"",
|
|
45
45
|
"smoke-test": "./scripts/smoke-test.sh",
|
|
46
46
|
"debug:cli": "node scripts/cli-debug.js",
|
|
47
47
|
"debug:require-story": "node scripts/debug-require-story.js",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"eslint": "^9.0.0"
|
|
100
100
|
},
|
|
101
101
|
"engines": {
|
|
102
|
-
"node": "
|
|
102
|
+
"node": "^18.18.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
103
103
|
},
|
|
104
104
|
"overrides": {
|
|
105
105
|
"glob": "12.0.0",
|
|
@@ -15,7 +15,7 @@ In addition to the core `@story` and `@req` annotations, the plugin also underst
|
|
|
15
15
|
`@supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND`
|
|
16
16
|
to indicate that a given function supports a particular requirement from a payments story document within that project’s own `docs/stories` tree. For a detailed explanation of `@supports` behavior and validation, see [Migration Guide](migration-guide.md) (section **3.1 Multi-story @supports annotations**). Additional background on multi-story semantics is available in the project’s internal rule documentation, which is intended for maintainers rather than end users.
|
|
17
17
|
|
|
18
|
-
The `prefer-
|
|
18
|
+
The `prefer-supports-annotation` rule is an **opt-in migration helper** that is disabled by default and **not** part of any built-in preset. It can be enabled and given a severity like `"warn"` or `"error"` using normal ESLint rule configuration when you want to gradually encourage multi-story `@supports` usage. The legacy rule key `traceability/prefer-implements-annotation` remains available as a **deprecated alias** for backward compatibility, but new configurations should prefer `traceability/prefer-supports-annotation`. Detailed behavior and migration guidance are documented in the project’s internal rule documentation, which is targeted at maintainers; typical end users can rely on the high-level guidance in this API reference and the [Migration Guide](migration-guide.md).
|
|
19
19
|
|
|
20
20
|
### traceability/require-story-annotation
|
|
21
21
|
|
|
@@ -68,11 +68,17 @@ function initAuth() {
|
|
|
68
68
|
|
|
69
69
|
### traceability/require-branch-annotation
|
|
70
70
|
|
|
71
|
-
Description: Ensures significant code branches (if/else, loops, switch cases, try/catch) have both `@story` and `@req` annotations in preceding comments.
|
|
71
|
+
Description: Ensures significant code branches (if/else, loops, switch cases, try/catch) have both `@story` and `@req` annotations in preceding comments. For `catch` clauses specifically, the rule accepts annotations either immediately before the `catch` keyword or as the first comment-only lines inside the catch block. This dual-position handling is designed to stay compatible with common formatters such as Prettier, which often move comments from before `catch` into the catch body.
|
|
72
|
+
|
|
72
73
|
Options:
|
|
73
74
|
|
|
74
75
|
- `branchTypes` (string[], optional) – AST node types that are treated as significant branches for annotation enforcement. Allowed values: "IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement". Default: ["IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"]. If an invalid branch type is provided, the rule reports a configuration error for each invalid value.
|
|
75
76
|
|
|
77
|
+
Behavior notes:
|
|
78
|
+
|
|
79
|
+
- When both before-`catch` and inside-block annotations are present for the same catch clause, the comments immediately before `catch` take precedence for validation and reporting.
|
|
80
|
+
- When auto-fixing missing annotations on a catch clause, the rule inserts placeholder comments inside the catch body so that formatters like Prettier preserve them and do not move them to unexpected locations.
|
|
81
|
+
|
|
76
82
|
Default Severity: `error`
|
|
77
83
|
Example:
|
|
78
84
|
|
|
@@ -246,9 +252,9 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
|
|
|
246
252
|
});
|
|
247
253
|
```
|
|
248
254
|
|
|
249
|
-
### traceability/prefer-
|
|
255
|
+
### traceability/prefer-supports-annotation
|
|
250
256
|
|
|
251
|
-
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.
|
|
257
|
+
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.
|
|
252
258
|
|
|
253
259
|
Options: None – this rule does not accept a configuration object. All tuning is done via the ESLint rule level (`"off"`, `"warn"`, `"error"`).
|
|
254
260
|
|
|
@@ -318,7 +324,7 @@ function issueRefund() {
|
|
|
318
324
|
}
|
|
319
325
|
```
|
|
320
326
|
|
|
321
|
-
With `traceability/prefer-
|
|
327
|
+
With `traceability/prefer-supports-annotation` enabled (or its deprecated alias `traceability/prefer-implements-annotation`) and ESLint run with `--fix`, the rule rewrites it to:
|
|
322
328
|
|
|
323
329
|
```js
|
|
324
330
|
/**
|
|
@@ -343,7 +349,9 @@ export default [
|
|
|
343
349
|
traceability.configs.recommended,
|
|
344
350
|
{
|
|
345
351
|
rules: {
|
|
346
|
-
"traceability/prefer-
|
|
352
|
+
"traceability/prefer-supports-annotation": "warn",
|
|
353
|
+
// The deprecated alias is still honored if you prefer:
|
|
354
|
+
// "traceability/prefer-implements-annotation": "warn",
|
|
347
355
|
},
|
|
348
356
|
},
|
|
349
357
|
];
|
|
@@ -358,7 +366,7 @@ The plugin provides two built-in presets for easy configuration:
|
|
|
358
366
|
Enables the **six core traceability rules** with severities tuned for common usage (most at `error`, with
|
|
359
367
|
`traceability/valid-annotation-format` at `warn` to reduce noise). This `warn` level for `traceability/valid-annotation-format` is intentional to keep early adoption noise low, but you can safely raise it to `error` in projects that want strict enforcement of annotation formatting.
|
|
360
368
|
|
|
361
|
-
The `prefer-
|
|
369
|
+
The `prefer-supports-annotation` migration rule (and its deprecated alias key `traceability/prefer-implements-annotation`) is **not included** in this (or any) preset and remains disabled by default. If you want to encourage or enforce multi-story `@supports` annotations, you must enable `traceability/prefer-supports-annotation` explicitly in your ESLint configuration and choose an appropriate severity (for example, `"warn"` during migration or `"error"` once fully adopted).
|
|
362
370
|
|
|
363
371
|
Core rules enabled by the `recommended` preset:
|
|
364
372
|
|
|
@@ -381,7 +389,7 @@ export default [js.configs.recommended, traceability.configs.recommended];
|
|
|
381
389
|
|
|
382
390
|
### strict
|
|
383
391
|
|
|
384
|
-
Currently mirrors the **recommended** preset, reserved for future stricter policies. As with the `recommended` preset, the `traceability/prefer-
|
|
392
|
+
Currently mirrors the **recommended** preset, reserved for future stricter policies. As with the `recommended` preset, the `traceability/prefer-supports-annotation` rule is **not** enabled here by default, and the deprecated alias key `traceability/prefer-implements-annotation` continues to be honored only when you opt into it manually in your own configuration.
|
|
385
393
|
|
|
386
394
|
Usage:
|
|
387
395
|
|
|
@@ -57,18 +57,22 @@ function integrate() {}
|
|
|
57
57
|
|
|
58
58
|
You **do not** need to change existing, single-story annotations that already use `@story` and `@req`. Migration to `@supports` is only recommended when a function or module genuinely implements requirements from more than one story file.
|
|
59
59
|
|
|
60
|
-
#### Optional `prefer-
|
|
60
|
+
#### Optional `prefer-supports-annotation` migration rule
|
|
61
61
|
|
|
62
|
-
For teams that want to gradually migrate from `@story` + `@req` to `@supports`, the plugin provides an optional rule: `traceability/prefer-
|
|
62
|
+
For teams that want to gradually migrate from `@story` + `@req` to `@supports`, the plugin provides an optional rule: `traceability/prefer-supports-annotation`.
|
|
63
63
|
|
|
64
|
-
- This
|
|
64
|
+
- This is the canonical rule name starting in v1.x.
|
|
65
|
+
- The legacy key `traceability/prefer-implements-annotation` remains supported as a **deprecated alias** for backward compatibility, but should not be used in new configurations.
|
|
66
|
+
|
|
67
|
+
- This rule is **disabled by default** and is **not** included in any built-in presets (the deprecated alias is also not enabled by any presets).
|
|
65
68
|
- You can enable it with any standard ESLint severity (`"off"`, `"warn"`, or `"error"`) in your config, for example:
|
|
66
69
|
|
|
67
70
|
```js
|
|
68
71
|
// excerpt from eslint.config.js
|
|
69
72
|
{
|
|
70
73
|
rules: {
|
|
71
|
-
"traceability/prefer-
|
|
74
|
+
"traceability/prefer-supports-annotation": "warn",
|
|
75
|
+
// "traceability/prefer-implements-annotation": "warn", // deprecated alias
|
|
72
76
|
},
|
|
73
77
|
}
|
|
74
78
|
```
|
|
@@ -101,7 +105,7 @@ Aligned with the internal rule behavior, the key cases are:
|
|
|
101
105
|
- Comments that contain only `@supports` lines, and
|
|
102
106
|
- Line comments such as `// @story ...`.
|
|
103
107
|
|
|
104
|
-
These forms are still supported by the plugin and are not modified by `traceability/prefer-
|
|
108
|
+
These forms are still supported by the plugin and are not modified by `traceability/prefer-supports-annotation`.
|
|
105
109
|
|
|
106
110
|
A typical migration path is:
|
|
107
111
|
|