eslint-plugin-traceability 1.3.0 → 1.4.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 +2 -1
- package/lib/src/index.d.ts +12 -17
- package/lib/src/index.js +69 -24
- package/lib/src/maintenance/utils.js +5 -0
- package/lib/src/rules/require-branch-annotation.js +27 -147
- package/lib/src/rules/require-req-annotation.d.ts +5 -0
- package/lib/src/rules/require-req-annotation.js +20 -0
- package/lib/src/rules/require-story-annotation.d.ts +3 -12
- package/lib/src/rules/require-story-annotation.js +192 -162
- package/lib/src/rules/valid-annotation-format.js +11 -0
- package/lib/src/rules/valid-req-reference.js +65 -25
- package/lib/src/rules/valid-story-reference.js +55 -58
- package/lib/src/utils/annotation-checker.js +80 -13
- package/lib/src/utils/branch-annotation-helpers.d.ts +54 -0
- package/lib/src/utils/branch-annotation-helpers.js +148 -0
- package/lib/src/utils/storyReferenceUtils.d.ts +47 -0
- package/lib/src/utils/storyReferenceUtils.js +111 -0
- package/lib/tests/cli-error-handling.test.d.ts +1 -0
- package/lib/tests/cli-error-handling.test.js +44 -0
- package/lib/tests/integration/cli-integration.test.js +3 -5
- package/lib/tests/maintenance/index.test.d.ts +1 -0
- package/lib/tests/maintenance/index.test.js +25 -0
- package/lib/tests/plugin-setup-error.test.d.ts +5 -0
- package/lib/tests/plugin-setup-error.test.js +37 -0
- package/lib/tests/rules/error-reporting.test.d.ts +1 -0
- package/lib/tests/rules/error-reporting.test.js +47 -0
- package/lib/tests/rules/require-story-annotation.test.js +101 -25
- package/lib/tests/utils/branch-annotation-helpers.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-helpers.test.js +46 -0
- package/package.json +4 -3
|
@@ -11,59 +11,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
12
12
|
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
13
13
|
*/
|
|
14
|
-
const fs_1 = __importDefault(require("fs"));
|
|
15
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
|
|
16
16
|
const defaultStoryDirs = ["docs/stories", "stories"];
|
|
17
|
-
const fileExistCache = new Map();
|
|
18
17
|
/**
|
|
19
|
-
*
|
|
18
|
+
* Extract the story path from the annotation line and delegate validation.
|
|
20
19
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
21
|
-
* @req REQ-
|
|
22
|
-
*/
|
|
23
|
-
function buildCandidates(storyPath, cwd, storyDirs) {
|
|
24
|
-
const candidates = [];
|
|
25
|
-
if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
|
|
26
|
-
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
30
|
-
for (const dir of storyDirs) {
|
|
31
|
-
candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return candidates;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Check if any of the candidate files exist.
|
|
38
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
39
|
-
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
20
|
+
* @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
|
|
40
21
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
22
|
+
function validateStoryPath(opts) {
|
|
23
|
+
const { line, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
|
|
24
|
+
const parts = line.split(/\s+/);
|
|
25
|
+
const storyPath = parts[1];
|
|
26
|
+
if (!storyPath)
|
|
27
|
+
return;
|
|
28
|
+
processStoryPath({
|
|
29
|
+
storyPath,
|
|
30
|
+
commentNode,
|
|
31
|
+
context,
|
|
32
|
+
cwd,
|
|
33
|
+
storyDirs,
|
|
34
|
+
allowAbsolute,
|
|
35
|
+
requireExt,
|
|
36
|
+
});
|
|
53
37
|
}
|
|
54
38
|
/**
|
|
55
|
-
*
|
|
39
|
+
* Process and validate the story path for security, extension, and existence.
|
|
56
40
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
57
41
|
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
58
42
|
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
59
43
|
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
60
44
|
*/
|
|
61
|
-
function
|
|
62
|
-
const
|
|
63
|
-
const storyPath = parts[1];
|
|
64
|
-
if (!storyPath) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
45
|
+
function processStoryPath(opts) {
|
|
46
|
+
const { storyPath, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
|
|
67
47
|
// Absolute path check
|
|
68
48
|
if (path_1.default.isAbsolute(storyPath)) {
|
|
69
49
|
if (!allowAbsolute) {
|
|
@@ -75,10 +55,9 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
|
|
|
75
55
|
}
|
|
76
56
|
return;
|
|
77
57
|
}
|
|
78
|
-
// Path traversal
|
|
79
|
-
if (
|
|
80
|
-
const
|
|
81
|
-
const full = path_1.default.resolve(cwd, normalized);
|
|
58
|
+
// Path traversal check
|
|
59
|
+
if ((0, storyReferenceUtils_1.containsPathTraversal)(storyPath)) {
|
|
60
|
+
const full = path_1.default.resolve(cwd, path_1.default.normalize(storyPath));
|
|
82
61
|
if (!full.startsWith(cwd + path_1.default.sep)) {
|
|
83
62
|
context.report({
|
|
84
63
|
node: commentNode,
|
|
@@ -89,7 +68,7 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
|
|
|
89
68
|
}
|
|
90
69
|
}
|
|
91
70
|
// Extension check
|
|
92
|
-
if (requireExt && !
|
|
71
|
+
if (requireExt && !(0, storyReferenceUtils_1.hasValidExtension)(storyPath)) {
|
|
93
72
|
context.report({
|
|
94
73
|
node: commentNode,
|
|
95
74
|
messageId: "invalidExtension",
|
|
@@ -97,9 +76,8 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
|
|
|
97
76
|
});
|
|
98
77
|
return;
|
|
99
78
|
}
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
if (!existsAny(candidates)) {
|
|
79
|
+
// Existence check
|
|
80
|
+
if (!(0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs).exists) {
|
|
103
81
|
context.report({
|
|
104
82
|
node: commentNode,
|
|
105
83
|
messageId: "fileMissing",
|
|
@@ -112,13 +90,22 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
|
|
|
112
90
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
113
91
|
* @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
|
|
114
92
|
*/
|
|
115
|
-
function handleComment(
|
|
93
|
+
function handleComment(opts) {
|
|
94
|
+
const { commentNode, context, cwd, storyDirs, allowAbsolute, requireExt } = opts;
|
|
116
95
|
const lines = commentNode.value
|
|
117
96
|
.split(/\r?\n/)
|
|
118
97
|
.map((l) => l.replace(/^[^@]*/, "").trim());
|
|
119
98
|
for (const line of lines) {
|
|
120
99
|
if (line.startsWith("@story")) {
|
|
121
|
-
validateStoryPath(
|
|
100
|
+
validateStoryPath({
|
|
101
|
+
line,
|
|
102
|
+
commentNode,
|
|
103
|
+
context,
|
|
104
|
+
cwd,
|
|
105
|
+
storyDirs,
|
|
106
|
+
allowAbsolute,
|
|
107
|
+
requireExt,
|
|
108
|
+
});
|
|
122
109
|
}
|
|
123
110
|
}
|
|
124
111
|
}
|
|
@@ -138,10 +125,7 @@ exports.default = {
|
|
|
138
125
|
{
|
|
139
126
|
type: "object",
|
|
140
127
|
properties: {
|
|
141
|
-
storyDirectories: {
|
|
142
|
-
type: "array",
|
|
143
|
-
items: { type: "string" },
|
|
144
|
-
},
|
|
128
|
+
storyDirectories: { type: "array", items: { type: "string" } },
|
|
145
129
|
allowAbsolutePaths: { type: "boolean" },
|
|
146
130
|
requireStoryExtension: { type: "boolean" },
|
|
147
131
|
},
|
|
@@ -150,17 +134,30 @@ exports.default = {
|
|
|
150
134
|
],
|
|
151
135
|
},
|
|
152
136
|
create(context) {
|
|
153
|
-
const sourceCode = context.getSourceCode();
|
|
154
137
|
const cwd = process.cwd();
|
|
155
138
|
const opts = context.options[0];
|
|
156
139
|
const storyDirs = opts?.storyDirectories || defaultStoryDirs;
|
|
157
140
|
const allowAbsolute = opts?.allowAbsolutePaths || false;
|
|
158
141
|
const requireExt = opts?.requireStoryExtension !== false;
|
|
159
142
|
return {
|
|
143
|
+
/**
|
|
144
|
+
* Program-level handler: iterate comments and validate @story annotations.
|
|
145
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
146
|
+
* @req REQ-ANNOTATION-VALIDATION - Discover and dispatch @story annotations for validation
|
|
147
|
+
* @req REQ-FILE-EXISTENCE - Ensure referenced files exist
|
|
148
|
+
* @req REQ-PATH-RESOLUTION - Resolve using cwd and configured story directories
|
|
149
|
+
*/
|
|
160
150
|
Program() {
|
|
161
|
-
const comments =
|
|
151
|
+
const comments = context.getSourceCode().getAllComments() || [];
|
|
162
152
|
for (const comment of comments) {
|
|
163
|
-
handleComment(
|
|
153
|
+
handleComment({
|
|
154
|
+
commentNode: comment,
|
|
155
|
+
context,
|
|
156
|
+
cwd,
|
|
157
|
+
storyDirs,
|
|
158
|
+
allowAbsolute,
|
|
159
|
+
requireExt,
|
|
160
|
+
});
|
|
164
161
|
}
|
|
165
162
|
},
|
|
166
163
|
};
|
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkReqAnnotation = checkReqAnnotation;
|
|
4
|
+
/**
|
|
5
|
+
* Helper to retrieve the JSDoc comment for a node.
|
|
6
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
7
|
+
* @req REQ-ANNOTATION-GET-JSDOC - Retrieve JSDoc comment for a node
|
|
8
|
+
*/
|
|
9
|
+
function getJsdocComment(sourceCode, node) {
|
|
10
|
+
return sourceCode.getJSDocComment(node);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Helper to retrieve leading comments from a node (TypeScript declare style).
|
|
14
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
15
|
+
* @req REQ-ANNOTATION-LEADING-COMMENTS - Collect leading comments from node
|
|
16
|
+
*/
|
|
17
|
+
function getLeadingComments(node) {
|
|
18
|
+
return node.leadingComments || [];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Helper to retrieve comments before a node using the sourceCode API.
|
|
22
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
23
|
+
* @req REQ-ANNOTATION-COMMENTS-BEFORE - Collect comments before node via sourceCode
|
|
24
|
+
*/
|
|
25
|
+
function getCommentsBefore(sourceCode, node) {
|
|
26
|
+
return sourceCode.getCommentsBefore(node) || [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Helper to combine leading and before comments into a single array.
|
|
30
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
31
|
+
* @req REQ-ANNOTATION-COMBINE-COMMENTS - Combine comment arrays for checking
|
|
32
|
+
*/
|
|
33
|
+
function combineComments(leading, before) {
|
|
34
|
+
return [...leading, ...before];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Predicate helper to check whether a comment contains a @req annotation.
|
|
38
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
39
|
+
* @req REQ-ANNOTATION-CHECK-COMMENT - Detect @req tag inside a comment
|
|
40
|
+
*/
|
|
41
|
+
function commentContainsReq(c) {
|
|
42
|
+
return c && typeof c.value === "string" && c.value.includes("@req");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
|
|
46
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
47
|
+
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
48
|
+
*/
|
|
49
|
+
function hasReqAnnotation(jsdoc, comments) {
|
|
50
|
+
return ((jsdoc &&
|
|
51
|
+
typeof jsdoc.value === "string" &&
|
|
52
|
+
jsdoc.value.includes("@req")) ||
|
|
53
|
+
comments.some(commentContainsReq));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a fix function that inserts a missing @req JSDoc before the node.
|
|
57
|
+
* Returned function is a proper named function so no inline arrow is used.
|
|
58
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
59
|
+
* @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
|
|
60
|
+
*/
|
|
61
|
+
function createMissingReqFix(node) {
|
|
62
|
+
return function missingReqFix(fixer) {
|
|
63
|
+
return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Helper to report a missing @req annotation via the ESLint context API.
|
|
68
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
69
|
+
* @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
|
|
70
|
+
*/
|
|
71
|
+
function reportMissing(context, node) {
|
|
72
|
+
context.report({
|
|
73
|
+
node,
|
|
74
|
+
messageId: "missingReq",
|
|
75
|
+
fix: createMissingReqFix(node),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
4
78
|
/**
|
|
5
79
|
* Helper to check @req annotation presence on TS declare functions and method signatures.
|
|
6
80
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -8,19 +82,12 @@ exports.checkReqAnnotation = checkReqAnnotation;
|
|
|
8
82
|
*/
|
|
9
83
|
function checkReqAnnotation(context, node) {
|
|
10
84
|
const sourceCode = context.getSourceCode();
|
|
11
|
-
const jsdoc = sourceCode
|
|
12
|
-
const leading = node
|
|
13
|
-
const comments =
|
|
14
|
-
const all =
|
|
15
|
-
const hasReq = (jsdoc
|
|
16
|
-
all.some((c) => c.value.includes("@req"));
|
|
85
|
+
const jsdoc = getJsdocComment(sourceCode, node);
|
|
86
|
+
const leading = getLeadingComments(node);
|
|
87
|
+
const comments = getCommentsBefore(sourceCode, node);
|
|
88
|
+
const all = combineComments(leading, comments);
|
|
89
|
+
const hasReq = hasReqAnnotation(jsdoc, all);
|
|
17
90
|
if (!hasReq) {
|
|
18
|
-
context
|
|
19
|
-
node,
|
|
20
|
-
messageId: "missingReq",
|
|
21
|
-
fix(fixer) {
|
|
22
|
-
return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
|
|
23
|
-
},
|
|
24
|
-
});
|
|
91
|
+
reportMissing(context, node);
|
|
25
92
|
}
|
|
26
93
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Valid branch types for require-branch-annotation rule.
|
|
4
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
5
|
+
* @req REQ-SIGNIFICANCE-CRITERIA - Define criteria for which branches require annotations
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_BRANCH_TYPES: readonly ["IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"];
|
|
8
|
+
/**
|
|
9
|
+
* Type for branch nodes supported by require-branch-annotation rule.
|
|
10
|
+
*/
|
|
11
|
+
export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
|
|
12
|
+
/**
|
|
13
|
+
* Validate branchTypes configuration option and return branch types to enforce,
|
|
14
|
+
* or return an ESLint listener if configuration is invalid.
|
|
15
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
16
|
+
* @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateBranchTypes(context: Rule.RuleContext): BranchType[] | Rule.RuleListener;
|
|
19
|
+
/**
|
|
20
|
+
* Gather leading comment text for a branch node.
|
|
21
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
22
|
+
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
23
|
+
*/
|
|
24
|
+
export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
|
|
25
|
+
/**
|
|
26
|
+
* Report missing @story annotation on a branch node.
|
|
27
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
28
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
29
|
+
*/
|
|
30
|
+
export declare function reportMissingStory(context: Rule.RuleContext, node: any, options: {
|
|
31
|
+
indent: string;
|
|
32
|
+
insertPos: number;
|
|
33
|
+
storyFixCountRef: {
|
|
34
|
+
count: number;
|
|
35
|
+
};
|
|
36
|
+
}): void;
|
|
37
|
+
/**
|
|
38
|
+
* Report missing @req annotation on a branch node.
|
|
39
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
40
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
41
|
+
*/
|
|
42
|
+
export declare function reportMissingReq(context: Rule.RuleContext, node: any, options: {
|
|
43
|
+
indent: string;
|
|
44
|
+
insertPos: number;
|
|
45
|
+
missingStory: boolean;
|
|
46
|
+
}): void;
|
|
47
|
+
/**
|
|
48
|
+
* Report missing annotations on a branch node.
|
|
49
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
50
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
51
|
+
*/
|
|
52
|
+
export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
|
|
53
|
+
count: number;
|
|
54
|
+
}): void;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_BRANCH_TYPES = void 0;
|
|
4
|
+
exports.validateBranchTypes = validateBranchTypes;
|
|
5
|
+
exports.gatherBranchCommentText = gatherBranchCommentText;
|
|
6
|
+
exports.reportMissingStory = reportMissingStory;
|
|
7
|
+
exports.reportMissingReq = reportMissingReq;
|
|
8
|
+
exports.reportMissingAnnotations = reportMissingAnnotations;
|
|
9
|
+
const PRE_COMMENT_OFFSET = 2; // number of lines above branch to inspect for comments
|
|
10
|
+
/**
|
|
11
|
+
* Valid branch types for require-branch-annotation rule.
|
|
12
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
13
|
+
* @req REQ-SIGNIFICANCE-CRITERIA - Define criteria for which branches require annotations
|
|
14
|
+
*/
|
|
15
|
+
exports.DEFAULT_BRANCH_TYPES = [
|
|
16
|
+
"IfStatement",
|
|
17
|
+
"SwitchCase",
|
|
18
|
+
"TryStatement",
|
|
19
|
+
"CatchClause",
|
|
20
|
+
"ForStatement",
|
|
21
|
+
"ForOfStatement",
|
|
22
|
+
"ForInStatement",
|
|
23
|
+
"WhileStatement",
|
|
24
|
+
"DoWhileStatement",
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Validate branchTypes configuration option and return branch types to enforce,
|
|
28
|
+
* or return an ESLint listener if configuration is invalid.
|
|
29
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
30
|
+
* @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
|
|
31
|
+
*/
|
|
32
|
+
function validateBranchTypes(context) {
|
|
33
|
+
const options = context.options[0] || {};
|
|
34
|
+
if (Array.isArray(options.branchTypes)) {
|
|
35
|
+
const invalidTypes = options.branchTypes.filter((t) => !exports.DEFAULT_BRANCH_TYPES.includes(t));
|
|
36
|
+
if (invalidTypes.length > 0) {
|
|
37
|
+
return {
|
|
38
|
+
Program(node) {
|
|
39
|
+
invalidTypes.forEach((t) => {
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
message: `Value "${t}" should be equal to one of the allowed values: ${exports.DEFAULT_BRANCH_TYPES.join(", ")}`,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.isArray(options.branchTypes)
|
|
50
|
+
? options.branchTypes
|
|
51
|
+
: Array.from(exports.DEFAULT_BRANCH_TYPES);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Gather leading comment text for a branch node.
|
|
55
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
56
|
+
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
57
|
+
*/
|
|
58
|
+
function gatherBranchCommentText(sourceCode, node) {
|
|
59
|
+
if (node.type === "SwitchCase") {
|
|
60
|
+
const lines = sourceCode.lines;
|
|
61
|
+
const startLine = node.loc.start.line;
|
|
62
|
+
let i = startLine - PRE_COMMENT_OFFSET;
|
|
63
|
+
const comments = [];
|
|
64
|
+
while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
|
|
65
|
+
comments.unshift(lines[i].trim());
|
|
66
|
+
i--;
|
|
67
|
+
}
|
|
68
|
+
return comments.join(" ");
|
|
69
|
+
}
|
|
70
|
+
const comments = sourceCode.getCommentsBefore(node) || [];
|
|
71
|
+
return comments.map((c) => c.value).join(" ");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Report missing @story annotation on a branch node.
|
|
75
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
76
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
77
|
+
*/
|
|
78
|
+
function reportMissingStory(context, node, options) {
|
|
79
|
+
const { indent, insertPos, storyFixCountRef } = options;
|
|
80
|
+
if (storyFixCountRef.count === 0) {
|
|
81
|
+
context.report({
|
|
82
|
+
node,
|
|
83
|
+
messageId: "missingAnnotation",
|
|
84
|
+
data: { missing: "@story" },
|
|
85
|
+
fix: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`),
|
|
86
|
+
});
|
|
87
|
+
storyFixCountRef.count++;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
context.report({
|
|
91
|
+
node,
|
|
92
|
+
messageId: "missingAnnotation",
|
|
93
|
+
data: { missing: "@story" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Report missing @req annotation on a branch node.
|
|
99
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
100
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
101
|
+
*/
|
|
102
|
+
function reportMissingReq(context, node, options) {
|
|
103
|
+
const { indent, insertPos, missingStory } = options;
|
|
104
|
+
if (!missingStory) {
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: "missingAnnotation",
|
|
108
|
+
data: { missing: "@req" },
|
|
109
|
+
fix: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
context.report({
|
|
114
|
+
node,
|
|
115
|
+
messageId: "missingAnnotation",
|
|
116
|
+
data: { missing: "@req" },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Report missing annotations on a branch node.
|
|
122
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
123
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
124
|
+
*/
|
|
125
|
+
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
126
|
+
const sourceCode = context.getSourceCode();
|
|
127
|
+
const text = gatherBranchCommentText(sourceCode, node);
|
|
128
|
+
const missingStory = !/@story\b/.test(text);
|
|
129
|
+
const missingReq = !/@req\b/.test(text);
|
|
130
|
+
const indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
|
|
131
|
+
const insertPos = sourceCode.getIndexFromLoc({
|
|
132
|
+
line: node.loc.start.line,
|
|
133
|
+
column: 0,
|
|
134
|
+
});
|
|
135
|
+
const actions = [
|
|
136
|
+
{
|
|
137
|
+
missing: missingStory,
|
|
138
|
+
fn: reportMissingStory,
|
|
139
|
+
args: [context, node, { indent, insertPos, storyFixCountRef }],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
missing: missingReq,
|
|
143
|
+
fn: reportMissingReq,
|
|
144
|
+
args: [context, node, { indent, insertPos, missingStory }],
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
actions.forEach(({ missing, fn, args }) => missing && fn(...args));
|
|
148
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build candidate file paths for a given story path.
|
|
3
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
4
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildStoryCandidates(storyPath: string, cwd: string, storyDirs: string[]): string[];
|
|
7
|
+
export declare function storyExists(paths: string[]): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a story path to candidate absolute paths and check existence.
|
|
10
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
11
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
12
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeStoryPath(storyPath: string, cwd: string, storyDirs: string[]): {
|
|
15
|
+
candidates: string[];
|
|
16
|
+
exists: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Check if the provided path is absolute.
|
|
20
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
21
|
+
* @req REQ-SECURITY-VALIDATION - Prevent absolute path usage
|
|
22
|
+
*/
|
|
23
|
+
export declare function isAbsolutePath(p: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Check for path traversal patterns.
|
|
26
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
27
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal
|
|
28
|
+
*/
|
|
29
|
+
export declare function containsPathTraversal(p: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Determine if a path is unsafe due to traversal or being absolute.
|
|
32
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
33
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
34
|
+
*/
|
|
35
|
+
export declare function isTraversalUnsafe(p: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Validate that the story file has an allowed extension.
|
|
38
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
39
|
+
* @req REQ-SECURITY-VALIDATION - Enforce allowed file extensions
|
|
40
|
+
*/
|
|
41
|
+
export declare function hasValidExtension(p: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Determine if a story path is unsafe due to traversal, being absolute, or invalid extension.
|
|
44
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
45
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal, absolute path usage, and enforce allowed file extensions
|
|
46
|
+
*/
|
|
47
|
+
export declare function isUnsafeStoryPath(p: string): boolean;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildStoryCandidates = buildStoryCandidates;
|
|
7
|
+
exports.storyExists = storyExists;
|
|
8
|
+
exports.normalizeStoryPath = normalizeStoryPath;
|
|
9
|
+
exports.isAbsolutePath = isAbsolutePath;
|
|
10
|
+
exports.containsPathTraversal = containsPathTraversal;
|
|
11
|
+
exports.isTraversalUnsafe = isTraversalUnsafe;
|
|
12
|
+
exports.hasValidExtension = hasValidExtension;
|
|
13
|
+
exports.isUnsafeStoryPath = isUnsafeStoryPath;
|
|
14
|
+
/**
|
|
15
|
+
* Utility functions for story path resolution and existence checking.
|
|
16
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
17
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
18
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
19
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
20
|
+
*/
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
/**
|
|
24
|
+
* Build candidate file paths for a given story path.
|
|
25
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
26
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
27
|
+
*/
|
|
28
|
+
function buildStoryCandidates(storyPath, cwd, storyDirs) {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
|
|
31
|
+
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
35
|
+
for (const dir of storyDirs) {
|
|
36
|
+
candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return candidates;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if any of the provided file paths exist.
|
|
43
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
44
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
45
|
+
*/
|
|
46
|
+
const fileExistCache = new Map();
|
|
47
|
+
function storyExists(paths) {
|
|
48
|
+
for (const candidate of paths) {
|
|
49
|
+
let ok = fileExistCache.get(candidate);
|
|
50
|
+
if (ok === undefined) {
|
|
51
|
+
ok = fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile();
|
|
52
|
+
fileExistCache.set(candidate, ok);
|
|
53
|
+
}
|
|
54
|
+
if (ok) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a story path to candidate absolute paths and check existence.
|
|
62
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
63
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
64
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
65
|
+
*/
|
|
66
|
+
function normalizeStoryPath(storyPath, cwd, storyDirs) {
|
|
67
|
+
const candidates = buildStoryCandidates(storyPath, cwd, storyDirs);
|
|
68
|
+
const exists = storyExists(candidates);
|
|
69
|
+
return { candidates, exists };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if the provided path is absolute.
|
|
73
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
74
|
+
* @req REQ-SECURITY-VALIDATION - Prevent absolute path usage
|
|
75
|
+
*/
|
|
76
|
+
function isAbsolutePath(p) {
|
|
77
|
+
return path_1.default.isAbsolute(p);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check for path traversal patterns.
|
|
81
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
82
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal
|
|
83
|
+
*/
|
|
84
|
+
function containsPathTraversal(p) {
|
|
85
|
+
const normalized = path_1.default.normalize(p);
|
|
86
|
+
return normalized.split(path_1.default.sep).includes("..");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Determine if a path is unsafe due to traversal or being absolute.
|
|
90
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
91
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
92
|
+
*/
|
|
93
|
+
function isTraversalUnsafe(p) {
|
|
94
|
+
return isAbsolutePath(p) || containsPathTraversal(p);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate that the story file has an allowed extension.
|
|
98
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
99
|
+
* @req REQ-SECURITY-VALIDATION - Enforce allowed file extensions
|
|
100
|
+
*/
|
|
101
|
+
function hasValidExtension(p) {
|
|
102
|
+
return p.endsWith(".story.md");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Determine if a story path is unsafe due to traversal, being absolute, or invalid extension.
|
|
106
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
107
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal, absolute path usage, and enforce allowed file extensions
|
|
108
|
+
*/
|
|
109
|
+
function isUnsafeStoryPath(p) {
|
|
110
|
+
return isTraversalUnsafe(p) || !hasValidExtension(p);
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|