eslint-plugin-traceability 1.19.3 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/README.md +43 -0
- package/lib/src/rules/helpers/require-story-core.js +9 -0
- package/lib/src/rules/helpers/require-story-helpers.d.ts +2 -1
- package/lib/src/rules/helpers/require-story-helpers.js +4 -1
- package/lib/src/rules/require-req-annotation.d.ts +4 -0
- package/lib/src/rules/require-req-annotation.js +3 -0
- package/lib/src/rules/require-story-annotation.d.ts +4 -0
- package/lib/src/rules/require-story-annotation.js +4 -0
- package/lib/src/utils/branch-annotation-helpers.d.ts +2 -0
- package/lib/src/utils/branch-annotation-helpers.js +9 -9
- 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/function-annotation-helpers.d.ts +23 -0
- package/lib/src/utils/function-annotation-helpers.js +101 -0
- package/lib/tests/rules/require-branch-annotation.test.js +8 -72
- package/package.json +1 -1
- package/user-docs/api-reference.md +6 -3
- package/user-docs/migration-guide.md +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# [1.20.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.4...v1.20.0) (2025-12-18)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* extend branch placement standard docs and helpers ([ae05a52](https://github.com/voder-ai/eslint-plugin-traceability/commit/ae05a5232b724060f7b1baae9a6be6eedf466617))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -89,6 +89,49 @@ 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
|
+
Function-level rules now support the same placement configuration model as branches:
|
|
127
|
+
- By default, annotations are still placed immediately before the function (JSDoc or line comments).
|
|
128
|
+
- When you configure `annotationPlacement: "inside"` on `traceability/require-story-annotation`, the rule prefers annotations as the first comment-only lines inside the function or method body, mirroring the branch-level inside-brace standard from Story 028.0. Declaration-only shapes such as `TSDeclareFunction` and `TSMethodSignature` remain before-function only, since they have no executable body.
|
|
129
|
+
|
|
130
|
+
For full configuration details and migration guidance between placement styles, see:
|
|
131
|
+
|
|
132
|
+
- `traceability/require-branch-annotation` rule docs: [docs/rules/require-branch-annotation.md](docs/rules/require-branch-annotation.md)
|
|
133
|
+
- Migration guide: [user-docs/migration-guide.md](user-docs/migration-guide.md)
|
|
134
|
+
|
|
92
135
|
### Available Rules
|
|
93
136
|
|
|
94
137
|
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.
|
|
@@ -139,6 +139,10 @@ function createMissingStoryReportDescriptor(config) {
|
|
|
139
139
|
],
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
+
function resolveAnnotationPlacement(options) {
|
|
143
|
+
const raw = options?.annotationPlacement;
|
|
144
|
+
return raw === "inside" || raw === "before" ? raw : "before";
|
|
145
|
+
}
|
|
142
146
|
/**
|
|
143
147
|
* Core helper to report a missing @story annotation for a function-like node.
|
|
144
148
|
* This reporting utility delegates behavior to injected dependencies so that
|
|
@@ -153,6 +157,11 @@ function createMissingStoryReportDescriptor(config) {
|
|
|
153
157
|
function coreReportMissing(deps, context, sourceCode, config) {
|
|
154
158
|
const { node, target: passedTarget, options = {} } = config;
|
|
155
159
|
withSafeReporting("coreReportMissing", () => {
|
|
160
|
+
const annotationPlacement = resolveAnnotationPlacement(options);
|
|
161
|
+
if (typeof deps.hasStoryAnnotationWithPlacement === "function" &&
|
|
162
|
+
deps.hasStoryAnnotationWithPlacement(sourceCode, node, annotationPlacement)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
156
165
|
if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
157
166
|
return;
|
|
158
167
|
}
|
|
@@ -53,6 +53,7 @@ declare function leadingCommentsHasStory(node: any): boolean;
|
|
|
53
53
|
* @req REQ-ANNOTATION-REQUIRED - Detect existing story annotations in JSDoc or comments
|
|
54
54
|
*/
|
|
55
55
|
declare function hasStoryAnnotation(sourceCode: any, node: any): boolean;
|
|
56
|
+
declare const hasStoryAnnotationWithPlacement: typeof hasStoryAnnotation;
|
|
56
57
|
/**
|
|
57
58
|
* Determine AST node where annotation should be inserted
|
|
58
59
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -84,4 +85,4 @@ declare function reportMethod(context: Rule.RuleContext, sourceCode: any, config
|
|
|
84
85
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
85
86
|
* @req REQ-ANNOTATION-REQUIRED
|
|
86
87
|
*/
|
|
87
|
-
export { STORY_PATH, getAnnotationTemplate, shouldApplyAutoFix, isExportedNode, jsdocHasStory, commentsBeforeHasStory, leadingCommentsHasStory, hasStoryAnnotation, getNodeName, extractName, resolveTargetNode, shouldProcessNode, DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory, reportMissing, reportMethod, };
|
|
88
|
+
export { STORY_PATH, getAnnotationTemplate, shouldApplyAutoFix, isExportedNode, jsdocHasStory, commentsBeforeHasStory, leadingCommentsHasStory, hasStoryAnnotation, hasStoryAnnotationWithPlacement, getNodeName, extractName, resolveTargetNode, shouldProcessNode, DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory, reportMissing, reportMethod, };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.getNodeName = exports.STORY_PATH = void 0;
|
|
3
|
+
exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.getNodeName = exports.hasStoryAnnotationWithPlacement = exports.STORY_PATH = void 0;
|
|
4
4
|
exports.getAnnotationTemplate = getAnnotationTemplate;
|
|
5
5
|
exports.shouldApplyAutoFix = shouldApplyAutoFix;
|
|
6
6
|
exports.isExportedNode = isExportedNode;
|
|
@@ -220,6 +220,9 @@ function hasStoryAnnotation(sourceCode, node) {
|
|
|
220
220
|
}
|
|
221
221
|
return false;
|
|
222
222
|
}
|
|
223
|
+
// Placement-aware alias reserved for future inside-brace function placement.
|
|
224
|
+
const hasStoryAnnotationWithPlacement = hasStoryAnnotation;
|
|
225
|
+
exports.hasStoryAnnotationWithPlacement = hasStoryAnnotationWithPlacement;
|
|
223
226
|
/**
|
|
224
227
|
* Determine AST node where annotation should be inserted
|
|
225
228
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
|
|
7
7
|
* @req REQ-CONFIGURABLE-SCOPE - Allow configuration of which exports are checked
|
|
8
8
|
* @req REQ-EXPORT-PRIORITY - Allow configuration of export priority behavior
|
|
9
|
+
*
|
|
10
|
+
* Note: This rule accepts annotationPlacement for configuration parity with
|
|
11
|
+
* require-story-annotation, but currently still requires annotations before
|
|
12
|
+
* the function (no inside-function support yet).
|
|
9
13
|
*/
|
|
10
14
|
import type { Rule } from "eslint";
|
|
11
15
|
declare const rule: Rule.RuleModule;
|
|
@@ -17,6 +17,10 @@ import type { Rule } from "eslint";
|
|
|
17
17
|
/**
|
|
18
18
|
* ESLint rule to require @story annotations on functions/methods.
|
|
19
19
|
*
|
|
20
|
+
* This rule participates in Story 028.0 placement standardization by supporting
|
|
21
|
+
* configurable annotation placement, including inside-brace function annotations
|
|
22
|
+
* when configured.
|
|
23
|
+
*
|
|
20
24
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
21
25
|
* @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
|
|
22
26
|
* @story docs/stories/008.0-DEV-AUTO-FIX.story.md
|
|
@@ -5,6 +5,10 @@ const require_story_helpers_1 = require("./helpers/require-story-helpers");
|
|
|
5
5
|
/**
|
|
6
6
|
* ESLint rule to require @story annotations on functions/methods.
|
|
7
7
|
*
|
|
8
|
+
* This rule participates in Story 028.0 placement standardization by supporting
|
|
9
|
+
* configurable annotation placement, including inside-brace function annotations
|
|
10
|
+
* when configured.
|
|
11
|
+
*
|
|
8
12
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
9
13
|
* @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
|
|
10
14
|
* @story docs/stories/008.0-DEV-AUTO-FIX.story.md
|
|
@@ -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.
|
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "reportMissingAnnotations", { enumerable: true, g
|
|
|
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
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
|
|
@@ -402,21 +403,20 @@ function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement =
|
|
|
402
403
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
403
404
|
*/
|
|
404
405
|
function reportMissingStory(context, node, options) {
|
|
405
|
-
const { indent, insertPos, storyFixCountRef } = options;
|
|
406
|
+
const { indent, insertPos, storyFixCountRef, annotationPlacement, sourceCode, } = options;
|
|
406
407
|
/**
|
|
407
408
|
* Conditional branch deciding whether to offer an auto-fix for the missing story.
|
|
408
409
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
409
410
|
* @req REQ-TRACEABILITY-FIX-DECISION - Trace decision to provide fixer for missing @story
|
|
410
411
|
*/
|
|
411
412
|
if (storyFixCountRef.count === 0) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
413
|
+
const insertStoryFixer = (0, branch_annotation_story_fix_helpers_1.createStoryFixer)({
|
|
414
|
+
annotationPlacement,
|
|
415
|
+
sourceCode,
|
|
416
|
+
node,
|
|
417
|
+
insertPos,
|
|
418
|
+
indent,
|
|
419
|
+
});
|
|
420
420
|
context.report({
|
|
421
421
|
node,
|
|
422
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,23 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Determine whether a function-like node can support inside-brace
|
|
4
|
+
* placement semantics. Only nodes with a concrete BlockStatement body are
|
|
5
|
+
* eligible; TypeScript declarations and signature-only nodes are
|
|
6
|
+
* intentionally excluded.
|
|
7
|
+
*
|
|
8
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-ALL-BLOCK-TYPES
|
|
9
|
+
*/
|
|
10
|
+
export declare function supportsInsidePlacementForFunction(node: any): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Gather the concatenated comment text from the first contiguous
|
|
13
|
+
* comment-only lines inside a function body. When no such comments are
|
|
14
|
+
* present or the node has no block body, an empty string is returned.
|
|
15
|
+
*
|
|
16
|
+
* This mirrors the branch helpers' behaviour for inside-brace placement
|
|
17
|
+
* (for example, simple if-statements and switch cases) so that
|
|
18
|
+
* function-level rules can share the same mental model: annotations live
|
|
19
|
+
* on the first comment-only line(s) inside the body braces.
|
|
20
|
+
*
|
|
21
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-ALL-BLOCK-TYPES
|
|
22
|
+
*/
|
|
23
|
+
export declare function getFunctionInsideBodyCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.supportsInsidePlacementForFunction = supportsInsidePlacementForFunction;
|
|
4
|
+
exports.getFunctionInsideBodyCommentText = getFunctionInsideBodyCommentText;
|
|
5
|
+
const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
|
|
6
|
+
/**
|
|
7
|
+
* Helpers for determining function-body annotation placement.
|
|
8
|
+
*
|
|
9
|
+
* These utilities are shared between the function-level traceability rules
|
|
10
|
+
* (require-story-annotation, require-req-annotation) so they can honour the
|
|
11
|
+
* same "inside" placement semantics used by branch rules when
|
|
12
|
+
* `annotationPlacement: "inside"` is configured.
|
|
13
|
+
*
|
|
14
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-ALL-BLOCK-TYPES REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Locate the BlockStatement that represents the executable body of a
|
|
18
|
+
* function-like construct. Returns null when the node has no block body
|
|
19
|
+
* (for example, TypeScript declarations or arrow functions with
|
|
20
|
+
* expression bodies).
|
|
21
|
+
*
|
|
22
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-ALL-BLOCK-TYPES
|
|
23
|
+
*/
|
|
24
|
+
function getFunctionBodyBlock(node) {
|
|
25
|
+
if (!node || typeof node.type !== "string") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (node.type === "FunctionDeclaration" ||
|
|
29
|
+
node.type === "FunctionExpression" ||
|
|
30
|
+
node.type === "ArrowFunctionExpression") {
|
|
31
|
+
const body = node.body;
|
|
32
|
+
return body && body.type === "BlockStatement" ? body : null;
|
|
33
|
+
}
|
|
34
|
+
if (node.type === "MethodDefinition") {
|
|
35
|
+
const value = node.value;
|
|
36
|
+
if (value && value.type === "FunctionExpression") {
|
|
37
|
+
const body = value.body;
|
|
38
|
+
return body && body.type === "BlockStatement" ? body : null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Determine whether a function-like node can support inside-brace
|
|
45
|
+
* placement semantics. Only nodes with a concrete BlockStatement body are
|
|
46
|
+
* eligible; TypeScript declarations and signature-only nodes are
|
|
47
|
+
* intentionally excluded.
|
|
48
|
+
*
|
|
49
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-ALL-BLOCK-TYPES
|
|
50
|
+
*/
|
|
51
|
+
function supportsInsidePlacementForFunction(node) {
|
|
52
|
+
return !!getFunctionBodyBlock(node);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Gather the concatenated comment text from the first contiguous
|
|
56
|
+
* comment-only lines inside a function body. When no such comments are
|
|
57
|
+
* present or the node has no block body, an empty string is returned.
|
|
58
|
+
*
|
|
59
|
+
* This mirrors the branch helpers' behaviour for inside-brace placement
|
|
60
|
+
* (for example, simple if-statements and switch cases) so that
|
|
61
|
+
* function-level rules can share the same mental model: annotations live
|
|
62
|
+
* on the first comment-only line(s) inside the body braces.
|
|
63
|
+
*
|
|
64
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-ALL-BLOCK-TYPES
|
|
65
|
+
*/
|
|
66
|
+
function getFunctionInsideBodyCommentText(sourceCode, node) {
|
|
67
|
+
const block = getFunctionBodyBlock(node);
|
|
68
|
+
if (!block ||
|
|
69
|
+
!block.loc ||
|
|
70
|
+
!block.loc.start ||
|
|
71
|
+
!block.loc.end ||
|
|
72
|
+
typeof block.loc.start.line !== "number" ||
|
|
73
|
+
typeof block.loc.end.line !== "number") {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
const getCommentsInside = sourceCode.getCommentsInside;
|
|
77
|
+
if (typeof getCommentsInside === "function") {
|
|
78
|
+
try {
|
|
79
|
+
const insideComments = getCommentsInside(block) || [];
|
|
80
|
+
const insideText = insideComments
|
|
81
|
+
.filter((c) => c && typeof c.value === "string")
|
|
82
|
+
.map((c) => c.value)
|
|
83
|
+
.join(" ");
|
|
84
|
+
if (insideText) {
|
|
85
|
+
return insideText;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Fall through to the line-based fallback when structured comment
|
|
90
|
+
// retrieval is unavailable or fails.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const lines = sourceCode.lines;
|
|
94
|
+
if (!Array.isArray(lines)) {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
const startIndex = block.loc.start.line - 1;
|
|
98
|
+
const endIndex = block.loc.end.line - 1;
|
|
99
|
+
const insideText = (0, branch_annotation_helpers_1.scanCommentLinesInRange)(lines, startIndex + 1, endIndex);
|
|
100
|
+
return insideText || "";
|
|
101
|
+
}
|
|
@@ -473,16 +473,7 @@ try {
|
|
|
473
473
|
handleError(error);
|
|
474
474
|
}`,
|
|
475
475
|
options: [{ annotationPlacement: "inside" }],
|
|
476
|
-
output:
|
|
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
|
-
}`,
|
|
476
|
+
output: "\n\ntry {\n // @story <story-file>.story.md\n doSomething();\n} catch (error) {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n // @req REQ-INSIDE-CATCH\n handleError(error);\n}",
|
|
486
477
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
487
478
|
},
|
|
488
479
|
{
|
|
@@ -493,12 +484,7 @@ if (condition) {
|
|
|
493
484
|
doSomething();
|
|
494
485
|
}`,
|
|
495
486
|
options: [{ annotationPlacement: "inside" }],
|
|
496
|
-
output:
|
|
497
|
-
// @req REQ-BEFORE-BRACE-ERROR
|
|
498
|
-
if (condition) {
|
|
499
|
-
// @story <story-file>.story.md
|
|
500
|
-
doSomething();
|
|
501
|
-
}`,
|
|
487
|
+
output: "\n\nif (condition) {\n // @story <story-file>.story.md\n doSomething();\n}",
|
|
502
488
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
503
489
|
},
|
|
504
490
|
{
|
|
@@ -509,12 +495,7 @@ for (const item of items) {
|
|
|
509
495
|
process(item);
|
|
510
496
|
}`,
|
|
511
497
|
options: [{ annotationPlacement: "inside" }],
|
|
512
|
-
output:
|
|
513
|
-
// @req REQ-LOOP-BEFORE
|
|
514
|
-
// @story <story-file>.story.md
|
|
515
|
-
for (const item of items) {
|
|
516
|
-
process(item);
|
|
517
|
-
}`,
|
|
498
|
+
output: "\n\nfor (const item of items) {\n // @story <story-file>.story.md\n process(item);\n}",
|
|
518
499
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
519
500
|
},
|
|
520
501
|
{
|
|
@@ -530,17 +511,7 @@ catch (error) {
|
|
|
530
511
|
handleError(error);
|
|
531
512
|
}`,
|
|
532
513
|
options: [{ annotationPlacement: "inside" }],
|
|
533
|
-
output:
|
|
534
|
-
// @req REQ-BRANCH-TRY
|
|
535
|
-
// @story <story-file>.story.md
|
|
536
|
-
try {
|
|
537
|
-
doSomething();
|
|
538
|
-
}
|
|
539
|
-
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
540
|
-
// @req REQ-CATCH-BEFORE
|
|
541
|
-
catch (error) {
|
|
542
|
-
handleError(error);
|
|
543
|
-
}`,
|
|
514
|
+
output: "\n\ntry {\n // @story <story-file>.story.md\n doSomething();\n}\n// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n// @req REQ-CATCH-BEFORE\ncatch (error) {\n handleError(error);\n}",
|
|
544
515
|
errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
|
|
545
516
|
},
|
|
546
517
|
{
|
|
@@ -553,14 +524,7 @@ try {
|
|
|
553
524
|
cleanup();
|
|
554
525
|
}`,
|
|
555
526
|
options: [{ annotationPlacement: "inside" }],
|
|
556
|
-
output:
|
|
557
|
-
// @req REQ-TRY-BEFORE
|
|
558
|
-
// @story <story-file>.story.md
|
|
559
|
-
try {
|
|
560
|
-
doWork();
|
|
561
|
-
} finally {
|
|
562
|
-
cleanup();
|
|
563
|
-
}`,
|
|
527
|
+
output: "\n\ntry {\n // @story <story-file>.story.md\n doWork();\n} finally {\n cleanup();\n}",
|
|
564
528
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
565
529
|
},
|
|
566
530
|
{
|
|
@@ -576,17 +540,7 @@ else if (b) {
|
|
|
576
540
|
doB();
|
|
577
541
|
}`,
|
|
578
542
|
options: [{ annotationPlacement: "inside" }],
|
|
579
|
-
output:
|
|
580
|
-
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
581
|
-
// @req REQ-OUTER-IF-INSIDE
|
|
582
|
-
doA();
|
|
583
|
-
}
|
|
584
|
-
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
585
|
-
// @req REQ-ELSE-IF-BEFORE
|
|
586
|
-
else if (b) {
|
|
587
|
-
// @story <story-file>.story.md
|
|
588
|
-
doB();
|
|
589
|
-
}`,
|
|
543
|
+
output: "if (a) {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n // @req REQ-OUTER-IF-INSIDE\n doA();\n}\n// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n// @req REQ-ELSE-IF-BEFORE\nelse if (b) {\n // @story <story-file>.story.md\n doB();\n}",
|
|
590
544
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
591
545
|
},
|
|
592
546
|
{
|
|
@@ -603,18 +557,7 @@ if (a) {
|
|
|
603
557
|
doC();
|
|
604
558
|
}`,
|
|
605
559
|
options: [{ annotationPlacement: "inside" }],
|
|
606
|
-
output:
|
|
607
|
-
// @req REQ-INSIDE-OUTER-IF
|
|
608
|
-
if (a) {
|
|
609
|
-
// @story <story-file>.story.md
|
|
610
|
-
doA();
|
|
611
|
-
} else if (b) {
|
|
612
|
-
// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
|
|
613
|
-
// @req REQ-INSIDE-ELSE-IF
|
|
614
|
-
doB();
|
|
615
|
-
} else {
|
|
616
|
-
doC();
|
|
617
|
-
}`,
|
|
560
|
+
output: "\n\nif (a) {\n // @story <story-file>.story.md\n doA();\n} else if (b) {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n // @req REQ-INSIDE-ELSE-IF\n doB();\n} else {\n doC();\n}",
|
|
618
561
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
619
562
|
},
|
|
620
563
|
{
|
|
@@ -627,14 +570,7 @@ if (a) {
|
|
|
627
570
|
}
|
|
628
571
|
}`,
|
|
629
572
|
options: [{ annotationPlacement: "inside" }],
|
|
630
|
-
output:
|
|
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
|
-
}
|
|
637
|
-
}`,
|
|
573
|
+
output: "switch (value) {\n \n \n // @story <story-file>.story.md\n case 'a': {\n doSomething();\n }\n}",
|
|
638
574
|
errors: makeMissingAnnotationErrors("@story", "@req"),
|
|
639
575
|
},
|
|
640
576
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
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",
|
|
@@ -26,7 +26,7 @@ For function-level traceability, the plugin exposes a unified rule and two legac
|
|
|
26
26
|
- `traceability/require-traceability` is the **canonical function-level rule** for new configurations. It ensures functions and methods have both story coverage and requirement coverage, and it accepts either `@supports` (preferred) or legacy `@story` / `@req` annotations.
|
|
27
27
|
- `traceability/require-story-annotation` and `traceability/require-req-annotation` are **backward-compatible aliases** that focus on the story and requirement aspects separately. They are retained for existing configurations and share the same underlying implementation model as the unified rule, but new ESLint configs should normally rely on `traceability/require-traceability` rather than enabling these legacy keys directly.
|
|
28
28
|
|
|
29
|
-
All three rule keys can still be configured individually if you need fine-grained control (for example, to tune severities separately), but the recommended and strict presets enable `traceability/require-traceability` by default and keep the legacy keys primarily for projects that adopted them before the unified rule existed.
|
|
29
|
+
All three rule keys can still be configured individually if you need fine-grained control (for example, to tune severities separately), but the recommended and strict presets enable `traceability/require-traceability` by default and keep the legacy keys primarily for projects that adopted them before the unified rule existed. When the underlying function rules are configured with `annotationPlacement: "inside"`, the unified `require-traceability` rule honours inside-brace placement for function and method bodies in the same formatter-aware way that `require-branch-annotation` handles branches.
|
|
30
30
|
|
|
31
31
|
### traceability/require-traceability
|
|
32
32
|
|
|
@@ -44,6 +44,7 @@ Options:
|
|
|
44
44
|
- `methodAnnotationTemplate` (string, optional) – Overrides the default placeholder JSDoc used when inserting missing `@story` annotations for class methods and TypeScript method signatures. When omitted or blank, falls back to `annotationTemplate` if provided, otherwise the built-in template.
|
|
45
45
|
- `autoFix` (boolean, optional) – When set to `false`, disables all automatic fix behavior for this rule while retaining its suggestions and diagnostics. When omitted or `true`, the rule behaves as before, inserting placeholder annotations in `--fix` mode.
|
|
46
46
|
- `excludeTestCallbacks` (boolean, optional) – When `true` (default), excludes anonymous arrow functions that are direct callbacks to common test framework functions (for example, Jest/Mocha/Vitest `describe`/`it`/`test`/`beforeEach`/`afterEach`/`beforeAll`/`afterAll`, plus focused/skipped/concurrent variants such as `fdescribe`, `xdescribe`, `fit`, `xit`, `test.concurrent`, `describe.concurrent`) from function-level annotation requirements. This assumes those test files are already covered by file-level `@supports` annotations and `traceability/require-test-traceability`. When set to `false`, these callbacks are treated like any other arrow function and must be annotated when in-scope.
|
|
47
|
+
- `annotationPlacement` ("before" | "inside", optional) – Controls whether the rule looks for annotations immediately before functions (`"before"`, the default and backward-compatible behaviour) or allows annotations as the first comment-only lines inside function and method bodies (`"inside"`). In inside mode, the rule continues to treat TypeScript declarations and signature-only nodes (such as `TSDeclareFunction` and `TSMethodSignature`) as before-function only, since they have no executable body.
|
|
47
48
|
|
|
48
49
|
Default Severity: `error`
|
|
49
50
|
Example:
|
|
@@ -70,6 +71,7 @@ Options:
|
|
|
70
71
|
|
|
71
72
|
- `scope` (string[], optional) – Controls which function-like node types are required to have @req annotations. Allowed values: "FunctionDeclaration", "FunctionExpression", "MethodDefinition", "TSDeclareFunction", "TSMethodSignature". Default: ["FunctionDeclaration", "FunctionExpression", "MethodDefinition", "TSDeclareFunction", "TSMethodSignature"].
|
|
72
73
|
- `exportPriority` ("all" | "exported" | "non-exported", optional) – Controls whether the rule checks all functions, only exported ones, or only non-exported ones. Default: "all".
|
|
74
|
+
- `annotationPlacement` ("before" | "inside", optional) – Accepted for configuration parity with `require-story-annotation`; requirement annotations are still evaluated using before-function comments and JSDoc today, so `"inside"` does not change behaviour yet.
|
|
73
75
|
|
|
74
76
|
Default Severity: `error`
|
|
75
77
|
Example (with both `@story` and `@req`, as typically used when both rules are enabled):
|
|
@@ -113,7 +115,7 @@ Behavior notes:
|
|
|
113
115
|
Placement modes:
|
|
114
116
|
|
|
115
117
|
- `"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.
|
|
118
|
+
- `"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. Story `028.0-DEV-FUNCTION-PLACEMENT` also extends this inside-brace standard to function and method bodies via `require-story-annotation` when configured, so projects can apply a single inside-brace rule consistently to both branches and functions.
|
|
117
119
|
|
|
118
120
|
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.
|
|
119
121
|
|
|
@@ -811,4 +813,5 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
|
|
|
811
813
|
In CI:
|
|
812
814
|
|
|
813
815
|
```bash
|
|
814
|
-
npm run traceability:verify
|
|
816
|
+
npm run traceability:verify
|
|
817
|
+
```
|
|
@@ -354,6 +354,46 @@ The full API reference documents all options, but the most important knobs for m
|
|
|
354
354
|
|
|
355
355
|
For most teams, the defaults in the recommended preset are a good starting point; you can then tune these options incrementally as your traceability style and `@supports` usage stabilize.
|
|
356
356
|
|
|
357
|
+
### 3.4 Inside-brace branch annotation placement (optional)
|
|
358
|
+
|
|
359
|
+
Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION introduces an **inside-brace** placement standard for branch annotations. Instead of placing annotations directly above a branch, you can configure `traceability/require-branch-annotation` to look for annotations as the first comment-only lines **inside** each block body.
|
|
360
|
+
|
|
361
|
+
The feature is controlled by the `annotationPlacement` option on `require-branch-annotation`:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
// eslint.config.js (flat config example)
|
|
365
|
+
import traceability from "eslint-plugin-traceability";
|
|
366
|
+
|
|
367
|
+
export default [
|
|
368
|
+
traceability.configs.recommended,
|
|
369
|
+
{
|
|
370
|
+
rules: {
|
|
371
|
+
"traceability/require-branch-annotation": [
|
|
372
|
+
"error",
|
|
373
|
+
{
|
|
374
|
+
annotationPlacement: "inside", // "before" (default) or "inside"
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
With `annotationPlacement: "inside"`, the rule expects annotations in these locations:
|
|
383
|
+
|
|
384
|
+
- `if` / `else if` / `else`: first comment-only lines inside the `{ ... }` block.
|
|
385
|
+
- Loops: first comment-only lines inside the loop body.
|
|
386
|
+
- `try` / `catch` / `finally`: first comment-only lines inside the corresponding block body.
|
|
387
|
+
- `switch` cases: first comment-only lines inside the `case` body when it is a block (`case 'a': { ... }`).
|
|
388
|
+
|
|
389
|
+
Before-brace annotations are still honored when you leave `annotationPlacement` at the default value (`"before"`), so you can migrate gradually:
|
|
390
|
+
|
|
391
|
+
1. **Start in default mode** — keep `annotationPlacement` unspecified (or set to `"before"`) and continue using your existing `// @story` / `// @req` comments above branches.
|
|
392
|
+
2. **Introduce inside-brace style for new code** — when adding or refactoring branches, place annotations on the first comment-only line inside the block body. This layout plays nicely with Prettier and is what the rule’s auto-fix uses for `if`/`else if` and similar branches.
|
|
393
|
+
3. **Opt-in to `annotationPlacement: "inside"`** — once your codebase is mostly using inside-brace annotations, enable the option. Branches that still rely only on before-brace comments will be reported as missing annotations in inside mode, and the rule’s autofix can insert placeholders at the correct inside location to help you complete the migration.
|
|
394
|
+
|
|
395
|
+
The default configuration in 1.x keeps `annotationPlacement` at `"before"` for backward compatibility, so existing projects do not need to change anything unless they want the new inside-brace behavior.
|
|
396
|
+
|
|
357
397
|
## 4. Test and Validate
|
|
358
398
|
|
|
359
399
|
Run your test suite to confirm everything passes:
|