eslint-plugin-traceability 1.6.5 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -1
- package/lib/src/index.d.ts +30 -27
- package/lib/src/index.js +51 -31
- package/lib/src/maintenance/cli.d.ts +12 -0
- package/lib/src/maintenance/cli.js +279 -0
- package/lib/src/maintenance/detect.js +27 -12
- package/lib/src/maintenance/update.js +42 -34
- package/lib/src/maintenance/utils.js +30 -30
- package/lib/src/rules/helpers/require-story-io.js +51 -15
- package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
- package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
- package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
- package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
- package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
- package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
- package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
- package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
- package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
- package/lib/src/rules/prefer-implements-annotation.js +276 -0
- package/lib/src/rules/valid-annotation-format.js +255 -208
- package/lib/src/rules/valid-req-reference.js +210 -29
- package/lib/src/rules/valid-story-reference.d.ts +7 -0
- package/lib/src/rules/valid-story-reference.js +38 -80
- package/lib/src/utils/annotation-checker.js +2 -145
- package/lib/src/utils/branch-annotation-helpers.js +12 -3
- package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
- package/lib/src/utils/reqAnnotationDetection.js +152 -0
- package/lib/tests/maintenance/cli.test.d.ts +1 -0
- package/lib/tests/maintenance/cli.test.js +172 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
- package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
- package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
- package/lib/tests/rules/require-branch-annotation.test.js +3 -2
- package/lib/tests/rules/require-req-annotation.test.js +57 -68
- package/lib/tests/rules/require-story-annotation.test.js +13 -28
- package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
- package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
- package/lib/tests/rules/valid-annotation-format.test.js +395 -40
- package/lib/tests/rules/valid-req-reference.test.js +34 -0
- package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
- package/lib/tests/utils/annotation-checker.test.js +24 -17
- package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
- package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
- package/lib/tests/utils/ts-language-options.d.ts +22 -0
- package/lib/tests/utils/ts-language-options.js +27 -0
- package/package.json +12 -3
|
@@ -36,6 +36,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.updateAnnotationReferences = updateAnnotationReferences;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const utils_1 = require("./utils");
|
|
39
|
+
/**
|
|
40
|
+
* Helper to process a single file for annotation reference updates
|
|
41
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
42
|
+
* @req REQ-MAINT-UPDATE
|
|
43
|
+
*/
|
|
44
|
+
function processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef) {
|
|
45
|
+
const stat = fs.statSync(fullPath);
|
|
46
|
+
/**
|
|
47
|
+
* Skip non-files in iteration
|
|
48
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
49
|
+
* @req REQ-MAINT-UPDATE
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Skip entries that are not regular files (e.g., directories)
|
|
53
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
54
|
+
* @req REQ-MAINT-UPDATE
|
|
55
|
+
*/
|
|
56
|
+
if (!stat.isFile())
|
|
57
|
+
return;
|
|
58
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
59
|
+
const newContent = content.replace(regex,
|
|
60
|
+
/**
|
|
61
|
+
* Replacement callback to update annotation references
|
|
62
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
63
|
+
* @req REQ-MAINT-UPDATE
|
|
64
|
+
*/
|
|
65
|
+
(match, p1) => {
|
|
66
|
+
replacementCountRef.count++;
|
|
67
|
+
return `${p1}${newPath}`;
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Write file only if content changed
|
|
71
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
72
|
+
* @req REQ-MAINT-UPDATE
|
|
73
|
+
*/
|
|
74
|
+
if (newContent !== content) {
|
|
75
|
+
fs.writeFileSync(fullPath, newContent, "utf8");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
39
78
|
/**
|
|
40
79
|
* Update annotation references when story files are moved or renamed
|
|
41
80
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
@@ -54,7 +93,7 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
|
|
|
54
93
|
!fs.statSync(codebasePath).isDirectory()) {
|
|
55
94
|
return 0;
|
|
56
95
|
}
|
|
57
|
-
|
|
96
|
+
const replacementCountRef = { count: 0 };
|
|
58
97
|
const escapedOldPath = oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
59
98
|
const regex = new RegExp(`(@story\\s*)${escapedOldPath}`, "g");
|
|
60
99
|
const files = (0, utils_1.getAllFiles)(codebasePath);
|
|
@@ -69,38 +108,7 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
|
|
|
69
108
|
* @req REQ-MAINT-UPDATE
|
|
70
109
|
*/
|
|
71
110
|
for (const fullPath of files) {
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Skip non-files in iteration
|
|
75
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
76
|
-
* @req REQ-MAINT-UPDATE
|
|
77
|
-
*/
|
|
78
|
-
/**
|
|
79
|
-
* Skip entries that are not regular files (e.g., directories)
|
|
80
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
81
|
-
* @req REQ-MAINT-UPDATE
|
|
82
|
-
*/
|
|
83
|
-
if (!stat.isFile())
|
|
84
|
-
continue;
|
|
85
|
-
const content = fs.readFileSync(fullPath, "utf8");
|
|
86
|
-
const newContent = content.replace(regex,
|
|
87
|
-
/**
|
|
88
|
-
* Replacement callback to update annotation references
|
|
89
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
90
|
-
* @req REQ-MAINT-UPDATE
|
|
91
|
-
*/
|
|
92
|
-
(match, p1) => {
|
|
93
|
-
replacementCount++;
|
|
94
|
-
return `${p1}${newPath}`;
|
|
95
|
-
});
|
|
96
|
-
/**
|
|
97
|
-
* Write file only if content changed
|
|
98
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
99
|
-
* @req REQ-MAINT-UPDATE
|
|
100
|
-
*/
|
|
101
|
-
if (newContent !== content) {
|
|
102
|
-
fs.writeFileSync(fullPath, newContent, "utf8");
|
|
103
|
-
}
|
|
111
|
+
processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef);
|
|
104
112
|
}
|
|
105
|
-
return
|
|
113
|
+
return replacementCountRef.count;
|
|
106
114
|
}
|
|
@@ -56,45 +56,45 @@ function getAllFiles(dir) {
|
|
|
56
56
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
57
57
|
return fileList;
|
|
58
58
|
}
|
|
59
|
+
traverseDirectory(dir, fileList);
|
|
60
|
+
return fileList;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recursively traverse a directory and collect file paths.
|
|
64
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
65
|
+
* @req REQ-MAINT-UTILS-TRAVERSE - Helper traversal function used by getAllFiles
|
|
66
|
+
*/
|
|
67
|
+
function traverseDirectory(currentDir, fileList) {
|
|
68
|
+
const entries = fs.readdirSync(currentDir);
|
|
59
69
|
/**
|
|
60
|
-
*
|
|
70
|
+
* Iterate over directory entries using a for-of loop.
|
|
61
71
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
62
|
-
* @req REQ-MAINT-UTILS-TRAVERSE -
|
|
72
|
+
* @req REQ-MAINT-UTILS-TRAVERSE-FOROF - Traceability for ForOfStatement branch handling entries
|
|
63
73
|
*/
|
|
64
|
-
|
|
65
|
-
const
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const fullPath = path.join(currentDir, entry);
|
|
76
|
+
const stat = fs.statSync(fullPath);
|
|
66
77
|
/**
|
|
67
|
-
*
|
|
78
|
+
* Recurse into directories to continue traversal.
|
|
68
79
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
69
|
-
* @req REQ-MAINT-UTILS-TRAVERSE-
|
|
80
|
+
* @req REQ-MAINT-UTILS-TRAVERSE-DIR - Handle directory entries during traversal
|
|
70
81
|
*/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const stat = fs.statSync(fullPath);
|
|
82
|
+
if (stat.isDirectory()) {
|
|
83
|
+
traverseDirectory(fullPath, fileList);
|
|
74
84
|
/**
|
|
75
|
-
*
|
|
85
|
+
* Collect regular file entries during traversal.
|
|
76
86
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
77
|
-
* @req REQ-MAINT-UTILS-TRAVERSE-
|
|
87
|
+
* @req REQ-MAINT-UTILS-TRAVERSE-FILE - Handle file entries during traversal
|
|
78
88
|
*/
|
|
79
|
-
if (stat.isDirectory()) {
|
|
80
|
-
traverse(fullPath);
|
|
81
|
-
/**
|
|
82
|
-
* Collect regular file entries during traversal.
|
|
83
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
84
|
-
* @req REQ-MAINT-UTILS-TRAVERSE-FILE - Handle file entries during traversal
|
|
85
|
-
*/
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Skip non-file entries encountered during traversal.
|
|
89
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
90
|
-
* @req REQ-MAINT-UTILS-TRAVERSE-SKIP-NONFILE - Traceability for skipping non-file entries
|
|
91
|
-
*/
|
|
92
|
-
if (!stat.isFile()) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
fileList.push(fullPath);
|
|
96
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Skip non-file entries encountered during traversal.
|
|
92
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
93
|
+
* @req REQ-MAINT-UTILS-TRAVERSE-SKIP-NONFILE - Traceability for skipping non-file entries
|
|
94
|
+
*/
|
|
95
|
+
if (!stat.isFile()) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
fileList.push(fullPath);
|
|
97
99
|
}
|
|
98
|
-
traverse(dir);
|
|
99
|
-
return fileList;
|
|
100
100
|
}
|
|
@@ -22,23 +22,41 @@ exports.LOOKBACK_LINES = 4;
|
|
|
22
22
|
*/
|
|
23
23
|
exports.FALLBACK_WINDOW = 800;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Shared predicate to determine if a given comment node contains an @story marker.
|
|
26
26
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
27
|
-
* @req REQ-ANNOTATION-REQUIRED -
|
|
27
|
+
* @req REQ-ANNOTATION-REQUIRED - Centralize @story detection logic for comment value inspection
|
|
28
28
|
*/
|
|
29
|
-
function
|
|
29
|
+
function commentContainsStory(comment) {
|
|
30
|
+
return typeof comment?.value === "string" && comment.value.includes("@story");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Safely extract the physical source lines array from sourceCode for scanning.
|
|
34
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
35
|
+
* @req REQ-ANNOTATION-REQUIRED - Centralize guards for safe access to source lines
|
|
36
|
+
*/
|
|
37
|
+
function getSourceLines(sourceCode) {
|
|
30
38
|
const lines = sourceCode && sourceCode.lines;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
return Array.isArray(lines) ? lines : null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Safely resolve the starting line number of a node for use in lookback scans.
|
|
43
|
+
* Returns null when the node does not provide a valid numeric start line.
|
|
44
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
45
|
+
* @req REQ-ANNOTATION-REQUIRED - Centralize guards for safe access to node location metadata
|
|
46
|
+
*/
|
|
47
|
+
function getNodeStartLine(node) {
|
|
48
|
+
if (!node || !node.loc) {
|
|
49
|
+
return null;
|
|
39
50
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
51
|
+
const line = node.loc.start?.line;
|
|
52
|
+
return typeof line === "number" ? line : null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generic helper to scan a range of physical source lines for the presence of an @story marker.
|
|
56
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
57
|
+
* @req REQ-ANNOTATION-REQUIRED - Reuse line scanning logic for story annotations across helpers
|
|
58
|
+
*/
|
|
59
|
+
function scanLinesForMarker(lines, from, to) {
|
|
42
60
|
// Walk each physical line in the configured lookback window to search for an inline @story marker.
|
|
43
61
|
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
44
62
|
// @req REQ-ANNOTATION-REQUIRED - Scan preceding lines for existing story annotations
|
|
@@ -53,6 +71,24 @@ function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES
|
|
|
53
71
|
}
|
|
54
72
|
return false;
|
|
55
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Inspect a fixed number of physical source lines before the node for @story text
|
|
76
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
77
|
+
* @req REQ-ANNOTATION-REQUIRED - Extract line-based detection into helper
|
|
78
|
+
*/
|
|
79
|
+
function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES) {
|
|
80
|
+
const lines = getSourceLines(sourceCode);
|
|
81
|
+
const startLine = getNodeStartLine(node);
|
|
82
|
+
// Guard against missing or non-array source lines or an invalid start line before scanning.
|
|
83
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
84
|
+
// @req REQ-ANNOTATION-REQUIRED - Fail gracefully when source lines or locations are unavailable
|
|
85
|
+
if (!lines || typeof startLine !== "number") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const from = Math.max(0, startLine - 1 - lookback);
|
|
89
|
+
const to = Math.max(0, startLine - 1);
|
|
90
|
+
return scanLinesForMarker(lines, from, to);
|
|
91
|
+
}
|
|
56
92
|
/**
|
|
57
93
|
* Walk parent chain and check comments before each parent and their leadingComments
|
|
58
94
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -70,7 +106,7 @@ function parentChainHasStory(sourceCode, node) {
|
|
|
70
106
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
71
107
|
* @req REQ-ANNOTATION-REQUIRED - Detect @story in parent comments via value inspection
|
|
72
108
|
*/
|
|
73
|
-
(c) =>
|
|
109
|
+
(c) => commentContainsStory(c))) {
|
|
74
110
|
return true;
|
|
75
111
|
}
|
|
76
112
|
const pLeading = p.leadingComments || [];
|
|
@@ -80,7 +116,7 @@ function parentChainHasStory(sourceCode, node) {
|
|
|
80
116
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
81
117
|
* @req REQ-ANNOTATION-REQUIRED - Detect @story in parent leadingComments via value inspection
|
|
82
118
|
*/
|
|
83
|
-
(c) =>
|
|
119
|
+
(c) => commentContainsStory(c))) {
|
|
84
120
|
return true;
|
|
85
121
|
}
|
|
86
122
|
p = p.parent;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers and types for the valid-annotation-format rule.
|
|
3
|
+
*
|
|
4
|
+
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
5
|
+
* @story docs/stories/008.0-DEV-AUTO-FIX.story.md
|
|
6
|
+
* @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
|
|
7
|
+
* @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
|
|
8
|
+
* @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
|
|
9
|
+
* @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
|
|
10
|
+
* @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
|
|
11
|
+
* @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Pending annotation state tracked while iterating through comment lines.
|
|
15
|
+
*/
|
|
16
|
+
export interface PendingAnnotation {
|
|
17
|
+
type: "story" | "req";
|
|
18
|
+
value: string;
|
|
19
|
+
hasValue: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a raw comment line to make annotation parsing more robust.
|
|
23
|
+
*
|
|
24
|
+
* This function trims whitespace, keeps any annotation tags that appear
|
|
25
|
+
* later in the line, and supports common JSDoc styles such as leading "*".
|
|
26
|
+
*
|
|
27
|
+
* It detects @story, @req, and @implements tags while preserving the rest
|
|
28
|
+
* of the line for downstream logic.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeCommentLine(rawLine: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Internal helpers and types for the valid-annotation-format rule.
|
|
4
|
+
*
|
|
5
|
+
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
6
|
+
* @story docs/stories/008.0-DEV-AUTO-FIX.story.md
|
|
7
|
+
* @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
|
|
8
|
+
* @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
|
|
9
|
+
* @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
|
|
10
|
+
* @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
|
|
11
|
+
* @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
|
|
12
|
+
* @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.normalizeCommentLine = normalizeCommentLine;
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a raw comment line to make annotation parsing more robust.
|
|
18
|
+
*
|
|
19
|
+
* This function trims whitespace, keeps any annotation tags that appear
|
|
20
|
+
* later in the line, and supports common JSDoc styles such as leading "*".
|
|
21
|
+
*
|
|
22
|
+
* It detects @story, @req, and @implements tags while preserving the rest
|
|
23
|
+
* of the line for downstream logic.
|
|
24
|
+
*/
|
|
25
|
+
function normalizeCommentLine(rawLine) {
|
|
26
|
+
const trimmed = rawLine.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
const annotationMatch = trimmed.match(/@story\b|@req\b|@implements\b/);
|
|
31
|
+
if (!annotationMatch || annotationMatch.index === undefined) {
|
|
32
|
+
const withoutLeadingStar = trimmed.replace(/^\*\s?/, "");
|
|
33
|
+
return withoutLeadingStar;
|
|
34
|
+
}
|
|
35
|
+
return trimmed.slice(annotationMatch.index);
|
|
36
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared option handling for the valid-annotation-format rule.
|
|
3
|
+
*
|
|
4
|
+
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
5
|
+
* @req REQ-PATTERN-CONFIG - Support configuration of custom story path and requirement ID patterns
|
|
6
|
+
* @req REQ-REGEX-VALIDATION - Validate that configured patterns are valid regular expressions
|
|
7
|
+
* @req REQ-BACKWARD-COMPAT - Maintain current behavior when no custom patterns configured
|
|
8
|
+
* @req REQ-EXAMPLE-MESSAGES - Support optional example strings in error messages
|
|
9
|
+
* @req REQ-SCHEMA-VALIDATION - Use JSON Schema to validate configuration options
|
|
10
|
+
*/
|
|
11
|
+
export interface AnnotationRuleOptions {
|
|
12
|
+
story?: {
|
|
13
|
+
/**
|
|
14
|
+
* Regex (string) the collapsed story path must match.
|
|
15
|
+
* Default: /^docs\/stories\/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$/
|
|
16
|
+
*/
|
|
17
|
+
pattern?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Human-readable example path used in error messages.
|
|
20
|
+
* Default: "docs/stories/005.0-DEV-EXAMPLE.story.md"
|
|
21
|
+
*/
|
|
22
|
+
example?: string;
|
|
23
|
+
};
|
|
24
|
+
req?: {
|
|
25
|
+
/**
|
|
26
|
+
* Regex (string) the collapsed requirement ID must match.
|
|
27
|
+
* Default: /^REQ-[A-Z0-9-]+$/
|
|
28
|
+
*/
|
|
29
|
+
pattern?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Human-readable example requirement ID used in error messages.
|
|
32
|
+
* Default: "REQ-EXAMPLE"
|
|
33
|
+
*/
|
|
34
|
+
example?: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Shorthand for story.pattern.
|
|
38
|
+
* Regex (string) the collapsed story path must match.
|
|
39
|
+
*/
|
|
40
|
+
storyPathPattern?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Shorthand for story.example.
|
|
43
|
+
* Human-readable example story path used in error messages.
|
|
44
|
+
*/
|
|
45
|
+
storyPathExample?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Shorthand for req.pattern.
|
|
48
|
+
* Regex (string) the collapsed requirement ID must match.
|
|
49
|
+
*/
|
|
50
|
+
requirementIdPattern?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Shorthand for req.example.
|
|
53
|
+
* Human-readable example requirement ID used in error messages.
|
|
54
|
+
*/
|
|
55
|
+
requirementIdExample?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolved, runtime-ready options for the rule.
|
|
59
|
+
*/
|
|
60
|
+
export interface ResolvedAnnotationOptions {
|
|
61
|
+
storyPattern: RegExp;
|
|
62
|
+
storyExample: string;
|
|
63
|
+
reqPattern: RegExp;
|
|
64
|
+
reqExample: string;
|
|
65
|
+
}
|
|
66
|
+
export declare function getDefaultReqExample(): string;
|
|
67
|
+
export declare function getResolvedDefaults(): ResolvedAnnotationOptions;
|
|
68
|
+
export declare function getOptionErrors(): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Resolve user options into concrete, validated configuration.
|
|
71
|
+
* Falls back to existing defaults when options are not provided or invalid.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveOptions(rawOptions: unknown[]): ResolvedAnnotationOptions;
|
|
74
|
+
/**
|
|
75
|
+
* Build the JSON schema for rule options.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getRuleSchema(): {
|
|
78
|
+
type: string;
|
|
79
|
+
properties: {
|
|
80
|
+
story: {
|
|
81
|
+
type: string;
|
|
82
|
+
properties: {
|
|
83
|
+
pattern: {
|
|
84
|
+
type: string;
|
|
85
|
+
};
|
|
86
|
+
example: {
|
|
87
|
+
type: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
additionalProperties: boolean;
|
|
91
|
+
};
|
|
92
|
+
req: {
|
|
93
|
+
type: string;
|
|
94
|
+
properties: {
|
|
95
|
+
pattern: {
|
|
96
|
+
type: string;
|
|
97
|
+
};
|
|
98
|
+
example: {
|
|
99
|
+
type: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
additionalProperties: boolean;
|
|
103
|
+
};
|
|
104
|
+
storyPathPattern: {
|
|
105
|
+
type: string;
|
|
106
|
+
};
|
|
107
|
+
storyPathExample: {
|
|
108
|
+
type: string;
|
|
109
|
+
};
|
|
110
|
+
requirementIdPattern: {
|
|
111
|
+
type: string;
|
|
112
|
+
};
|
|
113
|
+
requirementIdExample: {
|
|
114
|
+
type: string;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
additionalProperties: boolean;
|
|
118
|
+
}[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDefaultReqExample = getDefaultReqExample;
|
|
4
|
+
exports.getResolvedDefaults = getResolvedDefaults;
|
|
5
|
+
exports.getOptionErrors = getOptionErrors;
|
|
6
|
+
exports.resolveOptions = resolveOptions;
|
|
7
|
+
exports.getRuleSchema = getRuleSchema;
|
|
8
|
+
function getDefaultStoryPattern() {
|
|
9
|
+
return /^docs\/stories\/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$/;
|
|
10
|
+
}
|
|
11
|
+
function getDefaultStoryExample() {
|
|
12
|
+
return "docs/stories/005.0-DEV-EXAMPLE.story.md";
|
|
13
|
+
}
|
|
14
|
+
function getDefaultReqPattern() {
|
|
15
|
+
return /^REQ-[A-Z0-9-]+$/;
|
|
16
|
+
}
|
|
17
|
+
function getDefaultReqExample() {
|
|
18
|
+
return "REQ-EXAMPLE";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Global cache of the last resolved options for helpers that need access
|
|
22
|
+
* without having options explicitly passed in.
|
|
23
|
+
*/
|
|
24
|
+
let resolvedDefaults = {
|
|
25
|
+
storyPattern: getDefaultStoryPattern(),
|
|
26
|
+
storyExample: getDefaultStoryExample(),
|
|
27
|
+
reqPattern: getDefaultReqPattern(),
|
|
28
|
+
reqExample: getDefaultReqExample(),
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Collected configuration errors encountered while resolving options.
|
|
32
|
+
*/
|
|
33
|
+
let optionErrors = [];
|
|
34
|
+
function getResolvedDefaults() {
|
|
35
|
+
return resolvedDefaults;
|
|
36
|
+
}
|
|
37
|
+
function getOptionErrors() {
|
|
38
|
+
return optionErrors;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a stable, engine-independent configuration error message
|
|
42
|
+
* for invalid regex options.
|
|
43
|
+
*/
|
|
44
|
+
function buildInvalidRegexError(field, pattern) {
|
|
45
|
+
return `Invalid regular expression for option "${field}": "${pattern}"`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalize raw rule options into a single AnnotationRuleOptions object.
|
|
49
|
+
*
|
|
50
|
+
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
51
|
+
* @req REQ-PATTERN-CONFIG
|
|
52
|
+
* @req REQ-BACKWARD-COMPAT
|
|
53
|
+
*/
|
|
54
|
+
function normalizeUserOptions(rawOptions) {
|
|
55
|
+
if (!rawOptions || rawOptions.length === 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const first = rawOptions[0];
|
|
59
|
+
if (!first || typeof first !== "object") {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return first;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a user-configured regex pattern, handling both nested and flat
|
|
66
|
+
* configuration shapes and accumulating validation errors.
|
|
67
|
+
*
|
|
68
|
+
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
69
|
+
* @req REQ-PATTERN-CONFIG
|
|
70
|
+
* @req REQ-REGEX-VALIDATION
|
|
71
|
+
* @req REQ-BACKWARD-COMPAT
|
|
72
|
+
*/
|
|
73
|
+
// eslint-disable-next-line max-params -- Small, centralized helper; keeping parameters explicit is clearer than introducing an options object here.
|
|
74
|
+
function resolvePattern(nestedPattern, nestedFieldName, flatPattern, flatFieldName, defaultPattern) {
|
|
75
|
+
const effective = typeof nestedPattern === "string"
|
|
76
|
+
? { value: nestedPattern, field: nestedFieldName }
|
|
77
|
+
: typeof flatPattern === "string"
|
|
78
|
+
? { value: flatPattern, field: flatFieldName }
|
|
79
|
+
: null;
|
|
80
|
+
if (!effective) {
|
|
81
|
+
return defaultPattern;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return new RegExp(effective.value);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
optionErrors.push(buildInvalidRegexError(effective.field, effective.value));
|
|
88
|
+
return defaultPattern;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve an example string, preferring nested over flat configuration,
|
|
93
|
+
* and falling back to the provided default when necessary.
|
|
94
|
+
*
|
|
95
|
+
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
96
|
+
* @req REQ-EXAMPLE-MESSAGES
|
|
97
|
+
* @req REQ-BACKWARD-COMPAT
|
|
98
|
+
*/
|
|
99
|
+
function resolveExample(nestedExample, flatExample, defaultExample) {
|
|
100
|
+
if (typeof nestedExample === "string" && nestedExample.trim()) {
|
|
101
|
+
return nestedExample;
|
|
102
|
+
}
|
|
103
|
+
if (typeof flatExample === "string" && flatExample.trim()) {
|
|
104
|
+
return flatExample;
|
|
105
|
+
}
|
|
106
|
+
return defaultExample;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve user options into concrete, validated configuration.
|
|
110
|
+
* Falls back to existing defaults when options are not provided or invalid.
|
|
111
|
+
*/
|
|
112
|
+
function resolveOptions(rawOptions) {
|
|
113
|
+
optionErrors = [];
|
|
114
|
+
const user = normalizeUserOptions(rawOptions);
|
|
115
|
+
const nestedStoryPattern = user?.story?.pattern;
|
|
116
|
+
const flatStoryPattern = user?.storyPathPattern;
|
|
117
|
+
const nestedStoryExample = user?.story?.example;
|
|
118
|
+
const flatStoryExample = user?.storyPathExample;
|
|
119
|
+
const nestedReqPattern = user?.req?.pattern;
|
|
120
|
+
const flatReqPattern = user?.requirementIdPattern;
|
|
121
|
+
const nestedReqExample = user?.req?.example;
|
|
122
|
+
const flatReqExample = user?.requirementIdExample;
|
|
123
|
+
const storyPattern = resolvePattern(nestedStoryPattern, "story.pattern", flatStoryPattern, "storyPathPattern", getDefaultStoryPattern());
|
|
124
|
+
const reqPattern = resolvePattern(nestedReqPattern, "req.pattern", flatReqPattern, "requirementIdPattern", getDefaultReqPattern());
|
|
125
|
+
const storyExample = resolveExample(nestedStoryExample, flatStoryExample, getDefaultStoryExample());
|
|
126
|
+
const reqExample = resolveExample(nestedReqExample, flatReqExample, getDefaultReqExample());
|
|
127
|
+
resolvedDefaults = {
|
|
128
|
+
storyPattern,
|
|
129
|
+
storyExample,
|
|
130
|
+
reqPattern,
|
|
131
|
+
reqExample,
|
|
132
|
+
};
|
|
133
|
+
return resolvedDefaults;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build the JSON schema for rule options.
|
|
137
|
+
*/
|
|
138
|
+
function getRuleSchema() {
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
story: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
pattern: { type: "string" },
|
|
147
|
+
example: { type: "string" },
|
|
148
|
+
},
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
},
|
|
151
|
+
req: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
pattern: { type: "string" },
|
|
155
|
+
example: { type: "string" },
|
|
156
|
+
},
|
|
157
|
+
additionalProperties: false,
|
|
158
|
+
},
|
|
159
|
+
storyPathPattern: { type: "string" },
|
|
160
|
+
storyPathExample: { type: "string" },
|
|
161
|
+
requirementIdPattern: { type: "string" },
|
|
162
|
+
requirementIdExample: { type: "string" },
|
|
163
|
+
},
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|