eslint-plugin-traceability 1.14.0 → 1.15.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 +2 -2
- package/lib/src/index.js +2 -0
- package/lib/src/rules/helpers/require-story-core.js +1 -0
- package/lib/src/rules/helpers/require-story-helpers.d.ts +4 -0
- package/lib/src/rules/helpers/require-story-helpers.js +92 -3
- package/lib/src/rules/helpers/require-test-traceability-helpers.js +24 -0
- package/lib/src/rules/helpers/valid-annotation-options.d.ts +32 -1
- package/lib/src/rules/helpers/valid-annotation-options.js +144 -1
- package/lib/src/rules/helpers/valid-implements-utils.js +13 -5
- package/lib/src/rules/no-redundant-annotation.js +12 -0
- package/lib/src/rules/prefer-implements-annotation.js +39 -0
- package/lib/src/rules/require-branch-annotation.js +73 -1
- package/lib/src/rules/require-test-traceability.js +8 -0
- package/lib/src/rules/require-traceability.d.ts +19 -0
- package/lib/src/rules/require-traceability.js +73 -0
- package/lib/src/rules/valid-req-reference.js +4 -0
- package/lib/src/rules/valid-story-reference.d.ts +9 -0
- package/lib/src/rules/valid-story-reference.js +9 -0
- package/lib/src/utils/branch-annotation-helpers.d.ts +12 -10
- package/lib/src/utils/branch-annotation-helpers.js +31 -140
- package/lib/src/utils/branch-annotation-loop-helpers.d.ts +9 -0
- package/lib/src/utils/branch-annotation-loop-helpers.js +36 -0
- package/lib/src/utils/branch-annotation-report-helpers.d.ts +11 -0
- package/lib/src/utils/branch-annotation-report-helpers.js +111 -0
- package/lib/tests/config/flat-config-presets-integration.test.js +2 -0
- package/lib/tests/integration/dogfooding-validation.test.js +5 -2
- package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
- package/lib/tests/rules/require-branch-annotation.test.js +88 -19
- package/lib/tests/rules/require-story-annotation.test.js +56 -8
- package/lib/tests/utils/temp-dir-helpers.d.ts +6 -1
- package/lib/tests/utils/temp-dir-helpers.js +2 -1
- package/package.json +1 -1
- package/user-docs/api-reference.md +9 -6
|
@@ -175,6 +175,13 @@ function analyzeComment(comment) {
|
|
|
175
175
|
});
|
|
176
176
|
return { hasStory, hasReq, hasImplements, storyPaths };
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Check whether a given set of story paths represents multiple story/req
|
|
180
|
+
* blocks within the same comment, which cannot be safely auto-migrated.
|
|
181
|
+
*
|
|
182
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
183
|
+
* @req REQ-MIGRATE-INLINE
|
|
184
|
+
*/
|
|
178
185
|
function hasMultipleStories(storyPaths) {
|
|
179
186
|
// @req REQ-MULTI-STORY-DETECT - Use named threshold constant instead of a magic number
|
|
180
187
|
return storyPaths.size > MULTI_STORY_THRESHOLD;
|
|
@@ -215,10 +222,26 @@ function processBlockComment(comment, context) {
|
|
|
215
222
|
fix: fix ?? undefined,
|
|
216
223
|
});
|
|
217
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Extract the leading whitespace and `//` prefix from a line comment's full
|
|
227
|
+
* source text so that new inline annotations can be inserted with matching
|
|
228
|
+
* indentation and formatting.
|
|
229
|
+
*
|
|
230
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
231
|
+
* @req REQ-MIGRATE-INLINE
|
|
232
|
+
*/
|
|
218
233
|
function getLinePrefixFromText(fullText) {
|
|
219
234
|
const match = fullText.match(/^(\s*\/\/\s*)/);
|
|
220
235
|
return match ? match[1] : "";
|
|
221
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Attempt to construct an inline auto-fix that replaces a contiguous
|
|
239
|
+
* sequence of `@story` and `@req` line comments with a single `@supports`
|
|
240
|
+
* annotation while preserving the original comment prefix.
|
|
241
|
+
*
|
|
242
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
243
|
+
* @req REQ-MIGRATE-INLINE
|
|
244
|
+
*/
|
|
222
245
|
function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
|
|
223
246
|
const sourceCode = context.getSourceCode();
|
|
224
247
|
const storyComment = comments[storyIndex];
|
|
@@ -255,6 +278,14 @@ function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
|
|
|
255
278
|
const end = comments[reqIndices[reqIndices.length - 1]].range[1];
|
|
256
279
|
return (fixer) => fixer.replaceTextRange([start, end], implLine);
|
|
257
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Coordinate detection and optional migration of a single inline `@story`
|
|
283
|
+
* comment and its following `@req` comments, reporting diagnostics and
|
|
284
|
+
* scheduling auto-fixes where safe.
|
|
285
|
+
*
|
|
286
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
287
|
+
* @req REQ-MIGRATE-INLINE
|
|
288
|
+
*/
|
|
258
289
|
function handleInlineStorySequence(context, group, startIndex) {
|
|
259
290
|
const n = group.length;
|
|
260
291
|
const current = group[startIndex];
|
|
@@ -304,6 +335,14 @@ function handleInlineStorySequence(context, group, startIndex) {
|
|
|
304
335
|
}
|
|
305
336
|
return reqIndices[reqIndices.length - 1] + 1;
|
|
306
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Process a contiguous group of inline line comments, identifying legacy
|
|
340
|
+
* `@story`/`@req` sequences and scheduling the corresponding diagnostics
|
|
341
|
+
* and potential auto-fixes for migration to `@supports`.
|
|
342
|
+
*
|
|
343
|
+
* @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
|
|
344
|
+
* @req REQ-MIGRATE-INLINE
|
|
345
|
+
*/
|
|
307
346
|
function processInlineGroup(context, group) {
|
|
308
347
|
if (group.length === 0)
|
|
309
348
|
return;
|
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const branch_annotation_helpers_1 = require("../utils/branch-annotation-helpers");
|
|
4
|
+
/**
|
|
5
|
+
* @supports Switch case node detection for fall-through handling
|
|
6
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
7
|
+
* @req REQ-SWITCH-CASE-ANNOTATION
|
|
8
|
+
* @req REQ-SWITCH-DEFAULT-REQUIRED
|
|
9
|
+
* @req REQ-SWITCH-FALLTHROUGH
|
|
10
|
+
*/
|
|
11
|
+
function isSwitchCaseNode(node) {
|
|
12
|
+
return (!!node && typeof node === "object" && node.type === "SwitchCase");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sentinel index value used when a SwitchCase is not found in its parent's cases array.
|
|
16
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SWITCH-FALLTHROUGH
|
|
17
|
+
*/
|
|
18
|
+
const INVALID_INDEX = -1;
|
|
19
|
+
/**
|
|
20
|
+
* Determine whether a SwitchCase is an intermediate fall-through label
|
|
21
|
+
* that should not require its own annotation.
|
|
22
|
+
*
|
|
23
|
+
* An intermediate fall-through case:
|
|
24
|
+
* - Has an empty consequent array
|
|
25
|
+
* - Has a following SwitchCase sibling in the same SwitchStatement
|
|
26
|
+
* - That following sibling has a non-empty consequent array
|
|
27
|
+
*
|
|
28
|
+
* @supports Switch fall-through behavior for branch annotations
|
|
29
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
30
|
+
* @req REQ-SWITCH-CASE-ANNOTATION
|
|
31
|
+
* @req REQ-SWITCH-DEFAULT-REQUIRED
|
|
32
|
+
* @req REQ-SWITCH-FALLTHROUGH
|
|
33
|
+
*/
|
|
34
|
+
function isFallthroughIntermediateCase(node) {
|
|
35
|
+
if (!isSwitchCaseNode(node))
|
|
36
|
+
return false;
|
|
37
|
+
// Default cases must always be annotated when they represent a branch.
|
|
38
|
+
if (node.test == null) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(node.consequent) || node.consequent.length > 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const parent = node.parent;
|
|
45
|
+
if (!parent ||
|
|
46
|
+
parent.type !== "SwitchStatement" ||
|
|
47
|
+
!Array.isArray(parent.cases)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const cases = parent.cases;
|
|
51
|
+
const index = cases.indexOf(node);
|
|
52
|
+
if (index === INVALID_INDEX) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// Walk forward from this case until we either find a case with a non-empty
|
|
56
|
+
// consequent (shared body) or run out of cases. All empty cases in this
|
|
57
|
+
// prefix are treated as intermediate labels that participate in fall-through
|
|
58
|
+
// but do not themselves require annotations. The last case with the shared
|
|
59
|
+
// body remains subject to annotation requirements.
|
|
60
|
+
let j = index;
|
|
61
|
+
while (j < cases.length &&
|
|
62
|
+
(!Array.isArray(cases[j].consequent) || cases[j].consequent.length === 0)) {
|
|
63
|
+
j++;
|
|
64
|
+
}
|
|
65
|
+
if (j >= cases.length) {
|
|
66
|
+
// No later case with a body; treat this as an independent branch that
|
|
67
|
+
// should be annotated when appropriate.
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
4
72
|
/**
|
|
5
73
|
* ESLint rule definition for require-branch-annotation.
|
|
6
74
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -66,7 +134,11 @@ const rule = {
|
|
|
66
134
|
* @req REQ-CONFIGURABLE-SCOPE
|
|
67
135
|
*/
|
|
68
136
|
handlers[type] = function branchHandler(node) {
|
|
69
|
-
if (type === "SwitchCase" &&
|
|
137
|
+
if (type === "SwitchCase" &&
|
|
138
|
+
isSwitchCaseNode(node) &&
|
|
139
|
+
isFallthroughIntermediateCase(node)) {
|
|
140
|
+
// Skip intermediate fall-through labels; only the last case before a shared code block
|
|
141
|
+
// requires its own annotation per REQ-SWITCH-FALLTHROUGH.
|
|
70
142
|
return;
|
|
71
143
|
}
|
|
72
144
|
(0, branch_annotation_helpers_1.reportMissingAnnotations)(context, node, storyFixCountRef);
|
|
@@ -72,6 +72,14 @@ const rule = {
|
|
|
72
72
|
missingReqPrefix: "Test name should start with requirement ID (e.g., '[REQ-MAINT-DETECT] ...').",
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
|
+
/**
|
|
76
|
+
* Wire up the ESLint rule visitors that enforce test traceability conventions
|
|
77
|
+
* for supported test frameworks.
|
|
78
|
+
*
|
|
79
|
+
* @story docs/stories/020.0-DEV-TEST-ANNOTATION-VALIDATION.story.md
|
|
80
|
+
* @req REQ-TEST-FILE-SUPPORTS REQ-TEST-DESCRIBE-STORY REQ-TEST-IT-REQ-PREFIX
|
|
81
|
+
* @supports docs/stories/021.0-DEV-TEST-ANNOTATION-AUTO-FIX.story.md REQ-TEST-FIX-TEMPLATE REQ-TEST-FIX-PREFIX-FORMAT
|
|
82
|
+
*/
|
|
75
83
|
create(context) {
|
|
76
84
|
const filename = context.getFilename();
|
|
77
85
|
const rawOptions = (context.options && context.options[0]) || {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite ESLint rule that enforces both story and requirement traceability
|
|
3
|
+
* annotations on functions and methods.
|
|
4
|
+
*
|
|
5
|
+
* Implements Story 003.0-DEV-FUNCTION-ANNOTATIONS with:
|
|
6
|
+
* - REQ-ANNOTATION-REQUIRED
|
|
7
|
+
* - REQ-FUNCTION-DETECTION
|
|
8
|
+
* - REQ-CONFIGURABLE-SCOPE
|
|
9
|
+
* - REQ-EXPORT-PRIORITY
|
|
10
|
+
* - REQ-ERROR-LOCATION
|
|
11
|
+
* - REQ-TYPESCRIPT-SUPPORT
|
|
12
|
+
*
|
|
13
|
+
* via composition of:
|
|
14
|
+
* - ./require-story-annotation
|
|
15
|
+
* - ./require-req-annotation
|
|
16
|
+
*/
|
|
17
|
+
import type { Rule } from "eslint";
|
|
18
|
+
declare const rule: Rule.RuleModule;
|
|
19
|
+
export default rule;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Composite ESLint rule that enforces both story and requirement traceability
|
|
4
|
+
* annotations on functions and methods.
|
|
5
|
+
*
|
|
6
|
+
* Implements Story 003.0-DEV-FUNCTION-ANNOTATIONS with:
|
|
7
|
+
* - REQ-ANNOTATION-REQUIRED
|
|
8
|
+
* - REQ-FUNCTION-DETECTION
|
|
9
|
+
* - REQ-CONFIGURABLE-SCOPE
|
|
10
|
+
* - REQ-EXPORT-PRIORITY
|
|
11
|
+
* - REQ-ERROR-LOCATION
|
|
12
|
+
* - REQ-TYPESCRIPT-SUPPORT
|
|
13
|
+
*
|
|
14
|
+
* via composition of:
|
|
15
|
+
* - ./require-story-annotation
|
|
16
|
+
* - ./require-req-annotation
|
|
17
|
+
*/
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
const require_story_annotation_1 = __importDefault(require("./require-story-annotation"));
|
|
23
|
+
const require_req_annotation_1 = __importDefault(require("./require-req-annotation"));
|
|
24
|
+
const storyRule = require_story_annotation_1.default;
|
|
25
|
+
const reqRule = require_req_annotation_1.default;
|
|
26
|
+
const rule = {
|
|
27
|
+
meta: {
|
|
28
|
+
type: "problem",
|
|
29
|
+
docs: {
|
|
30
|
+
description: "Require @story and @req (or @supports) annotations on functions and methods",
|
|
31
|
+
recommended: "error",
|
|
32
|
+
},
|
|
33
|
+
hasSuggestions: Boolean(storyRule.meta?.hasSuggestions) ||
|
|
34
|
+
Boolean(reqRule.meta?.hasSuggestions),
|
|
35
|
+
fixable: (storyRule.meta && storyRule.meta.fixable) ||
|
|
36
|
+
(reqRule.meta && reqRule.meta.fixable) ||
|
|
37
|
+
undefined,
|
|
38
|
+
messages: {
|
|
39
|
+
...(storyRule.meta?.messages ?? {}),
|
|
40
|
+
...(reqRule.meta?.messages ?? {}),
|
|
41
|
+
},
|
|
42
|
+
schema: (storyRule.meta && storyRule.meta.schema) ??
|
|
43
|
+
(reqRule.meta && reqRule.meta.schema) ??
|
|
44
|
+
[],
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
const storyListeners = storyRule.create(context) || {};
|
|
48
|
+
const reqListeners = reqRule.create(context) || {};
|
|
49
|
+
const mergedListener = {};
|
|
50
|
+
const allEventNames = new Set([
|
|
51
|
+
...Object.keys(storyListeners),
|
|
52
|
+
...Object.keys(reqListeners),
|
|
53
|
+
]);
|
|
54
|
+
for (const eventName of allEventNames) {
|
|
55
|
+
const storyHandler = storyListeners[eventName];
|
|
56
|
+
const reqHandler = reqListeners[eventName];
|
|
57
|
+
if (storyHandler && reqHandler) {
|
|
58
|
+
mergedListener[eventName] = function mergedHandler(...args) {
|
|
59
|
+
storyHandler.apply(this, args);
|
|
60
|
+
reqHandler.apply(this, args);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
else if (storyHandler) {
|
|
64
|
+
mergedListener[eventName] = storyHandler;
|
|
65
|
+
}
|
|
66
|
+
else if (reqHandler) {
|
|
67
|
+
mergedListener[eventName] = reqHandler;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return mergedListener;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
exports.default = rule;
|
|
@@ -31,6 +31,10 @@ exports.default = {
|
|
|
31
31
|
/**
|
|
32
32
|
* Rule create entrypoint that returns the Program visitor.
|
|
33
33
|
* Delegates to createValidReqReferenceProgramVisitor helper.
|
|
34
|
+
*
|
|
35
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
36
|
+
* @req REQ-REQ-VALIDATION - Validate that requirement references in annotations resolve to known requirements
|
|
37
|
+
* @req REQ-REQ-FILE-EXISTS - Ensure that referenced story files exist before validating requirement references
|
|
34
38
|
*/
|
|
35
39
|
create(context) {
|
|
36
40
|
return {
|
|
@@ -6,5 +6,14 @@
|
|
|
6
6
|
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
7
7
|
*/
|
|
8
8
|
import type { Rule } from "eslint";
|
|
9
|
+
/**
|
|
10
|
+
* ESLint rule factory: configures and returns visitors that validate story
|
|
11
|
+
* references in @story and @supports annotations across all comments.
|
|
12
|
+
*
|
|
13
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
14
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
15
|
+
* @req REQ-STORY-FILE-EXISTS - Ensure each referenced story in @story/@supports annotations points to an existing file
|
|
16
|
+
* @req REQ-STORY-CONTENT - Provide a foundation for deeper story content validation by guaranteeing valid references
|
|
17
|
+
*/
|
|
9
18
|
declare const _default: Rule.RuleModule;
|
|
10
19
|
export default _default;
|
|
@@ -195,6 +195,15 @@ function handleComment(opts) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* ESLint rule factory: configures and returns visitors that validate story
|
|
200
|
+
* references in @story and @supports annotations across all comments.
|
|
201
|
+
*
|
|
202
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
203
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
204
|
+
* @req REQ-STORY-FILE-EXISTS - Ensure each referenced story in @story/@supports annotations points to an existing file
|
|
205
|
+
* @req REQ-STORY-CONTENT - Provide a foundation for deeper story content validation by guaranteeing valid references
|
|
206
|
+
*/
|
|
198
207
|
exports.default = {
|
|
199
208
|
meta: {
|
|
200
209
|
type: "problem",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Rule } from "eslint";
|
|
2
|
+
import { reportMissingAnnotations } from "./branch-annotation-report-helpers";
|
|
2
3
|
/**
|
|
3
4
|
* Valid branch types for require-branch-annotation rule.
|
|
4
5
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -16,6 +17,16 @@ export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
|
|
|
16
17
|
* @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
|
|
17
18
|
*/
|
|
18
19
|
export declare function validateBranchTypes(context: Rule.RuleContext): BranchType[] | Rule.RuleListener;
|
|
20
|
+
/**
|
|
21
|
+
* Scan contiguous formatter-aware comment lines between the provided 0-based
|
|
22
|
+
* start and end indices (inclusive), stopping when a non-comment or blank line
|
|
23
|
+
* is encountered. This helper is used as a line-based fallback when
|
|
24
|
+
* structured comment APIs are not available for branch bodies.
|
|
25
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
26
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC
|
|
27
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC-ELSE-IF
|
|
28
|
+
*/
|
|
29
|
+
export declare function scanCommentLinesInRange(lines: string[], startIndex: number, endIndexInclusive: number): string;
|
|
19
30
|
/**
|
|
20
31
|
* Gather leading comment text for a branch node.
|
|
21
32
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -46,13 +57,4 @@ export declare function reportMissingReq(context: Rule.RuleContext, node: any, o
|
|
|
46
57
|
insertPos: number;
|
|
47
58
|
missingStory: boolean;
|
|
48
59
|
}): void;
|
|
49
|
-
|
|
50
|
-
* Report missing annotations on a branch node.
|
|
51
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
52
|
-
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
53
|
-
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
54
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
55
|
-
*/
|
|
56
|
-
export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
|
|
57
|
-
count: number;
|
|
58
|
-
}): void;
|
|
60
|
+
export { reportMissingAnnotations };
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_BRANCH_TYPES = void 0;
|
|
3
|
+
exports.reportMissingAnnotations = exports.DEFAULT_BRANCH_TYPES = void 0;
|
|
4
4
|
exports.validateBranchTypes = validateBranchTypes;
|
|
5
|
+
exports.scanCommentLinesInRange = scanCommentLinesInRange;
|
|
5
6
|
exports.gatherBranchCommentText = gatherBranchCommentText;
|
|
6
7
|
exports.reportMissingStory = reportMissingStory;
|
|
7
8
|
exports.reportMissingReq = reportMissingReq;
|
|
8
|
-
|
|
9
|
+
const branch_annotation_report_helpers_1 = require("./branch-annotation-report-helpers");
|
|
10
|
+
Object.defineProperty(exports, "reportMissingAnnotations", { enumerable: true, get: function () { return branch_annotation_report_helpers_1.reportMissingAnnotations; } });
|
|
11
|
+
const branch_annotation_loop_helpers_1 = require("./branch-annotation-loop-helpers");
|
|
9
12
|
const PRE_COMMENT_OFFSET = 2; // number of lines above branch to inspect for comments
|
|
10
13
|
/**
|
|
11
14
|
* Valid branch types for require-branch-annotation rule.
|
|
@@ -150,6 +153,7 @@ function scanCommentLinesInRange(lines, startIndex, endIndexInclusive) {
|
|
|
150
153
|
}
|
|
151
154
|
return comments.join(" ");
|
|
152
155
|
}
|
|
156
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
153
157
|
function isElseIfBranch(node, parent) {
|
|
154
158
|
return (node &&
|
|
155
159
|
node.type === "IfStatement" &&
|
|
@@ -294,6 +298,18 @@ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
|
|
|
294
298
|
}
|
|
295
299
|
return beforeText;
|
|
296
300
|
}
|
|
301
|
+
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
302
|
+
function gatherSwitchCaseCommentText(sourceCode, node) {
|
|
303
|
+
const lines = sourceCode.lines;
|
|
304
|
+
const startLine = node.loc.start.line;
|
|
305
|
+
let i = startLine - PRE_COMMENT_OFFSET;
|
|
306
|
+
const comments = [];
|
|
307
|
+
while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
|
|
308
|
+
comments.unshift(lines[i].trim());
|
|
309
|
+
i--;
|
|
310
|
+
}
|
|
311
|
+
return comments.join(" ");
|
|
312
|
+
}
|
|
297
313
|
/**
|
|
298
314
|
* Gather leading comment text for a branch node.
|
|
299
315
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
@@ -308,17 +324,7 @@ function gatherBranchCommentText(sourceCode, node, parent) {
|
|
|
308
324
|
* @req REQ-TRACEABILITY-SWITCHCASE-COMMENTS - Trace collection of preceding comments for SwitchCase
|
|
309
325
|
*/
|
|
310
326
|
if (node.type === "SwitchCase") {
|
|
311
|
-
|
|
312
|
-
const startLine = node.loc.start.line;
|
|
313
|
-
let i = startLine - PRE_COMMENT_OFFSET;
|
|
314
|
-
const comments = [];
|
|
315
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
316
|
-
// @req REQ-TRACEABILITY-WHILE - Trace while loop that collects preceding comments for SwitchCase
|
|
317
|
-
while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
|
|
318
|
-
comments.unshift(lines[i].trim());
|
|
319
|
-
i--;
|
|
320
|
-
}
|
|
321
|
-
return comments.join(" ");
|
|
327
|
+
return gatherSwitchCaseCommentText(sourceCode, node);
|
|
322
328
|
}
|
|
323
329
|
const beforeComments = sourceCode.getCommentsBefore(node) || [];
|
|
324
330
|
const beforeText = beforeComments.map(extractCommentValue).join(" ");
|
|
@@ -334,6 +340,18 @@ function gatherBranchCommentText(sourceCode, node, parent) {
|
|
|
334
340
|
if (node.type === "IfStatement") {
|
|
335
341
|
return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
|
|
336
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Conditional branch for loop nodes that may include annotations either on the loop
|
|
345
|
+
* statement itself or at the top of the loop body, allowing flexible placement.
|
|
346
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-LOOP-ANNOTATION REQ-LOOP-PLACEMENT-FLEXIBLE
|
|
347
|
+
*/
|
|
348
|
+
if (node.type === "ForStatement" ||
|
|
349
|
+
node.type === "ForInStatement" ||
|
|
350
|
+
node.type === "ForOfStatement" ||
|
|
351
|
+
node.type === "WhileStatement" ||
|
|
352
|
+
node.type === "DoWhileStatement") {
|
|
353
|
+
return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, beforeText);
|
|
354
|
+
}
|
|
337
355
|
return beforeText;
|
|
338
356
|
}
|
|
339
357
|
/**
|
|
@@ -409,130 +427,3 @@ function reportMissingReq(context, node, options) {
|
|
|
409
427
|
});
|
|
410
428
|
}
|
|
411
429
|
}
|
|
412
|
-
/**
|
|
413
|
-
* Compute the base indent and insert position for a branch node, including
|
|
414
|
-
* special handling for CatchClause bodies.
|
|
415
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
416
|
-
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
417
|
-
* @supports REQ-ANNOTATION-PARSING
|
|
418
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
419
|
-
*/
|
|
420
|
-
/**
|
|
421
|
-
* Compute indentation and insert position for the start of a given 1-based line
|
|
422
|
-
* number. This keeps indentation and fixer insert positions consistent across
|
|
423
|
-
* branch helpers that need to align auto-inserted comments with existing
|
|
424
|
-
* source formatting.
|
|
425
|
-
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-ANNOTATION-PARSING
|
|
426
|
-
*/
|
|
427
|
-
function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
|
|
428
|
-
const lines = sourceCode.lines;
|
|
429
|
-
let indent = fallbackIndent;
|
|
430
|
-
if (line >= 1 && line <= lines.length) {
|
|
431
|
-
const rawLine = lines[line - 1];
|
|
432
|
-
indent = rawLine.match(/^(\s*)/)?.[1] || fallbackIndent;
|
|
433
|
-
}
|
|
434
|
-
const insertPos = sourceCode.getIndexFromLoc({
|
|
435
|
-
line,
|
|
436
|
-
column: 0,
|
|
437
|
-
});
|
|
438
|
-
return { indent, insertPos };
|
|
439
|
-
}
|
|
440
|
-
function getBaseBranchIndentAndInsertPos(sourceCode, node) {
|
|
441
|
-
let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
|
|
442
|
-
if (node.type === "CatchClause" && node.body) {
|
|
443
|
-
const bodyNode = node.body;
|
|
444
|
-
const bodyStatements = Array.isArray(bodyNode.body)
|
|
445
|
-
? bodyNode.body
|
|
446
|
-
: undefined;
|
|
447
|
-
const firstStatement = bodyStatements && bodyStatements.length > 0
|
|
448
|
-
? bodyStatements[0]
|
|
449
|
-
: undefined;
|
|
450
|
-
if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
|
|
451
|
-
const firstLine = firstStatement.loc.start.line;
|
|
452
|
-
const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, "");
|
|
453
|
-
indent = firstLineInfo.indent;
|
|
454
|
-
insertPos = firstLineInfo.insertPos;
|
|
455
|
-
}
|
|
456
|
-
else if (bodyNode.loc && bodyNode.loc.start) {
|
|
457
|
-
const blockLine = bodyNode.loc.start.line;
|
|
458
|
-
const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, "");
|
|
459
|
-
const innerIndent = `${blockLineInfo.indent} `;
|
|
460
|
-
indent = innerIndent;
|
|
461
|
-
insertPos = blockLineInfo.insertPos;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
return { indent, insertPos };
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Compute annotation-related metadata for a branch node.
|
|
468
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
469
|
-
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
470
|
-
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
471
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
472
|
-
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
|
|
473
|
-
*/
|
|
474
|
-
function getBranchAnnotationInfo(sourceCode, node, parent) {
|
|
475
|
-
const text = gatherBranchCommentText(sourceCode, node, parent);
|
|
476
|
-
const hasSupports = /@supports\b/.test(text);
|
|
477
|
-
const missingStory = !/@story\b/.test(text) && !hasSupports;
|
|
478
|
-
const missingReq = !/@req\b/.test(text) && !hasSupports;
|
|
479
|
-
let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
|
|
480
|
-
if (isElseIfBranch(node, parent) &&
|
|
481
|
-
node.consequent &&
|
|
482
|
-
node.consequent.type === "BlockStatement" &&
|
|
483
|
-
node.consequent.loc &&
|
|
484
|
-
node.consequent.loc.start) {
|
|
485
|
-
// For else-if blocks, align auto-fix comments with Prettier's tendency to place comments
|
|
486
|
-
// inside the wrapped block body; non-block consequents intentionally keep the default behavior.
|
|
487
|
-
const commentLine = node.consequent.loc.start.line + 1;
|
|
488
|
-
const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
|
|
489
|
-
indent = commentLineInfo.indent;
|
|
490
|
-
insertPos = commentLineInfo.insertPos;
|
|
491
|
-
}
|
|
492
|
-
return { missingStory, missingReq, indent, insertPos };
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Report missing annotations on a branch node.
|
|
496
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
497
|
-
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
498
|
-
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
499
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
500
|
-
*/
|
|
501
|
-
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
502
|
-
const sourceCode = context.getSourceCode();
|
|
503
|
-
/**
|
|
504
|
-
* Determine the direct parent of the node using the parent reference on the node.
|
|
505
|
-
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
506
|
-
* @supports REQ-DUAL-POSITION-DETECTION
|
|
507
|
-
*/
|
|
508
|
-
const parent = node.parent;
|
|
509
|
-
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
|
|
510
|
-
const actions = [
|
|
511
|
-
{
|
|
512
|
-
missing: missingStory,
|
|
513
|
-
fn: reportMissingStory,
|
|
514
|
-
args: [context, node, { indent, insertPos, storyFixCountRef }],
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
missing: missingReq,
|
|
518
|
-
fn: reportMissingReq,
|
|
519
|
-
args: [context, node, { indent, insertPos, missingStory }],
|
|
520
|
-
},
|
|
521
|
-
];
|
|
522
|
-
/**
|
|
523
|
-
* Process a single action from the actions array.
|
|
524
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
525
|
-
* @req REQ-TRACEABILITY-ACTIONS-FOREACH - Trace processing of actions array to report missing annotations
|
|
526
|
-
*/
|
|
527
|
-
function processAction(item) {
|
|
528
|
-
/**
|
|
529
|
-
* Callback invoked for each action to decide and execute reporting.
|
|
530
|
-
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
531
|
-
* @req REQ-TRACEABILITY-FOR-EACH-CALLBACK - Trace callback handling for each action item
|
|
532
|
-
*/
|
|
533
|
-
if (item.missing) {
|
|
534
|
-
item.fn(...item.args);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
actions.forEach(processAction);
|
|
538
|
-
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Gather annotation text for loop branches, supporting annotations either on the
|
|
4
|
+
* loop statement itself or on the first comment lines inside the loop body.
|
|
5
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
6
|
+
* @req REQ-LOOP-ANNOTATION
|
|
7
|
+
* @req REQ-LOOP-PLACEMENT-FLEXIBLE
|
|
8
|
+
*/
|
|
9
|
+
export declare function gatherLoopCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, beforeText: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gatherLoopCommentText = gatherLoopCommentText;
|
|
4
|
+
const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
|
|
5
|
+
/**
|
|
6
|
+
* Gather annotation text for loop branches, supporting annotations either on the
|
|
7
|
+
* loop statement itself or on the first comment lines inside the loop body.
|
|
8
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
9
|
+
* @req REQ-LOOP-ANNOTATION
|
|
10
|
+
* @req REQ-LOOP-PLACEMENT-FLEXIBLE
|
|
11
|
+
*/
|
|
12
|
+
function gatherLoopCommentText(sourceCode, node, beforeText) {
|
|
13
|
+
if (/@story\b/.test(beforeText) ||
|
|
14
|
+
/@req\b/.test(beforeText) ||
|
|
15
|
+
/@supports\b/.test(beforeText)) {
|
|
16
|
+
return beforeText;
|
|
17
|
+
}
|
|
18
|
+
const body = node.body;
|
|
19
|
+
if (body &&
|
|
20
|
+
body.type === "BlockStatement" &&
|
|
21
|
+
body.loc &&
|
|
22
|
+
body.loc.start &&
|
|
23
|
+
body.loc.end) {
|
|
24
|
+
const lines = sourceCode.lines;
|
|
25
|
+
const startIndex = body.loc.start.line; // first line inside block body (start.line is 1-based)
|
|
26
|
+
const endIndex = body.loc.end.line - 1;
|
|
27
|
+
const insideText = (0, branch_annotation_helpers_1.scanCommentLinesInRange)(lines, startIndex, endIndex);
|
|
28
|
+
if (insideText &&
|
|
29
|
+
(/@story\b/.test(insideText) ||
|
|
30
|
+
/@req\b/.test(insideText) ||
|
|
31
|
+
/@supports\b/.test(insideText))) {
|
|
32
|
+
return insideText;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return beforeText;
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Report missing annotations on a branch node.
|
|
4
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
5
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
6
|
+
* @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
7
|
+
* @supports REQ-DUAL-POSITION-DETECTION
|
|
8
|
+
*/
|
|
9
|
+
export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
|
|
10
|
+
count: number;
|
|
11
|
+
}): void;
|