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.
Files changed (34) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +107 -11
  3. package/lib/src/index.js +53 -33
  4. package/lib/src/maintenance/commands.d.ts +4 -0
  5. package/lib/src/maintenance/commands.js +4 -0
  6. package/lib/src/maintenance/index.d.ts +1 -0
  7. package/lib/src/maintenance/index.js +1 -0
  8. package/lib/src/maintenance/report.js +2 -2
  9. package/lib/src/maintenance/update.js +4 -2
  10. package/lib/src/rules/helpers/test-callback-exclusion.d.ts +5 -1
  11. package/lib/src/rules/helpers/test-callback-exclusion.js +2 -11
  12. package/lib/src/rules/helpers/valid-annotation-format-validators.js +8 -2
  13. package/lib/src/rules/no-redundant-annotation.js +4 -0
  14. package/lib/src/rules/prefer-implements-annotation.js +25 -20
  15. package/lib/src/rules/require-branch-annotation.js +16 -0
  16. package/lib/src/rules/valid-annotation-format.js +62 -42
  17. package/lib/src/utils/branch-annotation-helpers.d.ts +8 -1
  18. package/lib/src/utils/branch-annotation-helpers.js +2 -1
  19. package/lib/src/utils/branch-annotation-report-helpers.d.ts +1 -0
  20. package/lib/src/utils/branch-annotation-report-helpers.js +40 -11
  21. package/lib/tests/integration/no-redundant-annotation.integration.test.js +31 -0
  22. package/lib/tests/integration/require-traceability-test-callbacks.integration.test.d.ts +1 -0
  23. package/lib/tests/integration/require-traceability-test-callbacks.integration.test.js +148 -0
  24. package/lib/tests/maintenance/detect-isolated.test.js +22 -14
  25. package/lib/tests/perf/maintenance-cli-large-workspace.test.js +145 -64
  26. package/lib/tests/perf/maintenance-large-workspace.test.js +65 -46
  27. package/lib/tests/rules/no-redundant-annotation.test.js +15 -0
  28. package/lib/tests/rules/require-branch-annotation.test.js +18 -0
  29. package/lib/tests/utils/{annotation-checker-branches.test.d.ts → annotation-checker-autofix-behavior.test.d.ts} +1 -1
  30. package/lib/tests/utils/{annotation-checker-branches.test.js → annotation-checker-autofix-behavior.test.js} +2 -2
  31. package/package.json +2 -2
  32. package/user-docs/api-reference.md +6 -1
  33. package/user-docs/examples.md +32 -0
  34. 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 isStory = /@story\b/.test(normalized);
26
- const isReq = /@req\b/.test(normalized);
27
- const isImplements = /@supports\b/.test(normalized);
28
- // Handle @supports as an immediate, single-line annotation
29
- // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
30
- // @req REQ-IMPLEMENTS-PARSE - Immediately validate @supports without starting multi-line state
31
- if (isImplements) {
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
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
37
- // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
38
- // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
39
- // @req REQ-SYNTAX-VALIDATION - Start new pending annotation when a tag is found
40
- // @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
41
- // @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
42
- if (isStory || isReq) {
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
- if (pending) {
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 processComment(context, comment, options) {
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
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
26
- function getBaseBranchIndentAndInsertPos(sourceCode, node) {
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 annotation-related metadata for a branch node.
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
- * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
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 getBranchAnnotationInfo(sourceCode, node, parent) {
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
- let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
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,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 tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
81
- const dir = path.join(tmpDir2, "subdir");
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
- // Remove read permission
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
- fs.chmodSync(dir, 0o000);
93
- expect(() => (0, detect_1.detectStaleAnnotations)(tmpDir2)).toThrow();
106
+ const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
107
+ expect(result).toEqual([]);
94
108
  }
95
109
  finally {
96
- // Restore permissions and cleanup temporary directory, ignoring errors during cleanup
97
- try {
98
- fs.chmodSync(dir, 0o700);
99
- }
100
- catch {
101
- // ignore
102
- }
110
+ readSpy.mockRestore();
103
111
  try {
104
- fs.rmSync(tmpDir2, { recursive: true, force: true });
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
  });