eslint-plugin-traceability 1.19.2 → 1.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/README.md +41 -0
- package/lib/src/utils/branch-annotation-helpers.d.ts +2 -0
- package/lib/src/utils/branch-annotation-helpers.js +68 -30
- package/lib/src/utils/branch-annotation-indent-helpers.d.ts +52 -0
- package/lib/src/utils/branch-annotation-indent-helpers.js +137 -0
- package/lib/src/utils/branch-annotation-report-helpers.d.ts +1 -0
- package/lib/src/utils/branch-annotation-report-helpers.js +95 -13
- package/lib/src/utils/branch-annotation-story-fix-helpers.d.ts +27 -0
- package/lib/src/utils/branch-annotation-story-fix-helpers.js +48 -0
- 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 +70 -55
- package/lib/tests/utils/branch-annotation-helpers.test.js +156 -9
- package/package.json +1 -1
- package/user-docs/api-reference.md +7 -1
- package/user-docs/migration-guide.md +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
## [1.19.
|
|
1
|
+
## [1.19.4](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.3...v1.19.4) (2025-12-18)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* migrate before-brace annotations into inside-brace placement ([c8c381d](https://github.com/voder-ai/eslint-plugin-traceability/commit/c8c381d5f5fbd39fdb626e9dd6fe2dd837bffb53))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -89,6 +89,47 @@ export default [
|
|
|
89
89
|
];
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
### Annotation Placement
|
|
93
|
+
|
|
94
|
+
Traceability annotations are typically placed immediately adjacent to the code they index. The plugin exposes explicit placement options for branch-level rules and a stable, conventional placement for function-level rules.
|
|
95
|
+
|
|
96
|
+
- **Branch-level (`traceability/require-branch-annotation`)**
|
|
97
|
+
|
|
98
|
+
`require-branch-annotation` supports an `annotationPlacement` option:
|
|
99
|
+
- `"before"` – Annotation appears **immediately before** the branch statement (default).
|
|
100
|
+
- `"inside"` – Annotation appears as the **first comment-only lines inside** the branch block.
|
|
101
|
+
|
|
102
|
+
In `"inside"` mode, the rule expects the annotation to be the first meaningful content inside blocks for `if` / `else` / loops / `try` / `catch` / `finally` / `switch` cases.
|
|
103
|
+
|
|
104
|
+
Example (`if` statement):
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// annotationPlacement: "before"
|
|
108
|
+
// @supports docs/stories/auth.md REQ-AUTH-VALIDATION
|
|
109
|
+
if (isValidUser(user)) {
|
|
110
|
+
performLogin(user);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// annotationPlacement: "inside"
|
|
114
|
+
if (isValidUser(user)) {
|
|
115
|
+
// @supports docs/stories/auth.md REQ-AUTH-VALIDATION
|
|
116
|
+
performLogin(user);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **Function-level (`traceability/require-story-annotation`, `traceability/require-req-annotation`)**
|
|
121
|
+
|
|
122
|
+
Function-level rules continue to accept annotations:
|
|
123
|
+
- As JSDoc blocks immediately preceding the function, or
|
|
124
|
+
- As line comments placed directly before the function declaration or expression.
|
|
125
|
+
|
|
126
|
+
This placement is stable and supported for all current versions. Future versions may introduce an **inside-brace** placement mode for function bodies (similar to branch blocks) to align function annotations with the branch-level `"inside"` standard.
|
|
127
|
+
|
|
128
|
+
For full configuration details and migration guidance between placement styles, see:
|
|
129
|
+
|
|
130
|
+
- `traceability/require-branch-annotation` rule docs: [docs/rules/require-branch-annotation.md](docs/rules/require-branch-annotation.md)
|
|
131
|
+
- Migration guide: [user-docs/migration-guide.md](user-docs/migration-guide.md)
|
|
132
|
+
|
|
92
133
|
### Available Rules
|
|
93
134
|
|
|
94
135
|
The plugin exposes several rules. For **new configurations**, the unified function-level rule and `@supports` annotations are the canonical choice; the `@story` and `@req` forms remain available primarily for backward compatibility and gradual migration.
|
|
@@ -53,6 +53,8 @@ export declare function reportMissingStory(context: Rule.RuleContext, node: any,
|
|
|
53
53
|
storyFixCountRef: {
|
|
54
54
|
count: number;
|
|
55
55
|
};
|
|
56
|
+
annotationPlacement: AnnotationPlacement;
|
|
57
|
+
sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>;
|
|
56
58
|
}): void;
|
|
57
59
|
/**
|
|
58
60
|
* Report missing @req annotation tag on a branch node when that branch has no linked requirement identifier in its associated comments.
|
|
@@ -10,7 +10,8 @@ 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
|
+
const branch_annotation_story_fix_helpers_1 = require("./branch-annotation-story-fix-helpers");
|
|
14
15
|
/**
|
|
15
16
|
* Valid branch types for require-branch-annotation rule.
|
|
16
17
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -179,6 +180,30 @@ function getInsideCatchCommentText(sourceCode, node) {
|
|
|
179
180
|
}
|
|
180
181
|
return "";
|
|
181
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Gather comment text from the first contiguous comment lines inside a TryStatement block body.
|
|
185
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
186
|
+
*/
|
|
187
|
+
function getInsideTryBlockCommentText(sourceCode, node) {
|
|
188
|
+
const block = node && node.block;
|
|
189
|
+
if (!block ||
|
|
190
|
+
block.type !== "BlockStatement" ||
|
|
191
|
+
!block.loc ||
|
|
192
|
+
!block.loc.start ||
|
|
193
|
+
!block.loc.end ||
|
|
194
|
+
typeof block.loc.start.line !== "number" ||
|
|
195
|
+
typeof block.loc.end.line !== "number") {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
const lines = sourceCode.lines;
|
|
199
|
+
const startIndex = block.loc.start.line - 1;
|
|
200
|
+
const endIndex = block.loc.end.line - 1;
|
|
201
|
+
const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
|
|
202
|
+
if (insideText) {
|
|
203
|
+
return insideText;
|
|
204
|
+
}
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
182
207
|
/**
|
|
183
208
|
* Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
|
|
184
209
|
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
@@ -253,17 +278,33 @@ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, before
|
|
|
253
278
|
}
|
|
254
279
|
return "";
|
|
255
280
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
281
|
+
function handleTryCatchBranch(sourceCode, node, context) {
|
|
282
|
+
const { annotationPlacement, beforeText } = context;
|
|
283
|
+
if (node.type === "TryStatement") {
|
|
284
|
+
if (annotationPlacement === "inside") {
|
|
285
|
+
const insideText = getInsideTryBlockCommentText(sourceCode, node);
|
|
286
|
+
if (insideText) {
|
|
287
|
+
return insideText;
|
|
288
|
+
}
|
|
289
|
+
return "";
|
|
290
|
+
}
|
|
291
|
+
return beforeText;
|
|
265
292
|
}
|
|
266
|
-
|
|
293
|
+
if (node.type === "CatchClause") {
|
|
294
|
+
return gatherCatchClauseCommentText(sourceCode, node, annotationPlacement, beforeText);
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function handleLoopBranch(sourceCode, node, context) {
|
|
299
|
+
const { annotationPlacement, beforeText } = context;
|
|
300
|
+
if (node.type === "ForStatement" ||
|
|
301
|
+
node.type === "ForInStatement" ||
|
|
302
|
+
node.type === "ForOfStatement" ||
|
|
303
|
+
node.type === "WhileStatement" ||
|
|
304
|
+
node.type === "DoWhileStatement") {
|
|
305
|
+
return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, annotationPlacement, beforeText);
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
267
308
|
}
|
|
268
309
|
/**
|
|
269
310
|
* Helper that gathers comment text for non-IfStatement branch types using
|
|
@@ -274,19 +315,17 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
|
|
|
274
315
|
* @supports REQ-PLACEMENT-CONFIG
|
|
275
316
|
*/
|
|
276
317
|
function gatherNonIfBranchCommentText(sourceCode, node, context) {
|
|
277
|
-
const { annotationPlacement, beforeText } = context;
|
|
278
318
|
if (node.type === "SwitchCase") {
|
|
279
|
-
|
|
319
|
+
const { annotationPlacement, beforeText } = context;
|
|
320
|
+
return (0, branch_annotation_switch_helpers_1.gatherSwitchCaseCommentText)(sourceCode, node, annotationPlacement, beforeText);
|
|
280
321
|
}
|
|
281
|
-
|
|
282
|
-
|
|
322
|
+
const tryCatchResult = handleTryCatchBranch(sourceCode, node, context);
|
|
323
|
+
if (tryCatchResult != null) {
|
|
324
|
+
return tryCatchResult;
|
|
283
325
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
node.type === "WhileStatement" ||
|
|
288
|
-
node.type === "DoWhileStatement") {
|
|
289
|
-
return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, annotationPlacement, beforeText);
|
|
326
|
+
const loopResult = handleLoopBranch(sourceCode, node, context);
|
|
327
|
+
if (loopResult != null) {
|
|
328
|
+
return loopResult;
|
|
290
329
|
}
|
|
291
330
|
return null;
|
|
292
331
|
}
|
|
@@ -364,21 +403,20 @@ function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement =
|
|
|
364
403
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
365
404
|
*/
|
|
366
405
|
function reportMissingStory(context, node, options) {
|
|
367
|
-
const { indent, insertPos, storyFixCountRef } = options;
|
|
406
|
+
const { indent, insertPos, storyFixCountRef, annotationPlacement, sourceCode, } = options;
|
|
368
407
|
/**
|
|
369
408
|
* Conditional branch deciding whether to offer an auto-fix for the missing story.
|
|
370
409
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
371
410
|
* @req REQ-TRACEABILITY-FIX-DECISION - Trace decision to provide fixer for missing @story
|
|
372
411
|
*/
|
|
373
412
|
if (storyFixCountRef.count === 0) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
413
|
+
const insertStoryFixer = (0, branch_annotation_story_fix_helpers_1.createStoryFixer)({
|
|
414
|
+
annotationPlacement,
|
|
415
|
+
sourceCode,
|
|
416
|
+
node,
|
|
417
|
+
insertPos,
|
|
418
|
+
indent,
|
|
419
|
+
});
|
|
382
420
|
context.report({
|
|
383
421
|
node,
|
|
384
422
|
messageId: "missingAnnotation",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
import type { AnnotationPlacement } from "./branch-annotation-helpers";
|
|
3
|
+
/**
|
|
4
|
+
* Shared helpers for computing inside-brace indentation and insert positions
|
|
5
|
+
* for branch nodes used by require-branch-annotation. This module isolates
|
|
6
|
+
* the inside-placement logic so that the main report helpers stay small and
|
|
7
|
+
* within ESLint's max-lines-per-function limits.
|
|
8
|
+
*
|
|
9
|
+
* @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
10
|
+
* @supports REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG REQ-INDENTATION-CORRECT
|
|
11
|
+
*/
|
|
12
|
+
type SourceCode = ReturnType<Rule.RuleContext["getSourceCode"]>;
|
|
13
|
+
type IndentHelperContext = {
|
|
14
|
+
getInsideBlockIndentAndInsertPos: (_sourceCode: SourceCode, _blockNode: any, _baseFallbackIndent: string) => {
|
|
15
|
+
indent: string;
|
|
16
|
+
insertPos: number;
|
|
17
|
+
};
|
|
18
|
+
getIndentAndInsertPosForLine: (_sourceCode: SourceCode, _line: number, _fallbackIndent: string) => {
|
|
19
|
+
indent: string;
|
|
20
|
+
insertPos: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
type BranchIndentOptions = {
|
|
24
|
+
sourceCode: SourceCode;
|
|
25
|
+
node: any;
|
|
26
|
+
indent: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Inside-placement helper used by getBaseBranchIndentAndInsertPos to select the
|
|
30
|
+
* correct inside-placement strategy for the base branch.
|
|
31
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
32
|
+
*/
|
|
33
|
+
export declare function computeInsideBaseIndentAndInsertPos(options: {
|
|
34
|
+
sourceCode: SourceCode;
|
|
35
|
+
node: any;
|
|
36
|
+
annotationPlacement: AnnotationPlacement;
|
|
37
|
+
currentIndent: string;
|
|
38
|
+
}, context: IndentHelperContext): {
|
|
39
|
+
indent: string;
|
|
40
|
+
insertPos: number;
|
|
41
|
+
} | null;
|
|
42
|
+
/**
|
|
43
|
+
* Apply inside-placement overrides for non-if branches (switch, try, loops, catch).
|
|
44
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyInsidePlacementOverridesForBranch(options: BranchIndentOptions & {
|
|
47
|
+
annotationPlacement: AnnotationPlacement;
|
|
48
|
+
}, context: IndentHelperContext): {
|
|
49
|
+
indent: string;
|
|
50
|
+
insertPos: number;
|
|
51
|
+
} | null;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeInsideBaseIndentAndInsertPos = computeInsideBaseIndentAndInsertPos;
|
|
4
|
+
exports.applyInsidePlacementOverridesForBranch = applyInsidePlacementOverridesForBranch;
|
|
5
|
+
function isLoopNode(node) {
|
|
6
|
+
return (node.type === "ForStatement" ||
|
|
7
|
+
node.type === "ForInStatement" ||
|
|
8
|
+
node.type === "ForOfStatement" ||
|
|
9
|
+
node.type === "WhileStatement" ||
|
|
10
|
+
node.type === "DoWhileStatement");
|
|
11
|
+
}
|
|
12
|
+
function computeInsideCatchIndentAndInsertPos(sourceCode, node, currentIndent, context) {
|
|
13
|
+
if (!(node.type === "CatchClause" && node.body)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const bodyNode = node.body;
|
|
17
|
+
if (!bodyNode.loc || !bodyNode.loc.start) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return context.getInsideBlockIndentAndInsertPos(sourceCode, bodyNode, currentIndent);
|
|
21
|
+
}
|
|
22
|
+
function computeInsideLoopIndentAndInsertPos(options, context) {
|
|
23
|
+
const { sourceCode, node, indent } = options;
|
|
24
|
+
if (!isLoopNode(node) ||
|
|
25
|
+
!node.body ||
|
|
26
|
+
node.body.type !== "BlockStatement" ||
|
|
27
|
+
!node.body.loc ||
|
|
28
|
+
!node.body.loc.start) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return context.getInsideBlockIndentAndInsertPos(sourceCode, node.body, indent);
|
|
32
|
+
}
|
|
33
|
+
function computeInsideTryOrSwitchIndentAndInsertPos(sourceCode, node, currentIndent, context) {
|
|
34
|
+
if (!((node.type === "TryStatement" || node.type === "SwitchCase") &&
|
|
35
|
+
node.consequent &&
|
|
36
|
+
Array.isArray(node.consequent) &&
|
|
37
|
+
node.consequent.length > 0)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const firstStatement = node.consequent[0];
|
|
41
|
+
if (!firstStatement || !firstStatement.loc || !firstStatement.loc.start) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const commentLineInfo = context.getIndentAndInsertPosForLine(sourceCode, firstStatement.loc.start.line, currentIndent);
|
|
45
|
+
return {
|
|
46
|
+
indent: commentLineInfo.indent,
|
|
47
|
+
insertPos: commentLineInfo.insertPos,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function computeInsideTryBlockIndentAndInsertPos(options, context) {
|
|
51
|
+
const { sourceCode, node, indent } = options;
|
|
52
|
+
if (!(node.type === "TryStatement" &&
|
|
53
|
+
node.block &&
|
|
54
|
+
node.block.type === "BlockStatement" &&
|
|
55
|
+
node.block.loc &&
|
|
56
|
+
node.block.loc.start)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return context.getInsideBlockIndentAndInsertPos(sourceCode, node.block, indent);
|
|
60
|
+
}
|
|
61
|
+
function computeInsideSwitchCaseIndentAndInsertPos(options, context) {
|
|
62
|
+
const { sourceCode, node, indent } = options;
|
|
63
|
+
if (!(node.type === "SwitchCase" &&
|
|
64
|
+
node.consequent &&
|
|
65
|
+
Array.isArray(node.consequent) &&
|
|
66
|
+
node.consequent.length > 0)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const firstStatement = node.consequent[0];
|
|
70
|
+
if (!firstStatement || !firstStatement.loc || !firstStatement.loc.start) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Prefer line-based helper for consistency with other callers.
|
|
74
|
+
const commentLineInfo = context.getIndentAndInsertPosForLine(sourceCode, firstStatement.loc.start.line, indent);
|
|
75
|
+
return {
|
|
76
|
+
indent: commentLineInfo.indent,
|
|
77
|
+
insertPos: commentLineInfo.insertPos,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function computeInsideCatchBlockIndentAndInsertPos(options, context) {
|
|
81
|
+
const { sourceCode, node, indent } = options;
|
|
82
|
+
if (!(node.type === "CatchClause" &&
|
|
83
|
+
node.body &&
|
|
84
|
+
node.body.type === "BlockStatement" &&
|
|
85
|
+
node.body.loc &&
|
|
86
|
+
node.body.loc.start)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return context.getInsideBlockIndentAndInsertPos(sourceCode, node.body, indent);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Inside-placement helper used by getBaseBranchIndentAndInsertPos to select the
|
|
93
|
+
* correct inside-placement strategy for the base branch.
|
|
94
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
95
|
+
*/
|
|
96
|
+
function computeInsideBaseIndentAndInsertPos(options, context) {
|
|
97
|
+
const { sourceCode, node, annotationPlacement, currentIndent } = options;
|
|
98
|
+
if (annotationPlacement !== "inside") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const catchInside = computeInsideCatchIndentAndInsertPos(sourceCode, node, currentIndent, context);
|
|
102
|
+
if (catchInside) {
|
|
103
|
+
return catchInside;
|
|
104
|
+
}
|
|
105
|
+
const loopInside = computeInsideLoopIndentAndInsertPos({ sourceCode, node, indent: currentIndent }, context);
|
|
106
|
+
if (loopInside) {
|
|
107
|
+
return loopInside;
|
|
108
|
+
}
|
|
109
|
+
const tryOrSwitchInside = computeInsideTryOrSwitchIndentAndInsertPos(sourceCode, node, currentIndent, context);
|
|
110
|
+
if (tryOrSwitchInside) {
|
|
111
|
+
return tryOrSwitchInside;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Apply inside-placement overrides for non-if branches (switch, try, loops, catch).
|
|
117
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
118
|
+
*/
|
|
119
|
+
function applyInsidePlacementOverridesForBranch(options, context) {
|
|
120
|
+
const { annotationPlacement } = options;
|
|
121
|
+
if (annotationPlacement !== "inside") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const calculators = [
|
|
125
|
+
computeInsideSwitchCaseIndentAndInsertPos,
|
|
126
|
+
computeInsideTryBlockIndentAndInsertPos,
|
|
127
|
+
computeInsideLoopIndentAndInsertPos,
|
|
128
|
+
computeInsideCatchBlockIndentAndInsertPos,
|
|
129
|
+
];
|
|
130
|
+
for (const calculator of calculators) {
|
|
131
|
+
const result = calculator(options, context);
|
|
132
|
+
if (result) {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
@@ -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/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
8
9
|
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
9
10
|
*/
|
|
10
11
|
export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.reportMissingAnnotations = reportMissingAnnotations;
|
|
4
4
|
const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
|
|
5
|
+
const branch_annotation_indent_helpers_1 = require("./branch-annotation-indent-helpers");
|
|
5
6
|
/**
|
|
6
7
|
* Compute indentation and insert position for the start of a given 1-based line
|
|
7
8
|
* number. This keeps indentation and fixer insert positions consistent across
|
|
@@ -23,11 +24,43 @@ function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
|
|
|
23
24
|
return { indent, insertPos };
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* Compute indentation and insert position for the first "inner" line of a
|
|
28
|
+
* BlockStatement, used for inside-brace insertion.
|
|
29
|
+
* Falls back to the block's own line with one extra indent step if it has no
|
|
30
|
+
* body statements.
|
|
28
31
|
*/
|
|
29
|
-
function
|
|
30
|
-
let
|
|
32
|
+
function getInsideBlockIndentAndInsertPos(sourceCode, blockNode, baseFallbackIndent) {
|
|
33
|
+
let indent = baseFallbackIndent;
|
|
34
|
+
let insertPos = sourceCode.getIndexFromLoc({
|
|
35
|
+
line: blockNode.loc.start.line,
|
|
36
|
+
column: 0,
|
|
37
|
+
});
|
|
38
|
+
const bodyStatements = Array.isArray(blockNode.body)
|
|
39
|
+
? blockNode.body
|
|
40
|
+
: undefined;
|
|
41
|
+
const firstStatement = bodyStatements && bodyStatements.length > 0 ? bodyStatements[0] : undefined;
|
|
42
|
+
if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
|
|
43
|
+
const firstLine = firstStatement.loc.start.line;
|
|
44
|
+
const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, baseFallbackIndent);
|
|
45
|
+
indent = firstLineInfo.indent;
|
|
46
|
+
insertPos = firstLineInfo.insertPos;
|
|
47
|
+
}
|
|
48
|
+
else if (blockNode.loc && blockNode.loc.start) {
|
|
49
|
+
const blockLine = blockNode.loc.start.line;
|
|
50
|
+
const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, baseFallbackIndent);
|
|
51
|
+
const innerIndent = `${blockLineInfo.indent} `;
|
|
52
|
+
indent = innerIndent;
|
|
53
|
+
insertPos = blockLineInfo.insertPos;
|
|
54
|
+
}
|
|
55
|
+
return { indent, insertPos };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Apply the base catch-clause indentation/insert-position fallback used by
|
|
59
|
+
* getBaseBranchIndentAndInsertPos when no inside-placement override is applied.
|
|
60
|
+
*/
|
|
61
|
+
function applyCatchClauseBaseIndentFallback(sourceCode, node, currentIndent, currentInsertPos) {
|
|
62
|
+
let indent = currentIndent;
|
|
63
|
+
let insertPos = currentInsertPos;
|
|
31
64
|
if (node.type === "CatchClause" && node.body) {
|
|
32
65
|
const bodyNode = node.body;
|
|
33
66
|
const bodyStatements = Array.isArray(bodyNode.body)
|
|
@@ -52,6 +85,27 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement)
|
|
|
52
85
|
}
|
|
53
86
|
return { indent, insertPos };
|
|
54
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
90
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
91
|
+
*/
|
|
92
|
+
function getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement) {
|
|
93
|
+
let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
|
|
94
|
+
const indentHelpers = {
|
|
95
|
+
getInsideBlockIndentAndInsertPos,
|
|
96
|
+
getIndentAndInsertPosForLine,
|
|
97
|
+
};
|
|
98
|
+
const insideBase = (0, branch_annotation_indent_helpers_1.computeInsideBaseIndentAndInsertPos)({
|
|
99
|
+
sourceCode,
|
|
100
|
+
node,
|
|
101
|
+
annotationPlacement,
|
|
102
|
+
currentIndent: indent,
|
|
103
|
+
}, indentHelpers);
|
|
104
|
+
if (insideBase) {
|
|
105
|
+
return insideBase;
|
|
106
|
+
}
|
|
107
|
+
return applyCatchClauseBaseIndentFallback(sourceCode, node, indent, insertPos);
|
|
108
|
+
}
|
|
55
109
|
/**
|
|
56
110
|
* Determine whether a node represents an else-if branch that should be used for
|
|
57
111
|
* determining comment insertion position.
|
|
@@ -112,7 +166,8 @@ function getBranchMissingFlags(sourceCode, node, parent, annotationPlacement) {
|
|
|
112
166
|
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
113
167
|
*/
|
|
114
168
|
function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement) {
|
|
115
|
-
const
|
|
169
|
+
const base = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
|
|
170
|
+
let { indent, insertPos } = base;
|
|
116
171
|
if (node.type === "IfStatement") {
|
|
117
172
|
const context = { indent, insertPos };
|
|
118
173
|
const updatedContext = getIfStatementIndentAndInsertPos(sourceCode, node, { parent, annotationPlacement }, context);
|
|
@@ -121,6 +176,19 @@ function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlaceme
|
|
|
121
176
|
insertPos: updatedContext.insertPos,
|
|
122
177
|
};
|
|
123
178
|
}
|
|
179
|
+
const indentHelpers = {
|
|
180
|
+
getInsideBlockIndentAndInsertPos,
|
|
181
|
+
getIndentAndInsertPosForLine,
|
|
182
|
+
};
|
|
183
|
+
const insideOverride = (0, branch_annotation_indent_helpers_1.applyInsidePlacementOverridesForBranch)({
|
|
184
|
+
sourceCode,
|
|
185
|
+
node,
|
|
186
|
+
indent,
|
|
187
|
+
annotationPlacement,
|
|
188
|
+
}, indentHelpers);
|
|
189
|
+
if (insideOverride) {
|
|
190
|
+
return insideOverride;
|
|
191
|
+
}
|
|
124
192
|
return { indent, insertPos };
|
|
125
193
|
}
|
|
126
194
|
/**
|
|
@@ -137,12 +205,22 @@ function getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement)
|
|
|
137
205
|
const { indent, insertPos } = getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement);
|
|
138
206
|
return { missingStory, missingReq, indent, insertPos };
|
|
139
207
|
}
|
|
208
|
+
function processMissingAnnotationActions(context, node, actions) {
|
|
209
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
210
|
+
function processAction(item) {
|
|
211
|
+
if (item.missing) {
|
|
212
|
+
item.fn(...item.args);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
actions.forEach(processAction);
|
|
216
|
+
}
|
|
140
217
|
/**
|
|
141
218
|
* Report missing annotations on a branch node.
|
|
142
219
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
143
220
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
144
221
|
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
145
222
|
* @supports REQ-DUAL-POSITION-DETECTION
|
|
223
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
146
224
|
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
|
|
147
225
|
*/
|
|
148
226
|
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
@@ -159,7 +237,17 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
|
159
237
|
{
|
|
160
238
|
missing: missingStory,
|
|
161
239
|
fn: branch_annotation_helpers_1.reportMissingStory,
|
|
162
|
-
args: [
|
|
240
|
+
args: [
|
|
241
|
+
context,
|
|
242
|
+
node,
|
|
243
|
+
{
|
|
244
|
+
indent,
|
|
245
|
+
insertPos,
|
|
246
|
+
storyFixCountRef,
|
|
247
|
+
annotationPlacement,
|
|
248
|
+
sourceCode,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
163
251
|
},
|
|
164
252
|
{
|
|
165
253
|
missing: missingReq,
|
|
@@ -167,11 +255,5 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
|
167
255
|
args: [context, node, { indent, insertPos, missingStory }],
|
|
168
256
|
},
|
|
169
257
|
];
|
|
170
|
-
|
|
171
|
-
function processAction(item) {
|
|
172
|
-
if (item.missing) {
|
|
173
|
-
item.fn(...item.args);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
actions.forEach(processAction);
|
|
258
|
+
processMissingAnnotationActions(context, node, actions);
|
|
177
259
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
import type { AnnotationPlacement } from "./branch-annotation-helpers";
|
|
3
|
+
/**
|
|
4
|
+
* Context object for building story-fixers used by require-branch-annotation.
|
|
5
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
6
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-INSIDE-BRACE-PLACEMENT
|
|
7
|
+
*/
|
|
8
|
+
export interface StoryFixContext {
|
|
9
|
+
annotationPlacement: AnnotationPlacement;
|
|
10
|
+
sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>;
|
|
11
|
+
node: any;
|
|
12
|
+
insertPos: number;
|
|
13
|
+
indent: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a fixer function that inserts or migrates a @story comment for a
|
|
17
|
+
* missing branch annotation, honoring the configured placement mode.
|
|
18
|
+
* When annotationPlacement is "inside", this helper uses
|
|
19
|
+
* buildInsidePlacementStoryFixes to migrate existing before-branch
|
|
20
|
+
* annotations into the standardized inside-brace location. Otherwise, it
|
|
21
|
+
* preserves the original "before" behavior of inserting directly above
|
|
22
|
+
* the branch.
|
|
23
|
+
*
|
|
24
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
25
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-AUTO-FIX-MIGRATION REQ-INDENTATION-CORRECT
|
|
26
|
+
*/
|
|
27
|
+
export declare function createStoryFixer(ctx: StoryFixContext): (fixer: any) => any;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStoryFixer = createStoryFixer;
|
|
4
|
+
/**
|
|
5
|
+
* Build the individual fixes needed when migrating existing before-branch
|
|
6
|
+
* annotations into inside-brace placement. This helper is responsible for
|
|
7
|
+
* removing redundant before-branch comments that already contain
|
|
8
|
+
* traceability tags and inserting the canonical placeholder inside the
|
|
9
|
+
* branch body at the computed insertion position.
|
|
10
|
+
*
|
|
11
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
12
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-AUTO-FIX-MIGRATION REQ-INSIDE-BRACE-PLACEMENT
|
|
13
|
+
*/
|
|
14
|
+
function buildInsidePlacementStoryFixes(ctx, fixer) {
|
|
15
|
+
const { sourceCode, node, insertPos, indent } = ctx;
|
|
16
|
+
const fixes = [];
|
|
17
|
+
const beforeComments = sourceCode.getCommentsBefore(node) || [];
|
|
18
|
+
const removableComments = beforeComments.filter((c) => /@story\b/.test(c.value) ||
|
|
19
|
+
/@req\b/.test(c.value) ||
|
|
20
|
+
/@supports\b/.test(c.value));
|
|
21
|
+
removableComments.forEach((comment) => {
|
|
22
|
+
fixes.push(fixer.remove(comment));
|
|
23
|
+
});
|
|
24
|
+
fixes.push(fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`));
|
|
25
|
+
return fixes;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a fixer function that inserts or migrates a @story comment for a
|
|
29
|
+
* missing branch annotation, honoring the configured placement mode.
|
|
30
|
+
* When annotationPlacement is "inside", this helper uses
|
|
31
|
+
* buildInsidePlacementStoryFixes to migrate existing before-branch
|
|
32
|
+
* annotations into the standardized inside-brace location. Otherwise, it
|
|
33
|
+
* preserves the original "before" behavior of inserting directly above
|
|
34
|
+
* the branch.
|
|
35
|
+
*
|
|
36
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
37
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-AUTO-FIX-MIGRATION REQ-INDENTATION-CORRECT
|
|
38
|
+
*/
|
|
39
|
+
function createStoryFixer(ctx) {
|
|
40
|
+
const { annotationPlacement, insertPos, indent } = ctx;
|
|
41
|
+
function insertStoryFixer(fixer) {
|
|
42
|
+
if (annotationPlacement === "inside") {
|
|
43
|
+
return buildInsidePlacementStoryFixes(ctx, fixer);
|
|
44
|
+
}
|
|
45
|
+
return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`);
|
|
46
|
+
}
|
|
47
|
+
return insertStoryFixer;
|
|
48
|
+
}
|
|
@@ -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;
|