eslint-plugin-traceability 1.16.1 → 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 +2 -2
- 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/require-story-helpers.d.ts +5 -11
- package/lib/src/rules/helpers/require-story-helpers.js +7 -74
- package/lib/src/rules/helpers/test-callback-exclusion.d.ts +43 -0
- package/lib/src/rules/helpers/test-callback-exclusion.js +100 -0
- 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-story-annotation.js +14 -1
- 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/rules/require-story-annotation.test.js +21 -0
- package/lib/tests/rules/require-story-helpers.test.js +69 -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
|
@@ -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
|
/**
|
|
@@ -49,6 +49,11 @@ const rule = {
|
|
|
49
49
|
methodAnnotationTemplate: { type: "string" },
|
|
50
50
|
autoFix: { type: "boolean" },
|
|
51
51
|
excludeTestCallbacks: { type: "boolean" },
|
|
52
|
+
additionalTestHelperNames: {
|
|
53
|
+
type: "array",
|
|
54
|
+
items: { type: "string" },
|
|
55
|
+
uniqueItems: true,
|
|
56
|
+
},
|
|
52
57
|
},
|
|
53
58
|
additionalProperties: false,
|
|
54
59
|
},
|
|
@@ -79,6 +84,10 @@ const rule = {
|
|
|
79
84
|
const excludeTestCallbacks = typeof opts.excludeTestCallbacks === "boolean"
|
|
80
85
|
? opts.excludeTestCallbacks
|
|
81
86
|
: true;
|
|
87
|
+
const additionalTestHelperNames = Array.isArray(opts.additionalTestHelperNames) &&
|
|
88
|
+
opts.additionalTestHelperNames.every((name) => typeof name === "string")
|
|
89
|
+
? opts.additionalTestHelperNames
|
|
90
|
+
: undefined;
|
|
82
91
|
/**
|
|
83
92
|
* Optional debug logging for troubleshooting this rule.
|
|
84
93
|
* Developers can temporarily uncomment the block below to log when the rule
|
|
@@ -94,7 +103,10 @@ const rule = {
|
|
|
94
103
|
// : "<unknown>",
|
|
95
104
|
// );
|
|
96
105
|
// Local closure that binds configured scope and export priority to the helper.
|
|
97
|
-
const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, {
|
|
106
|
+
const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, {
|
|
107
|
+
excludeTestCallbacks,
|
|
108
|
+
additionalTestHelperNames,
|
|
109
|
+
});
|
|
98
110
|
// Delegate visitor construction to helper to keep this file concise.
|
|
99
111
|
return (0, require_story_visitors_1.buildVisitors)(context, sourceCode, {
|
|
100
112
|
shouldProcessNode: should,
|
|
@@ -104,6 +116,7 @@ const rule = {
|
|
|
104
116
|
methodAnnotationTemplate,
|
|
105
117
|
autoFix,
|
|
106
118
|
excludeTestCallbacks,
|
|
119
|
+
additionalTestHelperNames,
|
|
107
120
|
});
|
|
108
121
|
},
|
|
109
122
|
};
|
|
@@ -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
|
});
|