eslint-plugin-traceability 1.19.2 → 1.19.3
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/utils/branch-annotation-helpers.js +59 -21
- package/lib/src/utils/branch-annotation-switch-helpers.d.ts +11 -0
- package/lib/src/utils/branch-annotation-switch-helpers.js +68 -0
- package/lib/tests/integration/annotation-placement-inside-prettier.integration.test.d.ts +1 -0
- package/lib/tests/integration/annotation-placement-inside-prettier.integration.test.js +132 -0
- package/lib/tests/integration/catch-annotation-prettier.integration.test.js +4 -15
- package/lib/tests/integration/else-if-annotation-prettier.integration.test.js +3 -14
- package/lib/tests/integration/prettier-test-helpers.d.ts +9 -0
- package/lib/tests/integration/prettier-test-helpers.js +34 -0
- package/lib/tests/rules/require-branch-annotation.test.js +93 -14
- package/lib/tests/utils/branch-annotation-helpers.test.js +156 -9
- package/package.json +1 -1
- package/user-docs/api-reference.md +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
## [1.19.
|
|
1
|
+
## [1.19.3](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.2...v1.19.3) (2025-12-18)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* support inside placement for switch cases in branch helpers ([3fd08d1](https://github.com/voder-ai/eslint-plugin-traceability/commit/3fd08d1314085a7aa9894f6248dbb7f42a07bda0))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
@@ -10,7 +10,7 @@ const branch_annotation_report_helpers_1 = require("./branch-annotation-report-h
|
|
|
10
10
|
Object.defineProperty(exports, "reportMissingAnnotations", { enumerable: true, get: function () { return branch_annotation_report_helpers_1.reportMissingAnnotations; } });
|
|
11
11
|
const branch_annotation_loop_helpers_1 = require("./branch-annotation-loop-helpers");
|
|
12
12
|
const branch_annotation_if_helpers_1 = require("./branch-annotation-if-helpers");
|
|
13
|
-
const
|
|
13
|
+
const branch_annotation_switch_helpers_1 = require("./branch-annotation-switch-helpers");
|
|
14
14
|
/**
|
|
15
15
|
* Valid branch types for require-branch-annotation rule.
|
|
16
16
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -179,6 +179,30 @@ function getInsideCatchCommentText(sourceCode, node) {
|
|
|
179
179
|
}
|
|
180
180
|
return "";
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Gather comment text from the first contiguous comment lines inside a TryStatement block body.
|
|
184
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
185
|
+
*/
|
|
186
|
+
function getInsideTryBlockCommentText(sourceCode, node) {
|
|
187
|
+
const block = node && node.block;
|
|
188
|
+
if (!block ||
|
|
189
|
+
block.type !== "BlockStatement" ||
|
|
190
|
+
!block.loc ||
|
|
191
|
+
!block.loc.start ||
|
|
192
|
+
!block.loc.end ||
|
|
193
|
+
typeof block.loc.start.line !== "number" ||
|
|
194
|
+
typeof block.loc.end.line !== "number") {
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
const lines = sourceCode.lines;
|
|
198
|
+
const startIndex = block.loc.start.line - 1;
|
|
199
|
+
const endIndex = block.loc.end.line - 1;
|
|
200
|
+
const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
|
|
201
|
+
if (insideText) {
|
|
202
|
+
return insideText;
|
|
203
|
+
}
|
|
204
|
+
return "";
|
|
205
|
+
}
|
|
182
206
|
/**
|
|
183
207
|
* Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
|
|
184
208
|
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
@@ -253,17 +277,33 @@ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, before
|
|
|
253
277
|
}
|
|
254
278
|
return "";
|
|
255
279
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
280
|
+
function handleTryCatchBranch(sourceCode, node, context) {
|
|
281
|
+
const { annotationPlacement, beforeText } = context;
|
|
282
|
+
if (node.type === "TryStatement") {
|
|
283
|
+
if (annotationPlacement === "inside") {
|
|
284
|
+
const insideText = getInsideTryBlockCommentText(sourceCode, node);
|
|
285
|
+
if (insideText) {
|
|
286
|
+
return insideText;
|
|
287
|
+
}
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
return beforeText;
|
|
265
291
|
}
|
|
266
|
-
|
|
292
|
+
if (node.type === "CatchClause") {
|
|
293
|
+
return gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText);
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function handleLoopBranch(sourceCode, node, context) {
|
|
298
|
+
const { annotationPlacement, beforeText } = context;
|
|
299
|
+
if (node.type === "ForStatement" ||
|
|
300
|
+
node.type === "ForInStatement" ||
|
|
301
|
+
node.type === "ForOfStatement" ||
|
|
302
|
+
node.type === "WhileStatement" ||
|
|
303
|
+
node.type === "DoWhileStatement") {
|
|
304
|
+
return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, annotationPlacement, beforeText);
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
267
307
|
}
|
|
268
308
|
/**
|
|
269
309
|
* Helper that gathers comment text for non-IfStatement branch types using
|
|
@@ -274,19 +314,17 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
|
|
|
274
314
|
* @supports REQ-PLACEMENT-CONFIG
|
|
275
315
|
*/
|
|
276
316
|
function gatherNonIfBranchCommentText(sourceCode, node, context) {
|
|
277
|
-
const { annotationPlacement, beforeText } = context;
|
|
278
317
|
if (node.type === "SwitchCase") {
|
|
279
|
-
|
|
318
|
+
const { annotationPlacement, beforeText } = context;
|
|
319
|
+
return (0, branch_annotation_switch_helpers_1.gatherSwitchCaseCommentText)(sourceCode, node, annotationPlacement, beforeText);
|
|
280
320
|
}
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
const tryCatchResult = handleTryCatchBranch(sourceCode, node, context);
|
|
322
|
+
if (tryCatchResult != null) {
|
|
323
|
+
return tryCatchResult;
|
|
283
324
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
node.type === "WhileStatement" ||
|
|
288
|
-
node.type === "DoWhileStatement") {
|
|
289
|
-
return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, annotationPlacement, beforeText);
|
|
325
|
+
const loopResult = handleLoopBranch(sourceCode, node, context);
|
|
326
|
+
if (loopResult != null) {
|
|
327
|
+
return loopResult;
|
|
290
328
|
}
|
|
291
329
|
return null;
|
|
292
330
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
import { type AnnotationPlacement } from "./branch-annotation-helpers";
|
|
3
|
+
/**
|
|
4
|
+
* Gather annotation text for SwitchCase branches, honoring the configured placement
|
|
5
|
+
* while preserving legacy before-branch behavior in the default mode.
|
|
6
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
7
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
8
|
+
* @supports REQ-PLACEMENT-CONFIG
|
|
9
|
+
* @supports REQ-INSIDE-BRACE-PLACEMENT
|
|
10
|
+
*/
|
|
11
|
+
export declare function gatherSwitchCaseCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, annotationPlacement: AnnotationPlacement, beforeText: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gatherSwitchCaseCommentText = gatherSwitchCaseCommentText;
|
|
4
|
+
const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
|
|
5
|
+
/**
|
|
6
|
+
* Gather comment text from the first contiguous comment lines "inside" a SwitchCase body.
|
|
7
|
+
* Prefers a BlockStatement consequent when present, with a fallback to the entire case range.
|
|
8
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
9
|
+
* @supports REQ-INSIDE-BRACE-PLACEMENT
|
|
10
|
+
* @supports REQ-PLACEMENT-CONFIG
|
|
11
|
+
*/
|
|
12
|
+
function getInsideSwitchCaseCommentText(sourceCode, node) {
|
|
13
|
+
const lines = sourceCode.lines;
|
|
14
|
+
const firstConsequent = node.consequent && node.consequent[0];
|
|
15
|
+
if (firstConsequent &&
|
|
16
|
+
firstConsequent.type === "BlockStatement" &&
|
|
17
|
+
firstConsequent.loc &&
|
|
18
|
+
firstConsequent.loc.start &&
|
|
19
|
+
firstConsequent.loc.end &&
|
|
20
|
+
typeof firstConsequent.loc.start.line === "number" &&
|
|
21
|
+
typeof firstConsequent.loc.end.line === "number") {
|
|
22
|
+
const startIndex = firstConsequent.loc.start.line - 1;
|
|
23
|
+
const endIndex = firstConsequent.loc.end.line - 1;
|
|
24
|
+
const insideText = (0, branch_annotation_helpers_1.scanCommentLinesInRange)(lines, startIndex + 1, endIndex);
|
|
25
|
+
if (insideText) {
|
|
26
|
+
return insideText;
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
if (node.loc &&
|
|
31
|
+
node.loc.start &&
|
|
32
|
+
node.loc.end &&
|
|
33
|
+
typeof node.loc.start.line === "number" &&
|
|
34
|
+
typeof node.loc.end.line === "number") {
|
|
35
|
+
const startIndex = node.loc.start.line - 1;
|
|
36
|
+
const endIndex = node.loc.end.line - 1;
|
|
37
|
+
const insideText = (0, branch_annotation_helpers_1.scanCommentLinesInRange)(lines, startIndex + 1, endIndex);
|
|
38
|
+
if (insideText) {
|
|
39
|
+
return insideText;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Gather annotation text for SwitchCase branches, honoring the configured placement
|
|
46
|
+
* while preserving legacy before-branch behavior in the default mode.
|
|
47
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
48
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
49
|
+
* @supports REQ-PLACEMENT-CONFIG
|
|
50
|
+
* @supports REQ-INSIDE-BRACE-PLACEMENT
|
|
51
|
+
*/
|
|
52
|
+
function gatherSwitchCaseCommentText(sourceCode, node, annotationPlacement, beforeText) {
|
|
53
|
+
if (annotationPlacement === "inside") {
|
|
54
|
+
const insideText = getInsideSwitchCaseCommentText(sourceCode, node);
|
|
55
|
+
if (insideText) {
|
|
56
|
+
return insideText;
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
if (/@story\b/.test(beforeText) ||
|
|
61
|
+
/@req\b/.test(beforeText) ||
|
|
62
|
+
/@supports\b/.test(beforeText)) {
|
|
63
|
+
return beforeText;
|
|
64
|
+
}
|
|
65
|
+
// In before-placement mode, rely on the caller's beforeText and any
|
|
66
|
+
// configured PRE_COMMENT_OFFSET logic in the main helpers module.
|
|
67
|
+
return beforeText;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
* Prettier integration tests for annotationPlacement: "inside" across multiple branch types.
|
|
8
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
9
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PRETTIER-STABLE REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
10
|
+
*/
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
const prettier_test_helpers_1 = require("./prettier-test-helpers");
|
|
14
|
+
describe("annotationPlacement: 'inside' with Prettier (Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION)", () => {
|
|
15
|
+
const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
|
|
16
|
+
const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
|
|
17
|
+
const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
|
|
18
|
+
function buildInsidePlacementArgs(stdinFilename) {
|
|
19
|
+
return [
|
|
20
|
+
"--no-config-lookup",
|
|
21
|
+
"--config",
|
|
22
|
+
configPath,
|
|
23
|
+
"--stdin",
|
|
24
|
+
"--stdin-filename",
|
|
25
|
+
stdinFilename,
|
|
26
|
+
"--rule",
|
|
27
|
+
"no-unused-vars:off",
|
|
28
|
+
"--rule",
|
|
29
|
+
"no-magic-numbers:off",
|
|
30
|
+
"--rule",
|
|
31
|
+
"no-undef:off",
|
|
32
|
+
"--rule",
|
|
33
|
+
"no-console:off",
|
|
34
|
+
"--rule",
|
|
35
|
+
'traceability/require-branch-annotation:["error",{"annotationPlacement":"inside"}]',
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function runEslintWithInsidePlacement(code, _filename) {
|
|
39
|
+
// Pin stdin filename to a tsconfig-included path to satisfy @typescript-eslint/parser's project lookup in these integration tests.
|
|
40
|
+
const args = buildInsidePlacementArgs("src/annotation-placement-inside.ts");
|
|
41
|
+
return (0, child_process_1.spawnSync)(process.execPath, [eslintCliPath, ...args], {
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
input: code,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function formatWithPrettier(source) {
|
|
47
|
+
return (0, prettier_test_helpers_1.formatWithPrettier)(source, { parser: "typescript" });
|
|
48
|
+
}
|
|
49
|
+
it("[REQ-PRETTIER-STABLE][REQ-INSIDE-BRACE-PLACEMENT] accepts formatted code with inside-brace annotations for if/else and loops", () => {
|
|
50
|
+
const original = `
|
|
51
|
+
function demo(value: number) {
|
|
52
|
+
if (value > 0) {
|
|
53
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
54
|
+
// @req REQ-IF-INSIDE
|
|
55
|
+
console.log('positive');
|
|
56
|
+
} else if (value < 0) {
|
|
57
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
58
|
+
// @req REQ-ELSE-IF-INSIDE
|
|
59
|
+
console.log('negative');
|
|
60
|
+
} else {
|
|
61
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
62
|
+
// @req REQ-ELSE-INSIDE
|
|
63
|
+
console.log('zero');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const item of [1, 2, 3]) {
|
|
67
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
68
|
+
// @req REQ-LOOP-INSIDE
|
|
69
|
+
console.log(item);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
const formatted = formatWithPrettier(original);
|
|
74
|
+
const result = runEslintWithInsidePlacement(formatted, "annotation-placement-inside-if-loop.ts");
|
|
75
|
+
expect(result.stdout).not.toContain("traceability/require-branch-annotation");
|
|
76
|
+
expect([0, 1]).toContain(result.status);
|
|
77
|
+
});
|
|
78
|
+
it("[REQ-PRETTIER-STABLE][REQ-INSIDE-BRACE-PLACEMENT] accepts formatted code with inside-brace annotations for try/finally and catch", () => {
|
|
79
|
+
const original = `
|
|
80
|
+
function demoTry(flag: boolean) {
|
|
81
|
+
try {
|
|
82
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
83
|
+
// @req REQ-TRY-INSIDE
|
|
84
|
+
if (flag) {
|
|
85
|
+
throw new Error('boom');
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
89
|
+
// @req REQ-CATCH-INSIDE
|
|
90
|
+
console.error(error);
|
|
91
|
+
} finally {
|
|
92
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
93
|
+
// @req REQ-FINALLY-INSIDE
|
|
94
|
+
console.log('cleanup');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
const formatted = formatWithPrettier(original);
|
|
99
|
+
const result = runEslintWithInsidePlacement(formatted, "annotation-placement-inside-try.ts");
|
|
100
|
+
expect(result.stdout).not.toContain("traceability/require-branch-annotation");
|
|
101
|
+
expect([0, 1]).toContain(result.status);
|
|
102
|
+
});
|
|
103
|
+
it("[REQ-PRETTIER-STABLE][REQ-INSIDE-BRACE-PLACEMENT] accepts formatted code with inside-brace annotations for switch cases", () => {
|
|
104
|
+
const original = `
|
|
105
|
+
function demoSwitch(status: 'pending' | 'done' | 'other') {
|
|
106
|
+
switch (status) {
|
|
107
|
+
case 'pending': {
|
|
108
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
109
|
+
// @req REQ-SWITCH-PENDING-INSIDE
|
|
110
|
+
console.log('pending');
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'done': {
|
|
114
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
115
|
+
// @req REQ-SWITCH-DONE-INSIDE
|
|
116
|
+
console.log('done');
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
default: {
|
|
120
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
121
|
+
// @req REQ-SWITCH-DEFAULT-INSIDE
|
|
122
|
+
console.log('other');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
const formatted = formatWithPrettier(original);
|
|
128
|
+
const result = runEslintWithInsidePlacement(formatted, "annotation-placement-inside-switch.ts");
|
|
129
|
+
expect(result.stdout).not.toContain("traceability/require-branch-annotation");
|
|
130
|
+
expect([0, 1]).toContain(result.status);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -10,12 +10,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
*/
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
const prettier_test_helpers_1 = require("./prettier-test-helpers");
|
|
13
14
|
describe("CatchClause annotations with Prettier (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
|
|
14
15
|
const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
|
|
15
16
|
const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
|
|
16
17
|
const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
|
|
17
|
-
const prettierPackageJson = require.resolve("prettier/package.json");
|
|
18
|
-
const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
|
|
19
18
|
function runEslintWithRequireBranchAnnotation(code) {
|
|
20
19
|
const args = [
|
|
21
20
|
"--no-config-lookup",
|
|
@@ -40,16 +39,6 @@ describe("CatchClause annotations with Prettier (Story 025.0-DEV-CATCH-ANNOTATIO
|
|
|
40
39
|
input: code,
|
|
41
40
|
});
|
|
42
41
|
}
|
|
43
|
-
function formatWithPrettier(source) {
|
|
44
|
-
const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
|
|
45
|
-
encoding: "utf-8",
|
|
46
|
-
input: source,
|
|
47
|
-
});
|
|
48
|
-
if (result.status !== 0) {
|
|
49
|
-
throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
|
|
50
|
-
}
|
|
51
|
-
return result.stdout;
|
|
52
|
-
}
|
|
53
42
|
it("[REQ-PRETTIER-COMPATIBILITY-BEFORE] accepts code where annotations start before catch but are moved inside by Prettier", () => {
|
|
54
43
|
const original = `
|
|
55
44
|
function doSomething() {
|
|
@@ -71,7 +60,7 @@ catch (error) {
|
|
|
71
60
|
handleError(error);
|
|
72
61
|
}
|
|
73
62
|
`;
|
|
74
|
-
const formatted = formatWithPrettier(original);
|
|
63
|
+
const formatted = (0, prettier_test_helpers_1.formatWithPrettier)(original);
|
|
75
64
|
// Sanity check: Prettier should move the branch annotations inside the catch body.
|
|
76
65
|
expect(formatted).toContain("catch (error) {");
|
|
77
66
|
const catchIndex = formatted.indexOf("catch (error) {");
|
|
@@ -100,7 +89,7 @@ try {
|
|
|
100
89
|
handleError(error);
|
|
101
90
|
}
|
|
102
91
|
`;
|
|
103
|
-
const formatted = formatWithPrettier(original);
|
|
92
|
+
const formatted = (0, prettier_test_helpers_1.formatWithPrettier)(original);
|
|
104
93
|
// Sanity: annotations should still be associated with the catch body after formatting.
|
|
105
94
|
expect(formatted).toContain("catch (error) {");
|
|
106
95
|
const catchIndex = formatted.indexOf("catch (error) {");
|
|
@@ -124,7 +113,7 @@ try {
|
|
|
124
113
|
// @req REQ-CATCH-EMPTY
|
|
125
114
|
}
|
|
126
115
|
`;
|
|
127
|
-
const formatted = formatWithPrettier(original);
|
|
116
|
+
const formatted = (0, prettier_test_helpers_1.formatWithPrettier)(original);
|
|
128
117
|
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
129
118
|
expect(result.status).toBe(0);
|
|
130
119
|
});
|
|
@@ -10,12 +10,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
*/
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
const prettier_test_helpers_1 = require("./prettier-test-helpers");
|
|
13
14
|
describe("Else-if annotations with Prettier (Story 026.0-DEV-ELSE-IF-ANNOTATION-POSITION)", () => {
|
|
14
15
|
const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
|
|
15
16
|
const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
|
|
16
17
|
const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
|
|
17
|
-
const prettierPackageJson = require.resolve("prettier/package.json");
|
|
18
|
-
const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
|
|
19
18
|
function runEslintWithRequireBranchAnnotation(code) {
|
|
20
19
|
const args = [
|
|
21
20
|
"--no-config-lookup",
|
|
@@ -40,16 +39,6 @@ describe("Else-if annotations with Prettier (Story 026.0-DEV-ELSE-IF-ANNOTATION-
|
|
|
40
39
|
input: code,
|
|
41
40
|
});
|
|
42
41
|
}
|
|
43
|
-
function formatWithPrettier(source) {
|
|
44
|
-
const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
|
|
45
|
-
encoding: "utf-8",
|
|
46
|
-
input: source,
|
|
47
|
-
});
|
|
48
|
-
if (result.status !== 0) {
|
|
49
|
-
throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
|
|
50
|
-
}
|
|
51
|
-
return result.stdout;
|
|
52
|
-
}
|
|
53
42
|
it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
|
|
54
43
|
const original = `
|
|
55
44
|
function doA() {
|
|
@@ -71,7 +60,7 @@ else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
|
71
60
|
doB();
|
|
72
61
|
}
|
|
73
62
|
`;
|
|
74
|
-
const formatted = formatWithPrettier(original);
|
|
63
|
+
const formatted = (0, prettier_test_helpers_1.formatWithPrettier)(original);
|
|
75
64
|
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
76
65
|
// but the exact layout and comment movement may vary between versions.
|
|
77
66
|
expect(formatted).toContain("else if");
|
|
@@ -101,7 +90,7 @@ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherConditio
|
|
|
101
90
|
doB();
|
|
102
91
|
}
|
|
103
92
|
`;
|
|
104
|
-
const formatted = formatWithPrettier(original);
|
|
93
|
+
const formatted = (0, prettier_test_helpers_1.formatWithPrettier)(original);
|
|
105
94
|
// Note: Prettier's exact layout of the else-if and its comments may differ between versions;
|
|
106
95
|
// the rule should accept any of the supported annotation positions regardless of formatting.
|
|
107
96
|
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface FormatOptions {
|
|
2
|
+
parser?: "babel" | "typescript" | "babel-ts" | "espree" | string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Format arbitrary source with Prettier using the installed CLI binary.
|
|
6
|
+
* Defaults to the TypeScript parser when none is provided.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatWithPrettier(source: string, options?: FormatOptions): string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
exports.formatWithPrettier = formatWithPrettier;
|
|
7
|
+
/**
|
|
8
|
+
* Shared helpers for Prettier-based integration tests.
|
|
9
|
+
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
10
|
+
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
11
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
12
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-COMPATIBILITY
|
|
13
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-PRETTIER-AUTOFIX-ELSE-IF
|
|
14
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PRETTIER-STABLE
|
|
15
|
+
*/
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
/**
|
|
19
|
+
* Format arbitrary source with Prettier using the installed CLI binary.
|
|
20
|
+
* Defaults to the TypeScript parser when none is provided.
|
|
21
|
+
*/
|
|
22
|
+
function formatWithPrettier(source, options = {}) {
|
|
23
|
+
const prettierPackageJson = require.resolve("prettier/package.json");
|
|
24
|
+
const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
|
|
25
|
+
const parser = options.parser || "typescript";
|
|
26
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", parser], {
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
input: source,
|
|
29
|
+
});
|
|
30
|
+
if (result.status !== 0) {
|
|
31
|
+
throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
|
|
32
|
+
}
|
|
33
|
+
return result.stdout;
|
|
34
|
+
}
|
|
@@ -107,6 +107,17 @@ catch (error) {
|
|
|
107
107
|
handleError(error);
|
|
108
108
|
}`,
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] try block annotated inside body under annotationPlacement: 'inside' (Story 028.0)",
|
|
112
|
+
code: `try {
|
|
113
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
114
|
+
// @req REQ-TRY-INSIDE-BRANCH
|
|
115
|
+
doWork();
|
|
116
|
+
} finally {
|
|
117
|
+
cleanup();
|
|
118
|
+
}`,
|
|
119
|
+
options: [{ annotationPlacement: "inside" }],
|
|
120
|
+
},
|
|
110
121
|
{
|
|
111
122
|
name: "[REQ-BRANCH-DETECTION] valid do-while loop with annotations",
|
|
112
123
|
code: `/* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md */
|
|
@@ -201,19 +212,6 @@ if (condition) {}`,
|
|
|
201
212
|
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
202
213
|
// @req REQ-INSIDE-BRACE-PLACEMENT
|
|
203
214
|
doSomething();
|
|
204
|
-
}`,
|
|
205
|
-
options: [{ annotationPlacement: "inside" }],
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] catch clause annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
|
|
209
|
-
code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
210
|
-
// @req REQ-BRANCH-TRY
|
|
211
|
-
try {
|
|
212
|
-
doSomething();
|
|
213
|
-
} catch (error) {
|
|
214
|
-
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
215
|
-
// @req REQ-INSIDE-CATCH
|
|
216
|
-
handleError(error);
|
|
217
215
|
}`,
|
|
218
216
|
options: [{ annotationPlacement: "inside" }],
|
|
219
217
|
},
|
|
@@ -223,6 +221,22 @@ try {
|
|
|
223
221
|
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
224
222
|
// @req REQ-LOOP-INSIDE
|
|
225
223
|
process(item);
|
|
224
|
+
}`,
|
|
225
|
+
options: [{ annotationPlacement: "inside" }],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] switch cases annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
|
|
229
|
+
code: `switch (value) {
|
|
230
|
+
case 'a': {
|
|
231
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
232
|
+
// @req REQ-SWITCH-CASE-INSIDE
|
|
233
|
+
doSomething();
|
|
234
|
+
}
|
|
235
|
+
default: {
|
|
236
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
237
|
+
// @req REQ-SWITCH-DEFAULT-INSIDE
|
|
238
|
+
doDefault();
|
|
239
|
+
}
|
|
226
240
|
}`,
|
|
227
241
|
options: [{ annotationPlacement: "inside" }],
|
|
228
242
|
},
|
|
@@ -446,6 +460,31 @@ if (a) {
|
|
|
446
460
|
}`,
|
|
447
461
|
errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
|
|
448
462
|
},
|
|
463
|
+
{
|
|
464
|
+
// Current behavior: inside-only catch annotations do NOT satisfy try branch in inside-placement mode.
|
|
465
|
+
name: "TODO-FUTURE-BEHAVIOR: [REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] catch clause annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
|
|
466
|
+
code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
467
|
+
// @req REQ-BRANCH-TRY
|
|
468
|
+
try {
|
|
469
|
+
doSomething();
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
472
|
+
// @req REQ-INSIDE-CATCH
|
|
473
|
+
handleError(error);
|
|
474
|
+
}`,
|
|
475
|
+
options: [{ annotationPlacement: "inside" }],
|
|
476
|
+
output: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
477
|
+
// @req REQ-BRANCH-TRY
|
|
478
|
+
// @story <story-file>.story.md
|
|
479
|
+
try {
|
|
480
|
+
doSomething();
|
|
481
|
+
} catch (error) {
|
|
482
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
483
|
+
// @req REQ-INSIDE-CATCH
|
|
484
|
+
handleError(error);
|
|
485
|
+
}`,
|
|
486
|
+
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
487
|
+
},
|
|
449
488
|
{
|
|
450
489
|
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-brace annotations ignored when annotationPlacement: 'inside'",
|
|
451
490
|
code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
@@ -493,14 +532,34 @@ catch (error) {
|
|
|
493
532
|
options: [{ annotationPlacement: "inside" }],
|
|
494
533
|
output: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
495
534
|
// @req REQ-BRANCH-TRY
|
|
535
|
+
// @story <story-file>.story.md
|
|
496
536
|
try {
|
|
497
537
|
doSomething();
|
|
498
538
|
}
|
|
499
539
|
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
500
540
|
// @req REQ-CATCH-BEFORE
|
|
501
541
|
catch (error) {
|
|
502
|
-
// @story <story-file>.story.md
|
|
503
542
|
handleError(error);
|
|
543
|
+
}`,
|
|
544
|
+
errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-try annotations ignored when annotationPlacement: 'inside' for TryStatement (Story 028.0)",
|
|
548
|
+
code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
549
|
+
// @req REQ-TRY-BEFORE
|
|
550
|
+
try {
|
|
551
|
+
doWork();
|
|
552
|
+
} finally {
|
|
553
|
+
cleanup();
|
|
554
|
+
}`,
|
|
555
|
+
options: [{ annotationPlacement: "inside" }],
|
|
556
|
+
output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
557
|
+
// @req REQ-TRY-BEFORE
|
|
558
|
+
// @story <story-file>.story.md
|
|
559
|
+
try {
|
|
560
|
+
doWork();
|
|
561
|
+
} finally {
|
|
562
|
+
cleanup();
|
|
504
563
|
}`,
|
|
505
564
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
506
565
|
},
|
|
@@ -555,6 +614,26 @@ if (a) {
|
|
|
555
614
|
doB();
|
|
556
615
|
} else {
|
|
557
616
|
doC();
|
|
617
|
+
}`,
|
|
618
|
+
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-case annotations ignored when annotationPlacement: 'inside' for SwitchCase",
|
|
622
|
+
code: `switch (value) {
|
|
623
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
624
|
+
// @req REQ-SWITCH-BEFORE
|
|
625
|
+
case 'a': {
|
|
626
|
+
doSomething();
|
|
627
|
+
}
|
|
628
|
+
}`,
|
|
629
|
+
options: [{ annotationPlacement: "inside" }],
|
|
630
|
+
output: `switch (value) {
|
|
631
|
+
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
632
|
+
// @req REQ-SWITCH-BEFORE
|
|
633
|
+
// @story <story-file>.story.md
|
|
634
|
+
case 'a': {
|
|
635
|
+
doSomething();
|
|
636
|
+
}
|
|
558
637
|
}`,
|
|
559
638
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
560
639
|
},
|
|
@@ -45,14 +45,12 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
it("should gather SwitchCase comment text via gatherBranchCommentText (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
|
|
48
|
-
// Fake SourceCode-like object with lines aligned to PRE_COMMENT_OFFSET logic
|
|
49
48
|
const sourceCode = {
|
|
50
|
-
lines: [
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
],
|
|
55
|
-
getCommentsBefore: () => [],
|
|
49
|
+
lines: [],
|
|
50
|
+
getCommentsBefore: jest.fn().mockReturnValue([
|
|
51
|
+
{ value: "@story first part" },
|
|
52
|
+
{ value: "@req REQ-FIRST" },
|
|
53
|
+
]),
|
|
56
54
|
getText: jest.fn(),
|
|
57
55
|
};
|
|
58
56
|
// SwitchCase-like node with loc.start.line corresponding to "case 1:" line (line 3)
|
|
@@ -64,8 +62,7 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
|
|
|
64
62
|
},
|
|
65
63
|
};
|
|
66
64
|
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, switchCaseNode);
|
|
67
|
-
|
|
68
|
-
expect(text).toBe("// @story first part // continuation second part");
|
|
65
|
+
expect(text).toBe("@story first part @req REQ-FIRST");
|
|
69
66
|
});
|
|
70
67
|
it("should gather comment text for CatchClause and loop nodes via gatherBranchCommentText (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
|
|
71
68
|
// CatchClause: comments come from getCommentsBefore when beforeText already contains @story
|
|
@@ -191,6 +188,50 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
|
|
|
191
188
|
expect(insideText).toContain("@req REQ-CATCH-INSIDE");
|
|
192
189
|
expect(insideText).not.toContain("before-catch should be ignored");
|
|
193
190
|
});
|
|
191
|
+
it("[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] uses inside-switch comments when annotationPlacement is 'inside' and ignores before-case annotations", () => {
|
|
192
|
+
const sourceCode = {
|
|
193
|
+
lines: [
|
|
194
|
+
"// @story before-switch should be ignored in inside mode",
|
|
195
|
+
"switch (value) {",
|
|
196
|
+
" case 'a': {",
|
|
197
|
+
" // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
|
|
198
|
+
" // @req REQ-SWITCH-INSIDE",
|
|
199
|
+
" doSomething();",
|
|
200
|
+
" }",
|
|
201
|
+
"}",
|
|
202
|
+
],
|
|
203
|
+
getCommentsBefore: jest
|
|
204
|
+
.fn()
|
|
205
|
+
.mockReturnValue([
|
|
206
|
+
{ value: "@story before-switch should be ignored in inside mode" },
|
|
207
|
+
]),
|
|
208
|
+
};
|
|
209
|
+
const switchCaseNode = {
|
|
210
|
+
type: "SwitchCase",
|
|
211
|
+
loc: {
|
|
212
|
+
start: { line: 3, column: 2 },
|
|
213
|
+
end: { line: 7, column: 4 },
|
|
214
|
+
},
|
|
215
|
+
consequent: [
|
|
216
|
+
{
|
|
217
|
+
type: "BlockStatement",
|
|
218
|
+
loc: {
|
|
219
|
+
start: { line: 3, column: 16 },
|
|
220
|
+
end: { line: 7, column: 4 },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
const parent = {
|
|
226
|
+
type: "SwitchStatement",
|
|
227
|
+
discriminant: { type: "Identifier", name: "value" },
|
|
228
|
+
cases: [switchCaseNode],
|
|
229
|
+
};
|
|
230
|
+
const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, switchCaseNode, parent, "inside");
|
|
231
|
+
expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
|
|
232
|
+
expect(insideText).toContain("@req REQ-SWITCH-INSIDE");
|
|
233
|
+
expect(insideText).not.toContain("before-switch should be ignored");
|
|
234
|
+
});
|
|
194
235
|
});
|
|
195
236
|
/**
|
|
196
237
|
* Tests for annotationPlacement wiring at helper level
|
|
@@ -311,4 +352,110 @@ describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-AN
|
|
|
311
352
|
expect(insideText).not.toContain("REQ-BEFORE-ELSE");
|
|
312
353
|
expect(insideText).not.toContain("docs/stories/026.0-DEV-BRANCH-ANNOTATIONS-ELSE-BRANCHES.story.md");
|
|
313
354
|
});
|
|
355
|
+
it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors configured placement for TryStatement branches in try/finally patterns", () => {
|
|
356
|
+
const sourceCode = {
|
|
357
|
+
lines: [
|
|
358
|
+
"function demoTry() {", // 1
|
|
359
|
+
" // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md", // 2 (before try)
|
|
360
|
+
" // @req REQ-BEFORE-TRY", // 3
|
|
361
|
+
" try {", // 4
|
|
362
|
+
" // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md", // 5 (inside try)
|
|
363
|
+
" // @req REQ-TRY-INSIDE", // 6
|
|
364
|
+
" doWork();", // 7
|
|
365
|
+
" } finally {", // 8
|
|
366
|
+
" cleanup();", // 9
|
|
367
|
+
" }", // 10
|
|
368
|
+
"}", // 11
|
|
369
|
+
],
|
|
370
|
+
getCommentsBefore: jest.fn().mockImplementation((node) => {
|
|
371
|
+
if (node && node.loc && node.loc.start && node.loc.start.line === 4) {
|
|
372
|
+
return [
|
|
373
|
+
{
|
|
374
|
+
value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md",
|
|
375
|
+
},
|
|
376
|
+
{ value: "@req REQ-BEFORE-TRY" },
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
return [];
|
|
380
|
+
}),
|
|
381
|
+
};
|
|
382
|
+
const tryNode = {
|
|
383
|
+
type: "TryStatement",
|
|
384
|
+
loc: {
|
|
385
|
+
start: { line: 4, column: 2 },
|
|
386
|
+
end: { line: 9, column: 3 },
|
|
387
|
+
},
|
|
388
|
+
block: {
|
|
389
|
+
type: "BlockStatement",
|
|
390
|
+
loc: {
|
|
391
|
+
start: { line: 4, column: 8 },
|
|
392
|
+
end: { line: 7, column: 3 },
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
handler: null,
|
|
396
|
+
finalizer: {
|
|
397
|
+
type: "BlockStatement",
|
|
398
|
+
loc: {
|
|
399
|
+
start: { line: 8, column: 12 },
|
|
400
|
+
end: { line: 9, column: 3 },
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
const parent = {
|
|
405
|
+
type: "BlockStatement",
|
|
406
|
+
body: [tryNode],
|
|
407
|
+
};
|
|
408
|
+
const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, tryNode, parent, "before");
|
|
409
|
+
expect(beforeText).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
|
|
410
|
+
expect(beforeText).toContain("@req REQ-BEFORE-TRY");
|
|
411
|
+
expect(beforeText).not.toContain("REQ-TRY-INSIDE");
|
|
412
|
+
const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, tryNode, parent, "inside");
|
|
413
|
+
expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
|
|
414
|
+
expect(insideText).toContain("@req REQ-TRY-INSIDE");
|
|
415
|
+
expect(insideText).not.toContain("REQ-BEFORE-TRY");
|
|
416
|
+
});
|
|
417
|
+
it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors before-case annotations for SwitchCase in default placement mode", () => {
|
|
418
|
+
const sourceCode = {
|
|
419
|
+
lines: [
|
|
420
|
+
"// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md",
|
|
421
|
+
"// @req REQ-SWITCH-BEFORE",
|
|
422
|
+
"switch (value) {",
|
|
423
|
+
" case 'a':",
|
|
424
|
+
" doSomething();",
|
|
425
|
+
"}",
|
|
426
|
+
],
|
|
427
|
+
getCommentsBefore: jest
|
|
428
|
+
.fn()
|
|
429
|
+
.mockReturnValue([
|
|
430
|
+
{
|
|
431
|
+
value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md",
|
|
432
|
+
},
|
|
433
|
+
{ value: "@req REQ-SWITCH-BEFORE" },
|
|
434
|
+
]),
|
|
435
|
+
};
|
|
436
|
+
const switchCaseNode = {
|
|
437
|
+
type: "SwitchCase",
|
|
438
|
+
loc: {
|
|
439
|
+
start: { line: 4, column: 2 },
|
|
440
|
+
end: { line: 5, column: 18 },
|
|
441
|
+
},
|
|
442
|
+
consequent: [
|
|
443
|
+
{
|
|
444
|
+
type: "ExpressionStatement",
|
|
445
|
+
loc: {
|
|
446
|
+
start: { line: 5, column: 4 },
|
|
447
|
+
end: { line: 5, column: 18 },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
const parent = {
|
|
453
|
+
type: "SwitchStatement",
|
|
454
|
+
discriminant: { type: "Identifier", name: "value" },
|
|
455
|
+
cases: [switchCaseNode],
|
|
456
|
+
};
|
|
457
|
+
const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, switchCaseNode, parent, "before");
|
|
458
|
+
expect(beforeText).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
|
|
459
|
+
expect(beforeText).toContain("@req REQ-SWITCH-BEFORE");
|
|
460
|
+
});
|
|
314
461
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.3",
|
|
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",
|
|
@@ -86,13 +86,14 @@ function initAuth() {
|
|
|
86
86
|
|
|
87
87
|
### traceability/require-branch-annotation
|
|
88
88
|
|
|
89
|
-
Description: Ensures significant code branches (if/else chains, loops, switch cases, try/catch) have traceability coverage, typically via a single `@supports` line, while still accepting legacy `@story` and `@req` annotations in nearby comments. When you adopt multi-story `@supports` annotations, a single `@supports <storyPath> <REQ-ID>...` line placed in any of the valid branch comment locations is treated as satisfying both the story and requirement presence checks for that branch, while detailed format validation of the `@supports` value (including story paths and requirement IDs) continues to be handled by `traceability/valid-annotation-format`, `traceability/valid-story-reference`, and `traceability/valid-req-reference`.
|
|
89
|
+
Description: Ensures significant code branches (if/else chains, loops, switch cases, try/catch) have traceability coverage, typically via a single `@supports` line, while still accepting legacy `@story` and `@req` annotations in nearby comments. When you adopt multi-story `@supports` annotations, a single `@supports <storyPath> <REQ-ID>...` line placed in any of the valid branch comment locations is treated as satisfying both the story and requirement presence checks for that branch, while detailed format validation of the `@supports` value (including story paths and requirement IDs) continues to be handled by `traceability/valid-annotation-format`, `traceability/valid-story-reference`, and `traceability/valid-req-reference`. The rule supports a configurable placement mode for branch annotations via `annotationPlacement: "before" | "inside"`, defaulting to `"before"` for backward compatibility.
|
|
90
90
|
|
|
91
91
|
For most branches, the rule looks for annotations in comments immediately preceding the branch keyword (for example, the line above an `if` or `for` statement). For `catch` clauses and `else if` branches, the rule is formatter-aware and accepts annotations in additional positions that common formatters like Prettier use when they reflow code.
|
|
92
92
|
|
|
93
93
|
Options:
|
|
94
94
|
|
|
95
95
|
- `branchTypes` (string[], optional) – AST node types that are treated as significant branches for annotation enforcement. Allowed values: "IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement". Default: ["IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"]. If an invalid branch type is provided, the rule reports a configuration error for each invalid value.
|
|
96
|
+
- `annotationPlacement` ("before" | "inside", optional) – Controls whether the rule looks for annotations immediately before branches (`"before"`, the default and backward-compatible behavior) or requires annotations as the first comment lines inside branch bodies where supported (`"inside"`). When set to `"inside"`, the rule prefers comments at the top of the block bodies for simple `if`/`else if` branches, loops, `catch` clauses, and `try` blocks, while continuing to treat any existing before-branch comments as diagnostics in that mode.
|
|
96
97
|
|
|
97
98
|
Behavior notes:
|
|
98
99
|
|
|
@@ -109,6 +110,11 @@ Behavior notes:
|
|
|
109
110
|
- When annotations appear in more than one of these locations, the rule prefers the comments immediately before the `else if` line, then comments between the condition and the block, and finally comments inside the block body. This precedence is designed to closely mirror real-world formatter behavior and matches the formatter-aware scenarios described in stories 025.0 and 026.0.
|
|
110
111
|
- When auto-fixing missing annotations on an `else if` branch, the rule inserts placeholder comments as the first comment-only line inside the consequent block body (just after the opening `{`), which is a stable location under Prettier and similar formatters. As with catch clauses, a single `@supports` annotation placed in any of these accepted locations is treated as equivalent to having both `@story` and `@req` comments for that branch, with deep format and existence checks delegated to the other validation rules.
|
|
111
112
|
|
|
113
|
+
Placement modes:
|
|
114
|
+
|
|
115
|
+
- `"before"` mode preserves the existing semantics described above, including the dual-position behavior for `catch` and `else if` branches where comments immediately before the branch and the first comment-only lines inside the block are both acceptable and validated according to their existing precedence rules.
|
|
116
|
+
- `"inside"` mode standardizes on the first comment-only lines inside supported branch blocks (`if`/`else if`, loops, `catch`, and `try`) for validation and auto-fix insertion. This placement is designed to work well with Prettier and other formatters, while the current implementation still treats many before-branch annotations as needing migration and may, in corner cases where it cannot confidently rewrite placement, insert new placeholder comments above the branch rather than moving existing ones.
|
|
117
|
+
|
|
112
118
|
For a concrete illustration of how these rules interact with Prettier, see the formatter-aware if/else/else-if example in [user-docs/examples.md](examples.md) (section **6. Branch annotations with if/else/else-if and Prettier**), which shows both the hand-written and formatted code that the rule considers valid.
|
|
113
119
|
|
|
114
120
|
These behaviors are intentionally limited to `catch` clauses and `else if` branches; other branch types (plain `if`, `else`, loops, and `switch` cases) continue to use the simpler "comments immediately before the branch" association model for both validation and auto-fix placement.
|
|
@@ -805,5 +811,4 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
|
|
|
805
811
|
In CI:
|
|
806
812
|
|
|
807
813
|
```bash
|
|
808
|
-
npm run traceability:verify
|
|
809
|
-
```
|
|
814
|
+
npm run traceability:verify
|