eslint-plugin-traceability 1.17.0 → 1.18.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/README.md +107 -11
- package/lib/src/index.js +53 -33
- package/lib/src/maintenance/commands.d.ts +4 -0
- package/lib/src/maintenance/commands.js +4 -0
- package/lib/src/maintenance/index.d.ts +1 -0
- package/lib/src/maintenance/index.js +1 -0
- package/lib/src/maintenance/report.js +2 -2
- package/lib/src/maintenance/update.js +4 -2
- package/lib/src/rules/helpers/test-callback-exclusion.d.ts +5 -1
- package/lib/src/rules/helpers/test-callback-exclusion.js +2 -11
- package/lib/src/rules/helpers/valid-annotation-format-validators.js +8 -2
- package/lib/src/rules/no-redundant-annotation.js +4 -0
- package/lib/src/rules/prefer-implements-annotation.js +25 -20
- package/lib/src/rules/require-branch-annotation.js +16 -0
- package/lib/src/rules/valid-annotation-format.js +62 -42
- package/lib/src/utils/branch-annotation-helpers.d.ts +8 -1
- package/lib/src/utils/branch-annotation-helpers.js +2 -1
- package/lib/src/utils/branch-annotation-report-helpers.d.ts +1 -0
- package/lib/src/utils/branch-annotation-report-helpers.js +40 -11
- package/lib/tests/integration/no-redundant-annotation.integration.test.js +31 -0
- package/lib/tests/integration/require-traceability-test-callbacks.integration.test.d.ts +1 -0
- package/lib/tests/integration/require-traceability-test-callbacks.integration.test.js +148 -0
- package/lib/tests/maintenance/detect-isolated.test.js +22 -14
- package/lib/tests/perf/maintenance-cli-large-workspace.test.js +145 -64
- package/lib/tests/perf/maintenance-large-workspace.test.js +65 -46
- package/lib/tests/rules/no-redundant-annotation.test.js +15 -0
- package/lib/tests/rules/require-branch-annotation.test.js +18 -0
- package/lib/tests/utils/{annotation-checker-branches.test.d.ts → annotation-checker-autofix-behavior.test.d.ts} +1 -1
- package/lib/tests/utils/{annotation-checker-branches.test.js → annotation-checker-autofix-behavior.test.js} +2 -2
- package/package.json +2 -2
- package/user-docs/api-reference.md +6 -1
- package/user-docs/examples.md +32 -0
- package/user-docs/migration-guide.md +35 -1
|
@@ -42,6 +42,8 @@ const os = __importStar(require("os"));
|
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const perf_hooks_1 = require("perf_hooks");
|
|
44
44
|
const cli_1 = require("../../src/maintenance/cli");
|
|
45
|
+
// Performance budget documented in docs/maintenance-performance-tests.md
|
|
46
|
+
const CLI_LARGE_WORKSPACE_PERF_BUDGET_MS = 5000;
|
|
45
47
|
function createCliLargeWorkspace() {
|
|
46
48
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "traceability-cli-large-"));
|
|
47
49
|
// Create a modestly sized workspace reusing the same shape as the core perf tests,
|
|
@@ -71,78 +73,157 @@ export function cli_example_${moduleIndex}_${fileIndex}() {}
|
|
|
71
73
|
},
|
|
72
74
|
};
|
|
73
75
|
}
|
|
76
|
+
function createDeepNestedCliWorkspace() {
|
|
77
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "traceability-cli-deep-nested-"));
|
|
78
|
+
// Create a deeply nested directory structure with a small number of files.
|
|
79
|
+
for (let branchIndex = 0; branchIndex < 3; branchIndex += 1) {
|
|
80
|
+
const level1 = path.join(root, `branch-${branchIndex.toString().padStart(3, "0")}`);
|
|
81
|
+
fs.mkdirSync(level1);
|
|
82
|
+
const level2 = path.join(level1, "deep", "nested", "structure");
|
|
83
|
+
fs.mkdirSync(path.join(level1, "deep"), { recursive: true });
|
|
84
|
+
fs.mkdirSync(path.join(level1, "deep", "nested"), { recursive: true });
|
|
85
|
+
fs.mkdirSync(level2, { recursive: true });
|
|
86
|
+
for (let fileIndex = 0; fileIndex < 3; fileIndex += 1) {
|
|
87
|
+
const filePath = path.join(level2, `deep-file-${fileIndex.toString().padStart(3, "0")}.ts`);
|
|
88
|
+
const validStory = "cli-valid.story.md";
|
|
89
|
+
const staleStory = "cli-deep-stale.story.md";
|
|
90
|
+
const content = `/**
|
|
91
|
+
* @story ${validStory}
|
|
92
|
+
* @story ${staleStory}
|
|
93
|
+
*/
|
|
94
|
+
export function cli_deep_example_${branchIndex}_${fileIndex}() {}
|
|
95
|
+
`;
|
|
96
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Create the valid story file so that only the stale entries are reported.
|
|
100
|
+
fs.writeFileSync(path.join(root, "cli-valid.story.md"), "# cli valid", "utf8");
|
|
101
|
+
return {
|
|
102
|
+
root,
|
|
103
|
+
cleanup: () => {
|
|
104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
74
108
|
describe("Maintenance CLI on large workspaces (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
75
|
-
let workspace;
|
|
76
|
-
let originalCwd;
|
|
77
|
-
beforeAll(() => {
|
|
78
|
-
originalCwd = process.cwd();
|
|
79
|
-
workspace = createCliLargeWorkspace();
|
|
80
|
-
process.chdir(workspace.root);
|
|
81
|
-
});
|
|
82
|
-
afterAll(() => {
|
|
83
|
-
process.chdir(originalCwd);
|
|
84
|
-
workspace.cleanup();
|
|
85
|
-
});
|
|
86
109
|
it("[REQ-MAINT-DETECT] detect --json completes within a generous time budget and returns JSON payload", () => {
|
|
110
|
+
const { root, cleanup } = createCliLargeWorkspace();
|
|
111
|
+
const originalCwd = process.cwd();
|
|
112
|
+
process.chdir(root);
|
|
87
113
|
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
try {
|
|
115
|
+
const start = perf_hooks_1.performance.now();
|
|
116
|
+
const exitCode = (0, cli_1.runMaintenanceCli)([
|
|
117
|
+
"node",
|
|
118
|
+
"traceability-maint",
|
|
119
|
+
"detect",
|
|
120
|
+
"--root",
|
|
121
|
+
root,
|
|
122
|
+
"--json",
|
|
123
|
+
]);
|
|
124
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
125
|
+
expect(exitCode === 0 || exitCode === 1).toBe(true);
|
|
126
|
+
expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
127
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
128
|
+
const payloadRaw = String(logSpy.mock.calls[0][0]);
|
|
129
|
+
const payload = JSON.parse(payloadRaw);
|
|
130
|
+
expect(payload.root).toBe(root);
|
|
131
|
+
expect(Array.isArray(payload.stale)).toBe(true);
|
|
132
|
+
expect(payload.stale.length).toBeGreaterThan(0);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
logSpy.mockRestore();
|
|
136
|
+
process.chdir(originalCwd);
|
|
137
|
+
cleanup();
|
|
138
|
+
}
|
|
107
139
|
});
|
|
108
140
|
it("[REQ-MAINT-REPORT] report --format=json completes within a generous time budget", () => {
|
|
141
|
+
const { root, cleanup } = createCliLargeWorkspace();
|
|
142
|
+
const originalCwd = process.cwd();
|
|
143
|
+
process.chdir(root);
|
|
109
144
|
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
try {
|
|
146
|
+
const start = perf_hooks_1.performance.now();
|
|
147
|
+
const exitCode = (0, cli_1.runMaintenanceCli)([
|
|
148
|
+
"node",
|
|
149
|
+
"traceability-maint",
|
|
150
|
+
"report",
|
|
151
|
+
"--root",
|
|
152
|
+
root,
|
|
153
|
+
"--format",
|
|
154
|
+
"json",
|
|
155
|
+
]);
|
|
156
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
157
|
+
expect(exitCode).toBe(0);
|
|
158
|
+
expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
159
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
160
|
+
const payloadRaw = String(logSpy.mock.calls[0][0]);
|
|
161
|
+
const payload = JSON.parse(payloadRaw);
|
|
162
|
+
expect(payload.root).toBe(root);
|
|
163
|
+
expect(typeof payload.report).toBe("string");
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
logSpy.mockRestore();
|
|
167
|
+
process.chdir(originalCwd);
|
|
168
|
+
cleanup();
|
|
169
|
+
}
|
|
129
170
|
});
|
|
130
171
|
it("[REQ-MAINT-VERIFY] verify completes within a generous time budget and reports stale annotations", () => {
|
|
172
|
+
const { root, cleanup } = createCliLargeWorkspace();
|
|
173
|
+
const originalCwd = process.cwd();
|
|
174
|
+
process.chdir(root);
|
|
131
175
|
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
176
|
+
try {
|
|
177
|
+
const start = perf_hooks_1.performance.now();
|
|
178
|
+
const exitCode = (0, cli_1.runMaintenanceCli)([
|
|
179
|
+
"node",
|
|
180
|
+
"traceability-maint",
|
|
181
|
+
"verify",
|
|
182
|
+
"--root",
|
|
183
|
+
root,
|
|
184
|
+
]);
|
|
185
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
186
|
+
expect(exitCode).toBe(1);
|
|
187
|
+
expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
188
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
189
|
+
const message = String(logSpy.mock.calls[0][0]);
|
|
190
|
+
expect(message).toContain("Stale or invalid traceability annotations detected under");
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
logSpy.mockRestore();
|
|
194
|
+
process.chdir(originalCwd);
|
|
195
|
+
cleanup();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
it("[REQ-MAINT-DETECT] detect traverses deeply nested directories within a generous time budget", () => {
|
|
199
|
+
const { root, cleanup } = createDeepNestedCliWorkspace();
|
|
200
|
+
const originalCwd = process.cwd();
|
|
201
|
+
process.chdir(root);
|
|
202
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
203
|
+
try {
|
|
204
|
+
const start = perf_hooks_1.performance.now();
|
|
205
|
+
const exitCode = (0, cli_1.runMaintenanceCli)([
|
|
206
|
+
"node",
|
|
207
|
+
"traceability-maint",
|
|
208
|
+
"detect",
|
|
209
|
+
"--root",
|
|
210
|
+
root,
|
|
211
|
+
"--json",
|
|
212
|
+
]);
|
|
213
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
214
|
+
expect(exitCode === 0 || exitCode === 1).toBe(true);
|
|
215
|
+
expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
216
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
217
|
+
const payloadRaw = String(logSpy.mock.calls[0][0]);
|
|
218
|
+
const payload = JSON.parse(payloadRaw);
|
|
219
|
+
expect(payload.root).toBe(root);
|
|
220
|
+
expect(Array.isArray(payload.stale)).toBe(true);
|
|
221
|
+
expect(payload.stale.length).toBeGreaterThan(0);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
logSpy.mockRestore();
|
|
225
|
+
process.chdir(originalCwd);
|
|
226
|
+
cleanup();
|
|
227
|
+
}
|
|
147
228
|
});
|
|
148
229
|
});
|
|
@@ -45,6 +45,8 @@ const detect_1 = require("../../src/maintenance/detect");
|
|
|
45
45
|
const batch_1 = require("../../src/maintenance/batch");
|
|
46
46
|
const report_1 = require("../../src/maintenance/report");
|
|
47
47
|
const update_1 = require("../../src/maintenance/update");
|
|
48
|
+
// Performance budget for large-workspace maintenance tests; documented in docs/maintenance-performance-tests.md.
|
|
49
|
+
const LARGE_WORKSPACE_PERF_BUDGET_MS = 5000;
|
|
48
50
|
/**
|
|
49
51
|
* Shape of the synthetic large workspace:
|
|
50
52
|
* - 10 modules (module-000 .. module-009)
|
|
@@ -92,58 +94,75 @@ export function example_${moduleIndex}_${fileIndex}() {}
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
describe("Maintenance tools on large workspaces (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
95
|
-
let workspace;
|
|
96
|
-
beforeAll(() => {
|
|
97
|
-
workspace = createLargeWorkspace();
|
|
98
|
-
});
|
|
99
|
-
afterAll(() => {
|
|
100
|
-
workspace.cleanup();
|
|
101
|
-
});
|
|
102
97
|
it("[REQ-MAINT-DETECT] detectStaleAnnotations completes within a generous time budget", () => {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
const workspace = createLargeWorkspace();
|
|
99
|
+
try {
|
|
100
|
+
const start = perf_hooks_1.performance.now();
|
|
101
|
+
const stale = (0, detect_1.detectStaleAnnotations)(workspace.root);
|
|
102
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
103
|
+
// Sanity check: we expect at least some stale entries due to the generated stale-story-* references.
|
|
104
|
+
expect(stale.length).toBeGreaterThan(0);
|
|
105
|
+
// Guardrail: this operation should remain comfortably under ~5 seconds on CI hardware.
|
|
106
|
+
expect(durationMs).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
workspace.cleanup();
|
|
110
|
+
}
|
|
110
111
|
});
|
|
111
112
|
it("[REQ-MAINT-VERIFY] verifyAnnotations remains fast on large workspaces", () => {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
const workspace = createLargeWorkspace();
|
|
114
|
+
try {
|
|
115
|
+
const start = perf_hooks_1.performance.now();
|
|
116
|
+
const result = (0, batch_1.verifyAnnotations)(workspace.root);
|
|
117
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
118
|
+
// With both valid and stale references, verification should report false.
|
|
119
|
+
expect(result).toBe(false);
|
|
120
|
+
expect(durationMs).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
workspace.cleanup();
|
|
124
|
+
}
|
|
118
125
|
});
|
|
119
126
|
it("[REQ-MAINT-REPORT] generateMaintenanceReport produces output within a generous time budget", () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
const workspace = createLargeWorkspace();
|
|
128
|
+
try {
|
|
129
|
+
const start = perf_hooks_1.performance.now();
|
|
130
|
+
const report = (0, report_1.generateMaintenanceReport)(workspace.root);
|
|
131
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
132
|
+
expect(report).not.toBe("");
|
|
133
|
+
expect(durationMs).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
workspace.cleanup();
|
|
137
|
+
}
|
|
125
138
|
});
|
|
126
139
|
it("[REQ-MAINT-UPDATE] updateAnnotationReferences and batchUpdateAnnotations remain tractable", () => {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
const workspace = createLargeWorkspace();
|
|
141
|
+
try {
|
|
142
|
+
const exampleOldPath = "stale-story-0000.story.md";
|
|
143
|
+
const exampleNewPath = "updated-story-0000.story.md";
|
|
144
|
+
const singleStart = perf_hooks_1.performance.now();
|
|
145
|
+
const updatedCount = (0, update_1.updateAnnotationReferences)(workspace.root, exampleOldPath, exampleNewPath);
|
|
146
|
+
const singleDuration = perf_hooks_1.performance.now() - singleStart;
|
|
147
|
+
expect(updatedCount).toBeGreaterThan(0);
|
|
148
|
+
expect(singleDuration).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
149
|
+
const batchStart = perf_hooks_1.performance.now();
|
|
150
|
+
const totalUpdated = (0, batch_1.batchUpdateAnnotations)(workspace.root, [
|
|
151
|
+
{
|
|
152
|
+
oldPath: "stale-story-0001.story.md",
|
|
153
|
+
newPath: "updated-story-0001.story.md",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
oldPath: "stale-story-0002.story.md",
|
|
157
|
+
newPath: "updated-story-0002.story.md",
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
const batchDuration = perf_hooks_1.performance.now() - batchStart;
|
|
161
|
+
expect(totalUpdated).toBeGreaterThanOrEqual(2);
|
|
162
|
+
expect(batchDuration).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
workspace.cleanup();
|
|
166
|
+
}
|
|
148
167
|
});
|
|
149
168
|
});
|
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
* @req REQ-STATEMENT-SIGNIFICANCE - Verify that simple statements are treated as redundant when covered by scope
|
|
12
12
|
* @req REQ-SAFE-REMOVAL - Verify that auto-fix removes only redundant annotations and preserves code
|
|
13
13
|
* @req REQ-DIFFERENT-REQUIREMENTS - Verify that annotations with different requirement IDs are preserved
|
|
14
|
+
* @req REQ-CATCH-BLOCK-HANDLING - Verify that catch block annotations are not incorrectly treated as redundant
|
|
14
15
|
*/
|
|
15
16
|
const eslint_1 = require("eslint");
|
|
16
17
|
const no_redundant_annotation_1 = __importDefault(require("../../src/rules/no-redundant-annotation"));
|
|
@@ -37,6 +38,14 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
|
|
|
37
38
|
name: "[REQ-SCOPE-ANALYSIS] preserves annotations on both branch and statement when they intentionally duplicate each other",
|
|
38
39
|
code: `function example() {\n if (condition) { // @story docs/stories/007.0-EXAMPLE.story.md @req REQ-BRANCH\n // @story docs/stories/007.0-EXAMPLE.story.md\n // @req REQ-BRANCH\n doBranchWork();\n }\n}`,
|
|
39
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: "[REQ-CATCH-BLOCK-HANDLING] preserves catch block annotation from issue #6 scenario",
|
|
43
|
+
code: `async function example() {\n try {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (isSafeVersion({ version, vulnerabilityData })) {\n return version;\n }\n\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (!vulnerabilityData.isVulnerable) {\n return version;\n }\n } catch (error) {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n return null;\n }\n}`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "[REQ-CATCH-BLOCK-HANDLING] preserves annotations in nested catch blocks with repeated requirements",
|
|
47
|
+
code: `async function nestedCatches() {\n try {\n await checkPrimary();\n } catch (outerError) {\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n try {\n await attemptRecovery(outerError);\n } catch (innerError) {\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n await reportFailure(innerError);\n }\n }\n}`,
|
|
48
|
+
},
|
|
40
49
|
],
|
|
41
50
|
invalid: [
|
|
42
51
|
{
|
|
@@ -87,6 +96,12 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
|
|
|
87
96
|
output: `/**\n * @story docs/stories/009.0-EXAMPLE.story.md\n * @supports REQ-SUP-A, REQ-SUP-B\n */\nfunction example() {\n const supported = checkSupport();\n}`,
|
|
88
97
|
errors: [{ messageId: "redundantAnnotation" }],
|
|
89
98
|
},
|
|
99
|
+
{
|
|
100
|
+
name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation in finally block that repeats try-path coverage",
|
|
101
|
+
code: `async function example() {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n try {\n await doWork();\n } finally {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n await cleanUp();\n }\n}`,
|
|
102
|
+
output: `async function example() {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n try {\n await doWork();\n } finally {\n await cleanUp();\n }\n}`,
|
|
103
|
+
errors: [{ messageId: "redundantAnnotation" }],
|
|
104
|
+
},
|
|
90
105
|
// TODO: rule implementation exists; full invalid-case behavior tests pending refinement
|
|
91
106
|
// {
|
|
92
107
|
// name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation on simple return inside annotated if",
|
|
@@ -8,15 +8,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
9
9
|
* @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
|
|
10
10
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
11
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
11
12
|
* @req REQ-BRANCH-DETECTION - Verify require-branch-annotation rule enforces branch annotations
|
|
12
13
|
* @req REQ-ERROR-SPECIFIC - Branch-level missing-annotation error messages are specific and informative
|
|
13
14
|
* @req REQ-ERROR-CONSISTENCY - Branch-level missing-annotation error messages follow shared conventions
|
|
14
15
|
* @req REQ-ERROR-SUGGESTION - Branch-level missing-annotation errors include suggestions when applicable
|
|
15
16
|
* @req REQ-NESTED-HANDLING - Nested branch annotations are correctly enforced without duplicative reporting
|
|
16
17
|
* @req REQ-SUPPORTS-ALTERNATIVE - Branches annotated only with @supports are treated as fully annotated
|
|
18
|
+
* @req REQ-PLACEMENT-CONFIG - Rule supports configurable annotation placement modes
|
|
19
|
+
* @req REQ-DEFAULT-BACKWARD-COMPAT - Default placement remains backward compatible with existing behavior
|
|
17
20
|
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION REQ-NESTED-HANDLING REQ-SUPPORTS-ALTERNATIVE
|
|
18
21
|
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SPECIFIC REQ-ERROR-CONSISTENCY REQ-ERROR-SUGGESTION
|
|
19
22
|
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF REQ-POSITION-PRIORITY-ELSE-IF REQ-PRETTIER-AUTOFIX-ELSE-IF
|
|
23
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
|
|
20
24
|
*/
|
|
21
25
|
const eslint_1 = require("eslint");
|
|
22
26
|
const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));
|
|
@@ -184,6 +188,20 @@ if (outer) {
|
|
|
184
188
|
if (condition) {}`,
|
|
185
189
|
options: [{ branchTypes: ["IfStatement", "SwitchCase"] }],
|
|
186
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: "[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] if-statement with before-brace annotations using annotationPlacement: 'before'",
|
|
193
|
+
code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
194
|
+
// @req REQ-PLACEMENT-CONFIG
|
|
195
|
+
if (condition) {}`,
|
|
196
|
+
options: [{ annotationPlacement: "before" }],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] if-statement with before-brace annotations using annotationPlacement: 'inside' (temporary backward-compatible behavior)",
|
|
200
|
+
code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
201
|
+
// @req REQ-PLACEMENT-CONFIG
|
|
202
|
+
if (condition) {}`,
|
|
203
|
+
options: [{ annotationPlacement: "inside" }],
|
|
204
|
+
},
|
|
187
205
|
{
|
|
188
206
|
name: "[REQ-SUPPORTS-ALTERNATIVE] if-statement with only @supports annotation is treated as fully annotated",
|
|
189
207
|
code: `// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Focused
|
|
3
|
+
* Focused autofix behavior tests for annotation-checker helper.
|
|
4
4
|
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-AUTOFIX REQ-ANNOTATION-REPORTING
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -34,7 +34,7 @@ function createContextStub() {
|
|
|
34
34
|
};
|
|
35
35
|
return { context, report };
|
|
36
36
|
}
|
|
37
|
-
describe("annotation-checker helper
|
|
37
|
+
describe("annotation-checker helper autofix behavior (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
|
|
38
38
|
it("[REQ-ANNOTATION-AUTOFIX] attaches fix directly to node when parent is missing", () => {
|
|
39
39
|
const { context, report } = createContextStub();
|
|
40
40
|
const node = { type: "FunctionDeclaration" }; // no parent property
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"jest": "^30.2.0",
|
|
89
89
|
"jscpd": "^4.0.5",
|
|
90
90
|
"lint-staged": "^16.2.7",
|
|
91
|
-
"prettier": "^3.
|
|
91
|
+
"prettier": "^3.7.4",
|
|
92
92
|
"semantic-release": "25.0.2",
|
|
93
93
|
"ts-jest": "^29.4.6",
|
|
94
94
|
"typescript": "^5.9.3",
|
|
@@ -290,7 +290,7 @@ The rule accepts an optional configuration object:
|
|
|
290
290
|
- `requireDescribeStory` (boolean, optional) – When `true` (default), requires that each top-level `describe` block include a story reference somewhere in its description text (for example, a path such as `docs/stories/010.0-PAYMENTS.story.md` or a shorter project-specific alias that your team uses consistently).
|
|
291
291
|
- `requireTestReqPrefix` (boolean, optional) – When `true` (default), requires each `it`/`test` block name to begin with a requirement identifier in square brackets, such as `[REQ-PAYMENTS-REFUND]`. The exact `REQ-` pattern is shared with the `valid-annotation-format` rule’s requirement ID checks.
|
|
292
292
|
- `describePattern` (string, optional) – A JavaScript regular expression **source** (without leading and trailing `/`) that the `describe` description text must match when `requireDescribeStory` is enabled. This lets you enforce a project-specific format such as requiring a canonical story path or a `STORY-` style identifier in the `describe` string. If omitted, the default is equivalent to `"Story [0-9]+\\.[0-9]+-"`, which expects the description to include a story label such as `"Story 021.0-DEV-TEST-TRACEABILITY"`. You can override this to instead require full story paths or whatever story-labeling convention your project prefers.
|
|
293
|
-
- `autoFixTestTemplate` (boolean, optional) – When `true` (default), allows the rule’s `--fix` mode to insert a file-level `@supports` placeholder template at the top of test files that are missing it. The template is intentionally non-semantic and includes TODO-style guidance so humans can later replace it with a real story
|
|
293
|
+
- `autoFixTestTemplate` (boolean, optional) – When `true` (default), allows the rule’s `--fix` mode to insert a file-level `@supports` placeholder template at the top of test files that are missing it. The template is intentionally non-semantic and includes TODO-style guidance so humans can later replace it with a real story and requirement IDs; disabling this option prevents the rule from inserting the template automatically.
|
|
294
294
|
- `autoFixTestPrefixFormat` (boolean, optional) – When `true` (default), enables safe normalization of malformed `[REQ-XXX]` prefixes in `it`/`test` names during `--fix`. The rule only rewrites prefixes that already contain a recognizable requirement identifier and limits changes to formatting concerns (spacing, square brackets vs. parentheses, underscore and dash usage, and letter casing) without fabricating new IDs or guessing requirement names.
|
|
295
295
|
- `testSupportsTemplate` (string, optional) – Overrides the default file-level `@supports` placeholder template used when `autoFixTestTemplate` is enabled. This string should be a complete JSDoc-style block (for example, including `/**`, `*`, and `*/`) that encodes your project’s preferred TODO guidance or placeholder story path; it is inserted verbatim at the top of matching test files that lack a `@supports` annotation, and is never interpreted or expanded by the rule.
|
|
296
296
|
|
|
@@ -327,6 +327,8 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
|
|
|
327
327
|
|
|
328
328
|
Description: Detects and optionally removes **redundant** traceability annotations on code that is already covered by an enclosing annotated scope. It focuses on simple, statement-level constructs—such as `return` statements, basic variable declarations, and other leaf statements—where repeating the same `@story` / `@req` / `@supports` information adds noise without improving coverage. When run with `--fix`, the rule offers safe auto-fixes that remove only the redundant comments while preserving all annotations that are required to maintain correct traceability.
|
|
329
329
|
|
|
330
|
+
Catch blocks are treated as separate execution paths for traceability purposes, and annotations inside a `catch` block that intentionally repeat the requirements of the corresponding `try` path are not considered redundant and are never auto-removed. This behavior was introduced as part of story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION (Detect and Remove Redundant Annotations)` to avoid false positives on error-handling paths that implement the same requirement as the success path.
|
|
331
|
+
|
|
330
332
|
The rule is designed to complement the core presence and validation rules: it never treats removing a redundant annotation as valid if doing so would leave the underlying requirement or story **uncovered** according to the plugin’s normal rules. It only targets comments whose traceability content is already implied by a surrounding function, method, or branch annotation.
|
|
331
333
|
|
|
332
334
|
Options:
|
|
@@ -344,6 +346,9 @@ The rule accepts an optional configuration object:
|
|
|
344
346
|
Behavior notes:
|
|
345
347
|
|
|
346
348
|
- The rule only inspects comments that contain recognized traceability annotations (`@story`, `@req`, `@supports`) and are attached to simple statements (returns, expression statements, variable declarations, and similar leaf nodes). It intentionally does **not** attempt to de-duplicate annotations on functions, classes, or major branches, which remain the responsibility of the core rules. When a statement has multiple redundant traceability comments (for example, a small comment block that repeats both @story and @req lines), the rule reports a **single** diagnostic for that statement and, in fix mode, removes all of the redundant annotation comments associated with it in a single grouped fix.
|
|
349
|
+
- **Catch blocks and error-handling paths**
|
|
350
|
+
- The rule never treats annotations inside a `catch` block as redundant, even when they repeat exactly the same `(story, requirement)` pairs that already cover the surrounding `try` statement or other enclosing branches.
|
|
351
|
+
- This preserves explicit traceability for error-handling logic, in line with the requirements captured in story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION`, and avoids collapsing success-path and failure-path coverage into a single, less precise annotation.
|
|
347
352
|
- Auto-fix removes only the redundant traceability lines (and any now-empty comment delimiters when safe) while preserving surrounding non-traceability text in the same comment where possible.
|
|
348
353
|
- When no enclosing scope with compatible coverage is found within `maxScopeDepth`, the annotation is not considered redundant and is left unchanged.
|
|
349
354
|
|
package/user-docs/examples.md
CHANGED
|
@@ -187,3 +187,35 @@ Depending on your Prettier version and configuration, the exact layout of the `e
|
|
|
187
187
|
- For most branch types, `traceability/require-branch-annotation` associates comments immediately before the branch keyword (such as `if`, `else`, `switch`, `case`) with that branch. Branches can be annotated either with a single `@supports` line (preferred), or with the older `@story`/`@req` pair for backward compatibility. The rule treats a valid `@supports` annotation as satisfying both the story and requirement presence checks.
|
|
188
188
|
- For `catch` clauses and `else if` branches, the rule is formatter-aware and also looks at comments between the condition and the block, as well as the first comment-only lines inside the block body, so you do not need to fight Prettier if it moves your annotations.
|
|
189
189
|
- When annotations exist in more than one place around an `else if` branch, the rule prefers comments immediately before the `else if` line, then comments between the condition and the block, and finally comments inside the block body, matching the behavior described in the API reference and stories `025.0` and `026.0`.
|
|
190
|
+
|
|
191
|
+
## 7. Redundant annotations and catch blocks
|
|
192
|
+
|
|
193
|
+
When `traceability/no-redundant-annotation` is enabled (for example, via the recommended preset), `catch` blocks are always treated as distinct execution paths. Repeating the same `(story, requirement)` pair in a `catch` block as in the corresponding `try` path is not considered redundant and will be preserved. This behavior is part of the improvements captured in story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION`.
|
|
194
|
+
|
|
195
|
+
In the following example, running ESLint with `--fix` will not remove the `@supports` annotation on the `catch` block, even though it repeats the requirement from the `try` path, because it represents separate error-handling coverage.
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
function filterSafeVersions(allVersions) {
|
|
199
|
+
try {
|
|
200
|
+
const stable = allVersions.filter((v) => !v.includes("-beta"));
|
|
201
|
+
|
|
202
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
203
|
+
if (stable.length > 0) {
|
|
204
|
+
return stable;
|
|
205
|
+
}
|
|
206
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
207
|
+
else if (allVersions.length > 0) {
|
|
208
|
+
// Fall back to all versions if we have no clearly stable ones
|
|
209
|
+
return allVersions;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fallback when the input list is empty
|
|
213
|
+
return [];
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
216
|
+
// traceability/no-redundant-annotation keeps this as separate error-handling coverage,
|
|
217
|
+
// even though it repeats the requirement from the try path.
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
@@ -123,7 +123,7 @@ A typical migration path is:
|
|
|
123
123
|
- Enable it as `"warn"` to get non-breaking guidance and auto-fixes for straightforward cases.
|
|
124
124
|
- Optionally move to `"error"` once you want to strictly enforce `@supports` usage for all JSDoc blocks that are eligible for safe conversion.
|
|
125
125
|
|
|
126
|
-
#### When to keep `@story` +
|
|
126
|
+
#### When to keep `@story` + `req`
|
|
127
127
|
|
|
128
128
|
Keep your current annotations if:
|
|
129
129
|
|
|
@@ -229,6 +229,8 @@ In all cases, the rule is conservative:
|
|
|
229
229
|
|
|
230
230
|
The rule operates over both `@supports` and legacy `@story`/`@req` style annotations, so it continues to work even in mixed codebases during a long-running migration.
|
|
231
231
|
|
|
232
|
+
In addition, `catch` blocks are treated as distinct execution paths: repeating the same `(story, requirement)` pair in a `catch` block is **not** considered redundant, because the error-handling path is typically validated and reasoned about separately from the main control flow.
|
|
233
|
+
|
|
232
234
|
A simplified example, using an illustrative story path that represents a file in **your** documentation tree:
|
|
233
235
|
|
|
234
236
|
Before (redundant duplication inside a branch):
|
|
@@ -271,6 +273,38 @@ if (cart.items.length === 0) {
|
|
|
271
273
|
}
|
|
272
274
|
```
|
|
273
275
|
|
|
276
|
+
#### Example: try/if/else-if/catch with non-redundant catch annotation
|
|
277
|
+
|
|
278
|
+
The following example shows a `try` block with an `if` / `else if` chain that validates a safe operation, and a `catch` block that handles the error path for the **same** requirement. Both paths are annotated with the same `(story, requirement)` pair to make it clear that the requirement covers normal execution and error handling:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
async function performSafeOperation(input) {
|
|
282
|
+
try {
|
|
283
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
284
|
+
if (input == null) {
|
|
285
|
+
throw new Error("Missing input");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
289
|
+
if (typeof input === "string") {
|
|
290
|
+
return await doSafeStringOperation(input);
|
|
291
|
+
} else if (Array.isArray(input)) {
|
|
292
|
+
return await doSafeArrayOperation(input);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return await doSafeFallbackOperation(input);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// This catch represents the error-handling path for the same safe-operation requirement.
|
|
298
|
+
// Even though the coverage matches the try/if/else-if chain above, it is *not* redundant:
|
|
299
|
+
// it documents how failures are handled for the same requirement.
|
|
300
|
+
// @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
|
|
301
|
+
return handleSafeOperationFailure(error, input);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Here, `traceability/no-redundant-annotation` recognizes the `catch` block as a separate execution path from the main `try` body. The annotation in the `catch` remains intact and is **not** treated as redundant, even though it repeats the same `docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION` coverage as the guarded `if` / `else if` chain in the `try`. This behavior was introduced and validated as part of story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION (Detect and Remove Redundant Annotations)` to prevent regressions in real-world `try/if/else-if/catch` scenarios like the one discussed there.
|
|
307
|
+
|
|
274
308
|
#### Safe migration workflow
|
|
275
309
|
|
|
276
310
|
To use `traceability/no-redundant-annotation` safely during your v1.x migration:
|