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
|
@@ -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",
|
|
@@ -10,6 +10,12 @@ export declare const DEFAULT_BRANCH_TYPES: readonly ["IfStatement", "SwitchCase"
|
|
|
10
10
|
* Type for branch nodes supported by require-branch-annotation rule.
|
|
11
11
|
*/
|
|
12
12
|
export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Placement options for branch annotations relative to their associated branch.
|
|
15
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
16
|
+
* @req REQ-PLACEMENT-CONFIG - Allow configuration of annotation placement (before/inside)
|
|
17
|
+
*/
|
|
18
|
+
export type AnnotationPlacement = "before" | "inside";
|
|
13
19
|
/**
|
|
14
20
|
* Validate branchTypes configuration option and return branch types to enforce,
|
|
15
21
|
* or return an ESLint listener if configuration is invalid.
|
|
@@ -33,8 +39,9 @@ export declare function scanCommentLinesInRange(lines: string[], startIndex: num
|
|
|
33
39
|
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
34
40
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
35
41
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
42
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
36
43
|
*/
|
|
37
|
-
export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any): string;
|
|
44
|
+
export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any, _annotationPlacement?: AnnotationPlacement): string;
|
|
38
45
|
/**
|
|
39
46
|
* Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
|
|
40
47
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -316,8 +316,9 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
|
|
|
316
316
|
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
317
317
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
318
318
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
319
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
319
320
|
*/
|
|
320
|
-
function gatherBranchCommentText(sourceCode, node, parent) {
|
|
321
|
+
function gatherBranchCommentText(sourceCode, node, parent, _annotationPlacement = "before") {
|
|
321
322
|
/**
|
|
322
323
|
* Conditional branch for SwitchCase nodes that may include inline comments.
|
|
323
324
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -5,6 +5,7 @@ import type { Rule } from "eslint";
|
|
|
5
5
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
6
6
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
7
7
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
8
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
8
9
|
*/
|
|
9
10
|
export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
|
|
10
11
|
count: number;
|
|
@@ -22,8 +22,11 @@ function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
|
|
|
22
22
|
});
|
|
23
23
|
return { indent, insertPos };
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
27
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
28
|
+
*/
|
|
29
|
+
function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement) {
|
|
27
30
|
let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
|
|
28
31
|
if (node.type === "CatchClause" && node.body) {
|
|
29
32
|
const bodyNode = node.body;
|
|
@@ -50,19 +53,24 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
|
50
53
|
return { indent, insertPos };
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
|
-
* Compute
|
|
56
|
+
* Compute which annotations are missing for a branch based on its gathered comment text.
|
|
54
57
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
55
|
-
* @
|
|
56
|
-
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
57
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
58
|
-
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
58
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
59
59
|
*/
|
|
60
|
-
function
|
|
61
|
-
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
|
|
60
|
+
function getBranchMissingFlags(sourceCode, node, parent, annotationPlacement) {
|
|
61
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent, annotationPlacement);
|
|
62
62
|
const hasSupports = /@supports\b/.test(text);
|
|
63
63
|
const missingStory = !/@story\b/.test(text) && !hasSupports;
|
|
64
64
|
const missingReq = !/@req\b/.test(text) && !hasSupports;
|
|
65
|
-
|
|
65
|
+
return { missingStory, missingReq };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compute indentation and insert position used for auto-fix insertion on a branch.
|
|
69
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
70
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
71
|
+
*/
|
|
72
|
+
function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement) {
|
|
73
|
+
let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
|
|
66
74
|
if (node.type === "IfStatement" &&
|
|
67
75
|
parent &&
|
|
68
76
|
parent.type === "IfStatement" &&
|
|
@@ -76,6 +84,20 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
|
76
84
|
indent = commentLineInfo.indent;
|
|
77
85
|
insertPos = commentLineInfo.insertPos;
|
|
78
86
|
}
|
|
87
|
+
return { indent, insertPos };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Compute annotation-related metadata for a branch node.
|
|
91
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
92
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
93
|
+
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
94
|
+
* @supports REQ-DUAL-POSITION-DETECTION
|
|
95
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
96
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
97
|
+
*/
|
|
98
|
+
function getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement) {
|
|
99
|
+
const { missingStory, missingReq } = getBranchMissingFlags(sourceCode, node, parent, annotationPlacement);
|
|
100
|
+
const { indent, insertPos } = getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement);
|
|
79
101
|
return { missingStory, missingReq, indent, insertPos };
|
|
80
102
|
}
|
|
81
103
|
/**
|
|
@@ -84,11 +106,18 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
|
84
106
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
85
107
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
86
108
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
109
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
87
110
|
*/
|
|
88
111
|
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
89
112
|
const sourceCode = context.getSourceCode();
|
|
113
|
+
const rawOptions = context.options && context.options[0];
|
|
114
|
+
const annotationPlacement = rawOptions &&
|
|
115
|
+
(rawOptions.annotationPlacement === "inside" ||
|
|
116
|
+
rawOptions.annotationPlacement === "before")
|
|
117
|
+
? rawOptions.annotationPlacement
|
|
118
|
+
: "before";
|
|
90
119
|
const parent = node.parent;
|
|
91
|
-
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
|
|
120
|
+
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement);
|
|
92
121
|
const actions = [
|
|
93
122
|
{
|
|
94
123
|
missing: missingStory,
|
|
@@ -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
|
});
|