eslint-plugin-traceability 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- 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/valid-annotation-format.js +62 -42
- 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 +5 -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/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
## [1.17.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.17.0...v1.17.1) (2025-12-10)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* avoid redundant-annotation false positives for catch blocks ([2ac69e2](https://github.com/voder-ai/eslint-plugin-traceability/commit/2ac69e2a03b54cf29bf2bc175771bf3b23aba6e9))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/lib/src/index.js
CHANGED
|
@@ -86,61 +86,77 @@ RULE_NAMES.forEach(
|
|
|
86
86
|
* and diagnostics).
|
|
87
87
|
*
|
|
88
88
|
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-CONFIGURABLE-SCOPE REQ-EXPORT-PRIORITY
|
|
89
|
+
* @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-UNIFIED-ALIAS-ENGINE
|
|
89
90
|
*/
|
|
90
|
-
{
|
|
91
|
+
function createAliasRuleMeta(unifiedRule, legacyRule) {
|
|
92
|
+
if (!legacyRule) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const baseMeta = (unifiedRule.meta ?? {});
|
|
96
|
+
const legacyMeta = (legacyRule.meta ?? {});
|
|
97
|
+
return {
|
|
98
|
+
...baseMeta,
|
|
99
|
+
...legacyMeta,
|
|
100
|
+
docs: {
|
|
101
|
+
...(baseMeta.docs ?? {}),
|
|
102
|
+
...(legacyMeta.docs ?? {}),
|
|
103
|
+
},
|
|
104
|
+
messages: {
|
|
105
|
+
...(baseMeta.messages ?? {}),
|
|
106
|
+
...(legacyMeta.messages ?? {}),
|
|
107
|
+
},
|
|
108
|
+
schema: legacyMeta.schema ??
|
|
109
|
+
baseMeta.schema ??
|
|
110
|
+
[],
|
|
111
|
+
hasSuggestions: legacyMeta.hasSuggestions ??
|
|
112
|
+
baseMeta.hasSuggestions,
|
|
113
|
+
fixable: legacyMeta.fixable ??
|
|
114
|
+
baseMeta.fixable,
|
|
115
|
+
deprecated: legacyMeta.deprecated ??
|
|
116
|
+
baseMeta.deprecated,
|
|
117
|
+
replacedBy: legacyMeta.replacedBy ??
|
|
118
|
+
baseMeta.replacedBy,
|
|
119
|
+
type: legacyMeta.type ??
|
|
120
|
+
baseMeta.type ??
|
|
121
|
+
"problem",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Wire up the unified `require-traceability` rule and its legacy alias rules
|
|
126
|
+
* so that they share the same implementation while preserving legacy metadata.
|
|
127
|
+
*
|
|
128
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-EXPORT-PRIORITY
|
|
129
|
+
* @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-UNIFIED-ALIAS-ENGINE
|
|
130
|
+
*/
|
|
131
|
+
function wireUnifiedFunctionAnnotationAliases() {
|
|
91
132
|
const unifiedRule = rules["require-traceability"];
|
|
92
133
|
const legacyStoryRule = rules["require-story-annotation"];
|
|
93
134
|
const legacyReqRule = rules["require-req-annotation"];
|
|
94
135
|
if (unifiedRule) {
|
|
95
136
|
const createAliasRule = (legacyRule) => {
|
|
96
|
-
|
|
137
|
+
const mergedMeta = createAliasRuleMeta(unifiedRule, legacyRule);
|
|
138
|
+
if (!mergedMeta) {
|
|
97
139
|
return unifiedRule;
|
|
98
140
|
}
|
|
99
|
-
|
|
100
|
-
const legacyMeta = (legacyRule.meta ?? {});
|
|
101
|
-
const mergedMeta = {
|
|
102
|
-
...baseMeta,
|
|
103
|
-
...legacyMeta,
|
|
104
|
-
docs: {
|
|
105
|
-
...(baseMeta.docs ?? {}),
|
|
106
|
-
...(legacyMeta.docs ?? {}),
|
|
107
|
-
},
|
|
108
|
-
messages: {
|
|
109
|
-
...(baseMeta.messages ?? {}),
|
|
110
|
-
...(legacyMeta.messages ?? {}),
|
|
111
|
-
},
|
|
112
|
-
schema: legacyMeta.schema ??
|
|
113
|
-
baseMeta.schema ??
|
|
114
|
-
[],
|
|
115
|
-
hasSuggestions: legacyMeta.hasSuggestions ??
|
|
116
|
-
baseMeta.hasSuggestions,
|
|
117
|
-
fixable: legacyMeta.fixable ??
|
|
118
|
-
baseMeta.fixable,
|
|
119
|
-
deprecated: legacyMeta.deprecated ??
|
|
120
|
-
baseMeta.deprecated,
|
|
121
|
-
replacedBy: legacyMeta.replacedBy ??
|
|
122
|
-
baseMeta.replacedBy,
|
|
123
|
-
type: legacyMeta.type ??
|
|
124
|
-
baseMeta.type ??
|
|
125
|
-
"problem",
|
|
126
|
-
};
|
|
127
|
-
const aliasRule = {
|
|
141
|
+
return {
|
|
128
142
|
...unifiedRule,
|
|
129
143
|
meta: mergedMeta,
|
|
130
144
|
create: unifiedRule.create,
|
|
131
145
|
};
|
|
132
|
-
return aliasRule;
|
|
133
146
|
};
|
|
134
147
|
rules["require-story-annotation"] = createAliasRule(legacyStoryRule);
|
|
135
148
|
rules["require-req-annotation"] = createAliasRule(legacyReqRule);
|
|
136
149
|
}
|
|
137
150
|
}
|
|
151
|
+
wireUnifiedFunctionAnnotationAliases();
|
|
138
152
|
/**
|
|
139
153
|
* @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-RULE-NAME
|
|
140
154
|
* Wire up traceability/prefer-supports-annotation as the primary rule name and
|
|
141
155
|
* traceability/prefer-implements-annotation as its deprecated alias.
|
|
156
|
+
*
|
|
157
|
+
* @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-MIGRATION-RULE-NAMING
|
|
142
158
|
*/
|
|
143
|
-
{
|
|
159
|
+
function wirePreferSupportsAlias() {
|
|
144
160
|
const implementsRule = rules["prefer-implements-annotation"];
|
|
145
161
|
if (implementsRule) {
|
|
146
162
|
const originalMeta = implementsRule.meta ?? {};
|
|
@@ -163,6 +179,7 @@ RULE_NAMES.forEach(
|
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
}
|
|
182
|
+
wirePreferSupportsAlias();
|
|
166
183
|
/**
|
|
167
184
|
* Plugin metadata used by ESLint for debugging and caching.
|
|
168
185
|
*
|
|
@@ -226,6 +243,9 @@ const TRACEABILITY_RULE_SEVERITIES = {
|
|
|
226
243
|
* @req REQ-ERROR-SEVERITY - Map rule types to appropriate ESLint severity levels (errors vs warnings)
|
|
227
244
|
* @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md
|
|
228
245
|
* @req REQ-CONFIG-PRESETS - Provide flat-config presets that self-register the plugin and core rules
|
|
246
|
+
*
|
|
247
|
+
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SEVERITY
|
|
248
|
+
* @supports docs/stories/002.0-DEV-ESLINT-CONFIG.story.md REQ-CONFIG-PRESETS
|
|
229
249
|
*/
|
|
230
250
|
function createTraceabilityFlatConfig() {
|
|
231
251
|
return {
|
|
@@ -7,6 +7,7 @@ export declare const EXIT_USAGE = 2;
|
|
|
7
7
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
8
8
|
* @req REQ-MAINT-DETECT - CLI surface for detection of stale annotations
|
|
9
9
|
* @req REQ-MAINT-SAFE - Return specific exit codes for stale vs clean states
|
|
10
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-SAFE
|
|
10
11
|
*/
|
|
11
12
|
export declare function handleDetect(normalized: NormalizedCliArgs): number;
|
|
12
13
|
/**
|
|
@@ -14,6 +15,7 @@ export declare function handleDetect(normalized: NormalizedCliArgs): number;
|
|
|
14
15
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
15
16
|
* @req REQ-MAINT-VERIFY - CLI surface for verification of annotations
|
|
16
17
|
* @req REQ-MAINT-SAFE - Return distinct exit codes for verification failures
|
|
18
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-VERIFY REQ-MAINT-SAFE
|
|
17
19
|
*/
|
|
18
20
|
export declare function handleVerify(normalized: NormalizedCliArgs): number;
|
|
19
21
|
/**
|
|
@@ -21,6 +23,7 @@ export declare function handleVerify(normalized: NormalizedCliArgs): number;
|
|
|
21
23
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
22
24
|
* @req REQ-MAINT-REPORT - CLI surface for human-readable maintenance reports
|
|
23
25
|
* @req REQ-MAINT-SAFE - Support machine-readable formats for safe automation
|
|
26
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-REPORT REQ-MAINT-SAFE
|
|
24
27
|
*/
|
|
25
28
|
export declare function handleReport(normalized: NormalizedCliArgs): number;
|
|
26
29
|
/**
|
|
@@ -28,5 +31,6 @@ export declare function handleReport(normalized: NormalizedCliArgs): number;
|
|
|
28
31
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
29
32
|
* @req REQ-MAINT-UPDATE - CLI surface for updating annotation references
|
|
30
33
|
* @req REQ-MAINT-SAFE - Provide dry-run mode and explicit parameter checks
|
|
34
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE REQ-MAINT-SAFE
|
|
31
35
|
*/
|
|
32
36
|
export declare function handleUpdate(normalized: NormalizedCliArgs): number;
|
|
@@ -28,6 +28,7 @@ exports.EXIT_USAGE = 2;
|
|
|
28
28
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
29
29
|
* @req REQ-MAINT-DETECT - CLI surface for detection of stale annotations
|
|
30
30
|
* @req REQ-MAINT-SAFE - Return specific exit codes for stale vs clean states
|
|
31
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-SAFE
|
|
31
32
|
*/
|
|
32
33
|
function handleDetect(normalized) {
|
|
33
34
|
const flags = (0, flags_1.parseFlags)(normalized);
|
|
@@ -54,6 +55,7 @@ Run 'traceability-maint report' for a structured summary.`);
|
|
|
54
55
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
55
56
|
* @req REQ-MAINT-VERIFY - CLI surface for verification of annotations
|
|
56
57
|
* @req REQ-MAINT-SAFE - Return distinct exit codes for verification failures
|
|
58
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-VERIFY REQ-MAINT-SAFE
|
|
57
59
|
*/
|
|
58
60
|
function handleVerify(normalized) {
|
|
59
61
|
const flags = (0, flags_1.parseFlags)(normalized);
|
|
@@ -71,6 +73,7 @@ function handleVerify(normalized) {
|
|
|
71
73
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
72
74
|
* @req REQ-MAINT-REPORT - CLI surface for human-readable maintenance reports
|
|
73
75
|
* @req REQ-MAINT-SAFE - Support machine-readable formats for safe automation
|
|
76
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-REPORT REQ-MAINT-SAFE
|
|
74
77
|
*/
|
|
75
78
|
function handleReport(normalized) {
|
|
76
79
|
const flags = (0, flags_1.parseFlags)(normalized);
|
|
@@ -96,6 +99,7 @@ function handleReport(normalized) {
|
|
|
96
99
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
97
100
|
* @req REQ-MAINT-UPDATE - CLI surface for updating annotation references
|
|
98
101
|
* @req REQ-MAINT-SAFE - Provide dry-run mode and explicit parameter checks
|
|
102
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE REQ-MAINT-SAFE
|
|
99
103
|
*/
|
|
100
104
|
function handleUpdate(normalized) {
|
|
101
105
|
const flags = (0, flags_1.parseFlags)(normalized);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @req REQ-MAINT-VERIFY
|
|
8
8
|
* @req REQ-MAINT-REPORT
|
|
9
9
|
* @req REQ-MAINT-SAFE
|
|
10
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-UPDATE REQ-MAINT-BATCH REQ-MAINT-VERIFY REQ-MAINT-REPORT REQ-MAINT-SAFE
|
|
10
11
|
*/
|
|
11
12
|
export { detectStaleAnnotations } from "./detect";
|
|
12
13
|
export { updateAnnotationReferences } from "./update";
|
|
@@ -10,6 +10,7 @@ exports.generateMaintenanceReport = exports.verifyAnnotations = exports.batchUpd
|
|
|
10
10
|
* @req REQ-MAINT-VERIFY
|
|
11
11
|
* @req REQ-MAINT-REPORT
|
|
12
12
|
* @req REQ-MAINT-SAFE
|
|
13
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-UPDATE REQ-MAINT-BATCH REQ-MAINT-VERIFY REQ-MAINT-REPORT REQ-MAINT-SAFE
|
|
13
14
|
*/
|
|
14
15
|
var detect_1 = require("./detect");
|
|
15
16
|
Object.defineProperty(exports, "detectStaleAnnotations", { enumerable: true, get: function () { return detect_1.detectStaleAnnotations; } });
|
|
@@ -12,8 +12,8 @@ const detect_1 = require("./detect");
|
|
|
12
12
|
*/
|
|
13
13
|
function generateMaintenanceReport(codebasePath) {
|
|
14
14
|
const staleAnnotations = (0, detect_1.detectStaleAnnotations)(codebasePath);
|
|
15
|
-
// @
|
|
16
|
-
// @
|
|
15
|
+
// @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-SAFE - When no stale annotations are found, return empty string to indicate no actions required
|
|
16
|
+
// @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-REPORT - When stale annotations exist, produce a newline-separated report
|
|
17
17
|
if (staleAnnotations.length === 0) {
|
|
18
18
|
return "";
|
|
19
19
|
}
|
|
@@ -40,6 +40,7 @@ const utils_1 = require("./utils");
|
|
|
40
40
|
* Helper to process a single file for annotation reference updates
|
|
41
41
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
42
42
|
* @req REQ-MAINT-UPDATE
|
|
43
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
|
|
43
44
|
*/
|
|
44
45
|
function processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef) {
|
|
45
46
|
const content = fs.readFileSync(fullPath, "utf8"); // getAllFiles already returns regular files
|
|
@@ -78,8 +79,7 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
|
|
|
78
79
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
79
80
|
* @req REQ-MAINT-UPDATE
|
|
80
81
|
*/
|
|
81
|
-
// @
|
|
82
|
-
// @req REQ-MAINT-UPDATE
|
|
82
|
+
// @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
|
|
83
83
|
if (!fs.existsSync(codebasePath) ||
|
|
84
84
|
!fs.statSync(codebasePath).isDirectory()) {
|
|
85
85
|
return 0;
|
|
@@ -92,11 +92,13 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
|
|
|
92
92
|
* Iterate over all files and replace annotation references
|
|
93
93
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
94
94
|
* @req REQ-MAINT-UPDATE
|
|
95
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
|
|
95
96
|
*/
|
|
96
97
|
/**
|
|
97
98
|
* Loop over each discovered file path
|
|
98
99
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
99
100
|
* @req REQ-MAINT-UPDATE
|
|
101
|
+
* @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
|
|
100
102
|
*/
|
|
101
103
|
for (const fullPath of files) {
|
|
102
104
|
processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @story docs/stories/013-exclude-test-framework-callbacks.proposed.md
|
|
9
9
|
* @req REQ-TEST-CALLBACK-EXCLUSION - Provide reusable test callback exclusion logic
|
|
10
10
|
*/
|
|
11
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
11
12
|
/**
|
|
12
13
|
* Options controlling how test callbacks are treated by the helpers.
|
|
13
14
|
*
|
|
@@ -21,6 +22,9 @@ interface CallbackExclusionOptions {
|
|
|
21
22
|
excludeTestCallbacks?: boolean;
|
|
22
23
|
additionalTestHelperNames?: string[];
|
|
23
24
|
}
|
|
25
|
+
type TraceabilityNodeWithParent = TSESTree.Node & {
|
|
26
|
+
parent?: TraceabilityNodeWithParent | null;
|
|
27
|
+
};
|
|
24
28
|
/**
|
|
25
29
|
* Determine whether a node represents a callback passed to a known test
|
|
26
30
|
* framework function (Jest, Mocha, Vitest, etc).
|
|
@@ -34,6 +38,6 @@ interface CallbackExclusionOptions {
|
|
|
34
38
|
*
|
|
35
39
|
* @req REQ-TEST-CALLBACK-EXCLUSION
|
|
36
40
|
*/
|
|
37
|
-
declare function isTestFrameworkCallback(node:
|
|
41
|
+
declare function isTestFrameworkCallback(node: TraceabilityNodeWithParent | null | undefined, options?: CallbackExclusionOptions): boolean;
|
|
38
42
|
export type { CallbackExclusionOptions };
|
|
39
43
|
export { isTestFrameworkCallback };
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Shared helpers for determining whether a function-like node should be
|
|
4
|
-
* treated as a test framework callback that may be excluded from
|
|
5
|
-
* function-level annotation requirements.
|
|
6
|
-
*
|
|
7
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
8
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
9
|
-
* @story docs/stories/013-exclude-test-framework-callbacks.proposed.md
|
|
10
|
-
* @req REQ-TEST-CALLBACK-EXCLUSION - Provide reusable test callback exclusion logic
|
|
11
|
-
*/
|
|
12
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
3
|
exports.isTestFrameworkCallback = isTestFrameworkCallback;
|
|
14
4
|
/**
|
|
@@ -91,7 +81,8 @@ function isTestFrameworkCallback(node, options) {
|
|
|
91
81
|
if (!parent || parent.type !== "CallExpression") {
|
|
92
82
|
return false;
|
|
93
83
|
}
|
|
94
|
-
const
|
|
84
|
+
const callExpressionParent = parent;
|
|
85
|
+
const callee = callExpressionParent.callee;
|
|
95
86
|
if (callee.type === "Identifier") {
|
|
96
87
|
return isRecognizedTestHelperName(callee.name, options);
|
|
97
88
|
}
|
|
@@ -175,8 +175,8 @@ function validateStoryAnnotation(context, comment, rawValue, options) {
|
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
178
|
-
// @req REQ-PATH-FORMAT - Reject @story values containing internal whitespace
|
|
179
|
-
if (/\s/.test(trimmed)) {
|
|
178
|
+
// @req REQ-PATH-FORMAT - Reject @story values containing internal whitespace that do not collapse into a valid story path
|
|
179
|
+
if (/\s/.test(trimmed) && !pathPattern.test(collapsed)) {
|
|
180
180
|
reportInvalidStoryFormat(context, comment, collapsed, options);
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
@@ -223,6 +223,12 @@ function validateReqAnnotation(context, comment, rawValue, options) {
|
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
225
|
const collapsed = (0, valid_annotation_utils_1.collapseAnnotationValue)(trimmed);
|
|
226
|
+
// Allow mixed @req/@supports lines to pass without additional @req validation,
|
|
227
|
+
// while still validating simple multi-line @req identifiers that collapse
|
|
228
|
+
// to a single token.
|
|
229
|
+
if (collapsed.includes("@supports")) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
226
232
|
const reqPattern = options.reqPattern;
|
|
227
233
|
// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
228
234
|
// @req REQ-REQ-FORMAT - Flag @req identifiers that do not match the configured pattern
|
|
@@ -326,6 +326,10 @@ const rule = {
|
|
|
326
326
|
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
327
327
|
console.log("[no-redundant-annotation] BlockStatement parent=%s statements=%d", parent && parent.type, Array.isArray(node.body) ? node.body.length : 0);
|
|
328
328
|
}
|
|
329
|
+
// @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-CATCH-BLOCK-HANDLING
|
|
330
|
+
if (parent && parent.type === "CatchClause") {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
329
333
|
const scopePairs = collectScopePairs(context, parent, options.maxScopeDepth);
|
|
330
334
|
debugScopePairs(parent, scopePairs);
|
|
331
335
|
if (scopePairs.size === 0)
|
|
@@ -286,17 +286,8 @@ function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
|
|
|
286
286
|
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
287
287
|
* @req REQ-MIGRATE-INLINE
|
|
288
288
|
*/
|
|
289
|
-
function
|
|
289
|
+
function collectReqIndicesAfterStory(group, startIndex) {
|
|
290
290
|
const n = group.length;
|
|
291
|
-
const current = group[startIndex];
|
|
292
|
-
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
293
|
-
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
294
|
-
return startIndex + 1;
|
|
295
|
-
}
|
|
296
|
-
if (/^@supports\b/.test(normalized)) {
|
|
297
|
-
return startIndex + 1;
|
|
298
|
-
}
|
|
299
|
-
const storyIndex = startIndex;
|
|
300
291
|
const reqIndices = [];
|
|
301
292
|
let j = startIndex + 1;
|
|
302
293
|
while (j < n) {
|
|
@@ -312,6 +303,19 @@ function handleInlineStorySequence(context, group, startIndex) {
|
|
|
312
303
|
}
|
|
313
304
|
break;
|
|
314
305
|
}
|
|
306
|
+
return { reqIndices, nextIndex: j };
|
|
307
|
+
}
|
|
308
|
+
function handleInlineStorySequence(context, group, startIndex) {
|
|
309
|
+
const current = group[startIndex];
|
|
310
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
311
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
312
|
+
return startIndex + 1;
|
|
313
|
+
}
|
|
314
|
+
if (/^@supports\b/.test(normalized)) {
|
|
315
|
+
return startIndex + 1;
|
|
316
|
+
}
|
|
317
|
+
const storyIndex = startIndex;
|
|
318
|
+
const { reqIndices, nextIndex } = collectReqIndicesAfterStory(group, startIndex);
|
|
315
319
|
if (reqIndices.length === 0) {
|
|
316
320
|
context.report({
|
|
317
321
|
node: current,
|
|
@@ -333,7 +337,7 @@ function handleInlineStorySequence(context, group, startIndex) {
|
|
|
333
337
|
messageId: "preferImplements",
|
|
334
338
|
});
|
|
335
339
|
}
|
|
336
|
-
return
|
|
340
|
+
return nextIndex;
|
|
337
341
|
}
|
|
338
342
|
/**
|
|
339
343
|
* Process a contiguous group of inline line comments, identifying legacy
|
|
@@ -343,19 +347,20 @@ function handleInlineStorySequence(context, group, startIndex) {
|
|
|
343
347
|
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
344
348
|
* @req REQ-MIGRATE-INLINE
|
|
345
349
|
*/
|
|
350
|
+
function advanceInlineGroupIndex(context, group, currentIndex) {
|
|
351
|
+
const current = group[currentIndex];
|
|
352
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
353
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
354
|
+
return currentIndex + 1;
|
|
355
|
+
}
|
|
356
|
+
return handleInlineStorySequence(context, group, currentIndex);
|
|
357
|
+
}
|
|
346
358
|
function processInlineGroup(context, group) {
|
|
347
359
|
if (group.length === 0)
|
|
348
360
|
return;
|
|
349
|
-
const n = group.length;
|
|
350
361
|
let i = 0;
|
|
351
|
-
while (i <
|
|
352
|
-
|
|
353
|
-
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
354
|
-
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
355
|
-
i += 1;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
i = handleInlineStorySequence(context, group, i);
|
|
362
|
+
while (i < group.length) {
|
|
363
|
+
i = advanceInlineGroupIndex(context, group, i);
|
|
359
364
|
}
|
|
360
365
|
}
|
|
361
366
|
/**
|
|
@@ -3,6 +3,49 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const valid_annotation_options_1 = require("./helpers/valid-annotation-options");
|
|
4
4
|
const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
|
|
5
5
|
const valid_annotation_format_validators_1 = require("./helpers/valid-annotation-format-validators");
|
|
6
|
+
function handleImplementsLine(normalized, pending, deps) {
|
|
7
|
+
const { context, comment, options } = deps;
|
|
8
|
+
const isImplements = /@supports\b/.test(normalized);
|
|
9
|
+
if (!isImplements) {
|
|
10
|
+
return pending;
|
|
11
|
+
}
|
|
12
|
+
const implementsValue = normalized.replace(/^@supports\b/, "").trim();
|
|
13
|
+
(0, valid_annotation_format_validators_1.validateImplementsAnnotation)(context, comment, implementsValue, options);
|
|
14
|
+
return pending;
|
|
15
|
+
}
|
|
16
|
+
function handleStoryOrReqLine(normalized, pending, deps) {
|
|
17
|
+
const { context, comment, options } = deps;
|
|
18
|
+
const isStory = /@story\b/.test(normalized);
|
|
19
|
+
const isReq = /@req\b/.test(normalized);
|
|
20
|
+
if (!isStory && !isReq) {
|
|
21
|
+
return pending;
|
|
22
|
+
}
|
|
23
|
+
(0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
|
|
24
|
+
const rawValue = normalized.replace(/^@story\b|^@req\b/, "");
|
|
25
|
+
const trimmedValue = rawValue.trim();
|
|
26
|
+
return {
|
|
27
|
+
type: isStory ? "story" : "req",
|
|
28
|
+
value: trimmedValue,
|
|
29
|
+
hasValue: trimmedValue.length > 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function extendPendingAnnotation(normalized, pending) {
|
|
33
|
+
if (!pending) {
|
|
34
|
+
return pending;
|
|
35
|
+
}
|
|
36
|
+
const continuation = normalized.trim();
|
|
37
|
+
if (!continuation) {
|
|
38
|
+
return pending;
|
|
39
|
+
}
|
|
40
|
+
const updatedValue = pending.value
|
|
41
|
+
? `${pending.value} ${continuation}`
|
|
42
|
+
: continuation;
|
|
43
|
+
return {
|
|
44
|
+
...pending,
|
|
45
|
+
value: updatedValue,
|
|
46
|
+
hasValue: pending.hasValue || continuation.length > 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
6
49
|
/**
|
|
7
50
|
* Process a single normalized comment line and update the pending annotation state.
|
|
8
51
|
*
|
|
@@ -22,31 +65,21 @@ function processCommentLine({ normalized, pending, context, comment, options, })
|
|
|
22
65
|
if (!normalized) {
|
|
23
66
|
return pending;
|
|
24
67
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const implementsValue = normalized.replace(/^@supports\b/, "").trim();
|
|
33
|
-
(0, valid_annotation_format_validators_1.validateImplementsAnnotation)(context, comment, implementsValue, options);
|
|
34
|
-
return pending;
|
|
68
|
+
const afterImplements = handleImplementsLine(normalized, pending, {
|
|
69
|
+
context,
|
|
70
|
+
comment,
|
|
71
|
+
options,
|
|
72
|
+
});
|
|
73
|
+
if (afterImplements !== pending) {
|
|
74
|
+
return afterImplements;
|
|
35
75
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
|
|
44
|
-
const value = normalized.replace(/^@story\b|^@req\b/, "").trim();
|
|
45
|
-
return {
|
|
46
|
-
type: isStory ? "story" : "req",
|
|
47
|
-
value,
|
|
48
|
-
hasValue: value.trim().length > 0,
|
|
49
|
-
};
|
|
76
|
+
const afterStoryOrReq = handleStoryOrReqLine(normalized, pending, {
|
|
77
|
+
context,
|
|
78
|
+
comment,
|
|
79
|
+
options,
|
|
80
|
+
});
|
|
81
|
+
if (afterStoryOrReq !== pending) {
|
|
82
|
+
return afterStoryOrReq;
|
|
50
83
|
}
|
|
51
84
|
// Implement JSDoc tag coexistence behavior: terminate @story/@req values when a new non-traceability JSDoc tag line (e.g., @param, @returns) is encountered.
|
|
52
85
|
// @supports docs/stories/022.0-DEV-JSDOC-COEXISTENCE.story.md REQ-ANNOTATION-TERMINATION REQ-CONTINUATION-LOGIC
|
|
@@ -60,23 +93,7 @@ function processCommentLine({ normalized, pending, context, comment, options, })
|
|
|
60
93
|
// @req REQ-MULTILINE-SUPPORT - Extend value of existing pending annotation across lines
|
|
61
94
|
// @req REQ-AUTOFIX-FORMAT - Maintain complete logical value for downstream validation and fixes
|
|
62
95
|
// @req REQ-MIXED-SUPPORT - Leave non-annotation lines untouched when no pending state exists
|
|
63
|
-
|
|
64
|
-
const continuation = normalized.trim();
|
|
65
|
-
// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
66
|
-
// @req REQ-MULTILINE-SUPPORT - Skip blank continuation lines without altering pending annotation
|
|
67
|
-
if (!continuation) {
|
|
68
|
-
return pending;
|
|
69
|
-
}
|
|
70
|
-
const updatedValue = pending.value
|
|
71
|
-
? `${pending.value} ${continuation}`
|
|
72
|
-
: continuation;
|
|
73
|
-
return {
|
|
74
|
-
...pending,
|
|
75
|
-
value: updatedValue,
|
|
76
|
-
hasValue: pending.hasValue || continuation.length > 0,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return pending;
|
|
96
|
+
return extendPendingAnnotation(normalized, pending);
|
|
80
97
|
}
|
|
81
98
|
/**
|
|
82
99
|
* Process a single comment node and validate any @story/@req/@supports annotations it contains.
|
|
@@ -98,7 +115,7 @@ function processCommentLine({ normalized, pending, context, comment, options, })
|
|
|
98
115
|
* @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
|
|
99
116
|
* @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
|
|
100
117
|
*/
|
|
101
|
-
function
|
|
118
|
+
function processCommentLines({ context, comment, options, }) {
|
|
102
119
|
const rawLines = (comment.value || "").split(/\r?\n/);
|
|
103
120
|
let pending = null;
|
|
104
121
|
rawLines.forEach((rawLine) => {
|
|
@@ -113,6 +130,9 @@ function processComment(context, comment, options) {
|
|
|
113
130
|
});
|
|
114
131
|
(0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
|
|
115
132
|
}
|
|
133
|
+
function processComment(context, comment, options) {
|
|
134
|
+
processCommentLines({ context, comment, options });
|
|
135
|
+
}
|
|
116
136
|
exports.default = {
|
|
117
137
|
meta: {
|
|
118
138
|
type: "problem",
|
|
@@ -95,4 +95,35 @@ function process(value) {
|
|
|
95
95
|
expect(fixedB.output).toContain("@req REQ-PROCESS");
|
|
96
96
|
expect(fixedB.output).not.toContain("@req REQ-PROCESS\n */\n return");
|
|
97
97
|
});
|
|
98
|
+
it("[REQ-CATCH-BLOCK-HANDLING] does not report redundant annotations for try/if/else-if/catch pattern from story 027.0 (regression from issue #6)", async () => {
|
|
99
|
+
const code = `// @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
|
|
100
|
+
// @req REQ-SAFE-ONLY
|
|
101
|
+
async function filterVulnerableVersions(versionInfo, safeVersions) {
|
|
102
|
+
try {
|
|
103
|
+
// @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
|
|
104
|
+
// @req REQ-SAFE-ONLY
|
|
105
|
+
if (!versionInfo) {
|
|
106
|
+
return [];
|
|
107
|
+
} else if (!safeVersions || safeVersions.length === 0) {
|
|
108
|
+
return versionInfo;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
|
|
112
|
+
// @req REQ-SAFE-ONLY
|
|
113
|
+
return versionInfo.filter(v => safeVersions.includes(v));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
|
|
116
|
+
// @req REQ-SAFE-ONLY
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
const config = {
|
|
122
|
+
rules: {
|
|
123
|
+
"traceability/no-redundant-annotation": ["warn"],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const result = await lintTextWithConfig(code, "filter-vulnerable-versions.js", config);
|
|
127
|
+
expect(result.messages.filter((m) => m.ruleId === "traceability/no-redundant-annotation").length).toBe(0);
|
|
128
|
+
});
|
|
98
129
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
* Integration tests for require-traceability with configurable test callback exclusion.
|
|
8
|
+
*
|
|
9
|
+
* @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-UNIFIED-ALIAS-ENGINE
|
|
10
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-FUNCTION-DETECTION
|
|
11
|
+
* @supports docs/stories/013-exclude-test-framework-callbacks.proposed.md REQ-TEST-CALLBACK-EXCLUSION
|
|
12
|
+
*/
|
|
13
|
+
const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
|
|
14
|
+
const index_1 = __importDefault(require("../../src/index"));
|
|
15
|
+
async function lintTextWithConfig(text, filename, extraConfig) {
|
|
16
|
+
const baseConfig = {
|
|
17
|
+
plugins: {
|
|
18
|
+
traceability: index_1.default,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const eslint = new use_at_your_own_risk_1.FlatESLint({
|
|
22
|
+
overrideConfig: [baseConfig, ...extraConfig],
|
|
23
|
+
overrideConfigFile: true,
|
|
24
|
+
ignore: false,
|
|
25
|
+
});
|
|
26
|
+
const [result] = await eslint.lintText(text, { filePath: filename });
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
describe("Unified require-traceability with configurable test callback exclusion (Story 013-exclude-test-framework-callbacks)", () => {
|
|
30
|
+
const baseHeader = `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */`;
|
|
31
|
+
const jsTestCallback = `${baseHeader}\n
|
|
32
|
+
describe('suite', () => {\n it('does something', () => {\n const value = 1;\n });\n});`;
|
|
33
|
+
const tsTestCallback = `${baseHeader}\n
|
|
34
|
+
import { describe, it } from 'vitest';
|
|
35
|
+
|
|
36
|
+
describe('suite', () => {\n it('does something', () => {\n const value = 1;\n });\n});`;
|
|
37
|
+
const jsBenchCallback = `${baseHeader}\n
|
|
38
|
+
import { bench } from 'vitest';
|
|
39
|
+
|
|
40
|
+
bench('bench case', () => {\n function helper() {}\n helper();\n});`;
|
|
41
|
+
const jsCustomHelperCallback = `${baseHeader}\n
|
|
42
|
+
function helperWrapper(fn) {\n return fn;\n}
|
|
43
|
+
|
|
44
|
+
helperWrapper(() => {\n function helper() {}\n helper();\n});`;
|
|
45
|
+
async function getRuleMessages(code, filename, extraConfig) {
|
|
46
|
+
const result = await lintTextWithConfig(code, filename, extraConfig);
|
|
47
|
+
return result.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
48
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
49
|
+
}
|
|
50
|
+
it("[REQ-TEST-CALLBACK-EXCLUSION] excludes callbacks under known test helpers when configured", async () => {
|
|
51
|
+
const config = [
|
|
52
|
+
{
|
|
53
|
+
rules: {
|
|
54
|
+
"traceability/require-traceability": ["error"],
|
|
55
|
+
"traceability/require-story-annotation": [
|
|
56
|
+
"error",
|
|
57
|
+
{
|
|
58
|
+
excludeTestCallbacks: true,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const messagesJs = await getRuleMessages(jsTestCallback, "example.test.js", config);
|
|
65
|
+
const messagesTs = await getRuleMessages(tsTestCallback, "example.test.ts", config);
|
|
66
|
+
expect(messagesJs).toHaveLength(0);
|
|
67
|
+
expect(messagesTs).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
it("[REQ-TEST-CALLBACK-EXCLUSION] never excludes Vitest bench callbacks via test-callback exclusion, even when exclusion is enabled", async () => {
|
|
70
|
+
const baseConfig = [
|
|
71
|
+
{
|
|
72
|
+
rules: {
|
|
73
|
+
"traceability/require-traceability": ["error"],
|
|
74
|
+
"traceability/require-story-annotation": [
|
|
75
|
+
"error",
|
|
76
|
+
{
|
|
77
|
+
excludeTestCallbacks: true,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const withBenchAsHelperConfig = [
|
|
84
|
+
{
|
|
85
|
+
rules: {
|
|
86
|
+
"traceability/require-traceability": ["error"],
|
|
87
|
+
"traceability/require-story-annotation": [
|
|
88
|
+
"error",
|
|
89
|
+
{
|
|
90
|
+
excludeTestCallbacks: true,
|
|
91
|
+
additionalTestHelperNames: ["bench"],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
const baseResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", baseConfig);
|
|
98
|
+
const withBenchHelperResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", withBenchAsHelperConfig);
|
|
99
|
+
const baseMessages = baseResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
100
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
101
|
+
const withBenchHelperMessages = withBenchHelperResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
102
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
103
|
+
expect(withBenchHelperMessages.length).toBeGreaterThanOrEqual(baseMessages.length);
|
|
104
|
+
});
|
|
105
|
+
it("[REQ-TEST-CALLBACK-EXCLUSION] respects additionalTestHelperNames for custom helpers but not for bench callbacks", async () => {
|
|
106
|
+
const baseConfig = [
|
|
107
|
+
{
|
|
108
|
+
rules: {
|
|
109
|
+
"traceability/require-traceability": ["error"],
|
|
110
|
+
"traceability/require-story-annotation": [
|
|
111
|
+
"error",
|
|
112
|
+
{
|
|
113
|
+
excludeTestCallbacks: true,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
const withAdditionalHelpersConfig = [
|
|
120
|
+
{
|
|
121
|
+
rules: {
|
|
122
|
+
"traceability/require-traceability": ["error"],
|
|
123
|
+
"traceability/require-story-annotation": [
|
|
124
|
+
"error",
|
|
125
|
+
{
|
|
126
|
+
excludeTestCallbacks: true,
|
|
127
|
+
additionalTestHelperNames: ["helperWrapper", "bench"],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
const wrapperBaseResult = await lintTextWithConfig(jsCustomHelperCallback, "helper-wrapper.test.ts", baseConfig);
|
|
134
|
+
const wrapperWithHelpersResult = await lintTextWithConfig(jsCustomHelperCallback, "helper-wrapper.test.ts", withAdditionalHelpersConfig);
|
|
135
|
+
const benchBaseResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", baseConfig);
|
|
136
|
+
const benchWithHelpersResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", withAdditionalHelpersConfig);
|
|
137
|
+
const wrapperBaseMessages = wrapperBaseResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
138
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
139
|
+
const wrapperWithHelpersMessages = wrapperWithHelpersResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
140
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
141
|
+
const benchBaseMessages = benchBaseResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
142
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
143
|
+
const benchWithHelpersMessages = benchWithHelpersResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
|
|
144
|
+
m.ruleId === "traceability/require-story-annotation");
|
|
145
|
+
expect(wrapperWithHelpersMessages.length).toBeLessThanOrEqual(wrapperBaseMessages.length);
|
|
146
|
+
expect(benchWithHelpersMessages.length).toBeGreaterThanOrEqual(benchBaseMessages.length);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -77,8 +77,8 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
|
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
it("[REQ-MAINT-DETECT] handles permission denied errors by returning an empty result", () => {
|
|
80
|
-
const
|
|
81
|
-
const dir = path.join(
|
|
80
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
|
|
81
|
+
const dir = path.join(tmpDir, "subdir");
|
|
82
82
|
fs.mkdirSync(dir);
|
|
83
83
|
const filePath = path.join(dir, "file.ts");
|
|
84
84
|
const content = `
|
|
@@ -87,24 +87,32 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
|
|
|
87
87
|
*/
|
|
88
88
|
`;
|
|
89
89
|
fs.writeFileSync(filePath, content, "utf8");
|
|
90
|
-
|
|
90
|
+
const originalReadFileSync = fs.readFileSync;
|
|
91
|
+
const readSpy = jest
|
|
92
|
+
.spyOn(fs, "readFileSync")
|
|
93
|
+
.mockImplementation((p, ...args) => {
|
|
94
|
+
const strPath = typeof p === "string" ? p : p.toString();
|
|
95
|
+
if (strPath === filePath) {
|
|
96
|
+
const err = new Error("EACCES: permission denied, open");
|
|
97
|
+
err.code = "EACCES";
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
// Delegate to original implementation for all other paths
|
|
101
|
+
// to keep behavior realistic.
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
return originalReadFileSync(p, ...args);
|
|
104
|
+
});
|
|
91
105
|
try {
|
|
92
|
-
|
|
93
|
-
expect(
|
|
106
|
+
const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
|
|
107
|
+
expect(result).toEqual([]);
|
|
94
108
|
}
|
|
95
109
|
finally {
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
fs.chmodSync(dir, 0o700);
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// ignore
|
|
102
|
-
}
|
|
110
|
+
readSpy.mockRestore();
|
|
103
111
|
try {
|
|
104
|
-
fs.rmSync(
|
|
112
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
105
113
|
}
|
|
106
114
|
catch {
|
|
107
|
-
// ignore
|
|
115
|
+
// ignore cleanup errors
|
|
108
116
|
}
|
|
109
117
|
}
|
|
110
118
|
});
|
|
@@ -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,10 @@ 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
|
+
},
|
|
40
45
|
],
|
|
41
46
|
invalid: [
|
|
42
47
|
{
|
|
@@ -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.17.
|
|
3
|
+
"version": "1.17.1",
|
|
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",
|