eslint-plugin-traceability 1.6.2 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/src/maintenance/detect.js +91 -21
- package/lib/src/rules/helpers/require-story-io.js +26 -2
- package/lib/src/rules/helpers/require-story-utils.d.ts +1 -1
- package/lib/src/rules/helpers/require-story-utils.js +7 -7
- package/lib/src/rules/helpers/require-story-visitors.js +11 -3
- package/lib/src/rules/require-story-annotation.js +13 -3
- package/lib/src/rules/valid-story-reference.js +72 -9
- package/lib/src/utils/annotation-checker.d.ts +4 -0
- package/lib/src/utils/annotation-checker.js +64 -4
- package/lib/src/utils/storyReferenceUtils.d.ts +28 -0
- package/lib/src/utils/storyReferenceUtils.js +62 -1
- package/lib/tests/rules/valid-story-reference.test.js +263 -2
- package/package.json +1 -1
|
@@ -37,37 +37,107 @@ exports.detectStaleAnnotations = detectStaleAnnotations;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const utils_1 = require("./utils");
|
|
40
|
+
const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
|
|
40
41
|
/**
|
|
41
42
|
* Detect stale annotation references that point to moved or deleted story files
|
|
42
43
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
43
44
|
* @req REQ-MAINT-DETECT - Detect stale annotation references
|
|
44
45
|
*/
|
|
45
46
|
function detectStaleAnnotations(codebasePath) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
49
|
+
// @req REQ-MAINT-DETECT - Treat codebasePath as a workspace root resolved from process.cwd()
|
|
50
|
+
const workspaceRoot = path.resolve(cwd, codebasePath);
|
|
51
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
52
|
+
// @req REQ-MAINT-DETECT - Return empty result if workspaceRoot does not exist or is not a directory
|
|
53
|
+
if (!fs.existsSync(workspaceRoot) ||
|
|
54
|
+
!fs.statSync(workspaceRoot).isDirectory()) {
|
|
49
55
|
return [];
|
|
50
56
|
}
|
|
51
|
-
const cwd = process.cwd();
|
|
52
|
-
const baseDir = path.resolve(cwd, codebasePath);
|
|
53
57
|
const stale = new Set();
|
|
54
|
-
|
|
55
|
-
// @
|
|
58
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
59
|
+
// @req REQ-MAINT-DETECT - Iterate over all files in the isolated workspace root
|
|
60
|
+
const files = (0, utils_1.getAllFiles)(workspaceRoot);
|
|
56
61
|
for (const file of files) {
|
|
57
|
-
|
|
58
|
-
const regex = /@story\s+([^\s]+)/g;
|
|
59
|
-
let match;
|
|
60
|
-
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Iterate over regex matches for @story annotations
|
|
61
|
-
while ((match = regex.exec(content)) !== null) {
|
|
62
|
-
const storyPath = match[1];
|
|
63
|
-
const storyProjectPath = path.resolve(cwd, storyPath);
|
|
64
|
-
const storyCodebasePath = path.resolve(baseDir, storyPath);
|
|
65
|
-
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Check if referenced story file is missing in both project and codebase paths
|
|
66
|
-
if (!fs.existsSync(storyProjectPath) &&
|
|
67
|
-
!fs.existsSync(storyCodebasePath)) {
|
|
68
|
-
stale.add(storyPath);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
62
|
+
processFileForStaleAnnotations(file, workspaceRoot, cwd, stale);
|
|
71
63
|
}
|
|
72
64
|
return Array.from(stale);
|
|
73
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
68
|
+
* @req REQ-MAINT-DETECT - Process individual files to detect stale @story annotations
|
|
69
|
+
*/
|
|
70
|
+
function processFileForStaleAnnotations(file, workspaceRoot, cwd, stale) {
|
|
71
|
+
let content;
|
|
72
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
73
|
+
// @req REQ-MAINT-DETECT - Handle file read errors gracefully
|
|
74
|
+
try {
|
|
75
|
+
content = fs.readFileSync(file, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const regex = /@story\s+([^\s]+)/g;
|
|
81
|
+
let match;
|
|
82
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
83
|
+
// @req REQ-MAINT-DETECT - Iterate over regex matches for @story annotations
|
|
84
|
+
while ((match = regex.exec(content)) !== null) {
|
|
85
|
+
handleStoryMatch(match[1], workspaceRoot, cwd, stale);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
90
|
+
* @req REQ-MAINT-DETECT - Handle individual @story matches within a file
|
|
91
|
+
*/
|
|
92
|
+
function handleStoryMatch(storyPath, workspaceRoot, cwd, stale) {
|
|
93
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
94
|
+
// @req REQ-MAINT-DETECT - Skip traversal/absolute-unsafe story paths before any filesystem or boundary checks
|
|
95
|
+
if ((0, storyReferenceUtils_1.isTraversalUnsafe)(storyPath)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
99
|
+
// @req REQ-MAINT-DETECT - Compute project and codebase candidates relative to cwd and workspaceRoot
|
|
100
|
+
const storyProjectCandidate = path.resolve(cwd, storyPath);
|
|
101
|
+
const storyCodebaseCandidate = path.resolve(workspaceRoot, storyPath);
|
|
102
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
103
|
+
// @req REQ-MAINT-DETECT - Enforce workspaceRoot as the project boundary for resolved story paths
|
|
104
|
+
let projectBoundary;
|
|
105
|
+
let codebaseBoundary;
|
|
106
|
+
try {
|
|
107
|
+
projectBoundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(storyProjectCandidate, workspaceRoot);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
projectBoundary = {
|
|
111
|
+
isWithinProject: false,
|
|
112
|
+
candidate: storyProjectCandidate,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
codebaseBoundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(storyCodebaseCandidate, workspaceRoot);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
codebaseBoundary = {
|
|
120
|
+
isWithinProject: false,
|
|
121
|
+
candidate: storyCodebaseCandidate,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const inProjectCandidates = [];
|
|
125
|
+
if (projectBoundary.isWithinProject) {
|
|
126
|
+
inProjectCandidates.push(projectBoundary.candidate);
|
|
127
|
+
}
|
|
128
|
+
if (codebaseBoundary.isWithinProject) {
|
|
129
|
+
inProjectCandidates.push(codebaseBoundary.candidate);
|
|
130
|
+
}
|
|
131
|
+
// If both candidates are out-of-project, do not mark as stale and skip FS checks
|
|
132
|
+
if (inProjectCandidates.length === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
136
|
+
// @req REQ-MAINT-DETECT - Only check existence for in-project candidates
|
|
137
|
+
const anyExists = inProjectCandidates.some((p) => fs.existsSync(p));
|
|
138
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
139
|
+
// @req REQ-MAINT-DETECT - Mark story as stale if any in-project candidate exists conceptually but none exist on disk
|
|
140
|
+
if (!anyExists) {
|
|
141
|
+
stale.add(storyPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -31,13 +31,22 @@ function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES
|
|
|
31
31
|
const startLine = node && node.loc && typeof node.loc.start?.line === "number"
|
|
32
32
|
? node.loc.start.line
|
|
33
33
|
: null;
|
|
34
|
+
// Guard against missing or non-array source lines or an invalid start line before scanning.
|
|
35
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
36
|
+
// @req REQ-ANNOTATION-REQUIRED - Fail gracefully when source lines or locations are unavailable
|
|
34
37
|
if (!Array.isArray(lines) || typeof startLine !== "number") {
|
|
35
38
|
return false;
|
|
36
39
|
}
|
|
37
40
|
const from = Math.max(0, startLine - 1 - lookback);
|
|
38
41
|
const to = Math.max(0, startLine - 1);
|
|
42
|
+
// Walk each physical line in the configured lookback window to search for an inline @story marker.
|
|
43
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
44
|
+
// @req REQ-ANNOTATION-REQUIRED - Scan preceding lines for existing story annotations
|
|
39
45
|
for (let i = from; i < to; i++) {
|
|
40
46
|
const text = lines[i];
|
|
47
|
+
// Treat any line containing "@story" as evidence that the function is already annotated.
|
|
48
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
49
|
+
// @req REQ-ANNOTATION-REQUIRED - Detect explicit @story markers in raw source text
|
|
41
50
|
if (typeof text === "string" && text.includes("@story")) {
|
|
42
51
|
return true;
|
|
43
52
|
}
|
|
@@ -84,24 +93,39 @@ function parentChainHasStory(sourceCode, node) {
|
|
|
84
93
|
* @req REQ-ANNOTATION-REQUIRED - Provide fallback textual inspection when other heuristics fail
|
|
85
94
|
*/
|
|
86
95
|
function fallbackTextBeforeHasStory(sourceCode, node) {
|
|
96
|
+
// Skip fallback text inspection when the sourceCode API or node range information is not available.
|
|
97
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
98
|
+
// @req REQ-ANNOTATION-REQUIRED - Avoid throwing when source text or range metadata cannot be read
|
|
87
99
|
if (typeof sourceCode?.getText !== "function" ||
|
|
88
100
|
!Array.isArray((node && node.range) || [])) {
|
|
89
101
|
return false;
|
|
90
102
|
}
|
|
91
103
|
const range = node.range;
|
|
104
|
+
// Guard against malformed range values that cannot provide a numeric start index for slicing.
|
|
105
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
106
|
+
// @req REQ-ANNOTATION-REQUIRED - Validate node range structure before computing fallback window
|
|
92
107
|
if (!Array.isArray(range) || typeof range[0] !== "number") {
|
|
93
108
|
return false;
|
|
94
109
|
}
|
|
95
110
|
try {
|
|
96
|
-
// Limit the fallback
|
|
111
|
+
// Limit the fallback inspection window to a bounded region immediately preceding the node.
|
|
112
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
113
|
+
// @req REQ-ANNOTATION-REQUIRED - Restrict fallback text scanning to a safe, fixed-size window
|
|
97
114
|
const start = Math.max(0, range[0] - exports.FALLBACK_WINDOW);
|
|
98
115
|
const textBefore = sourceCode.getText().slice(start, range[0]);
|
|
116
|
+
// Detect any @story marker that appears within the bounded fallback window.
|
|
117
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
118
|
+
// @req REQ-ANNOTATION-REQUIRED - Recognize story annotations discovered via fallback text scanning
|
|
99
119
|
if (typeof textBefore === "string" && textBefore.includes("@story")) {
|
|
100
120
|
return true;
|
|
101
121
|
}
|
|
102
122
|
}
|
|
103
123
|
catch {
|
|
104
|
-
/*
|
|
124
|
+
/*
|
|
125
|
+
* Swallow low-level IO or slicing errors so annotation detection never breaks lint execution.
|
|
126
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
127
|
+
* @req REQ-ANNOTATION-REQUIRED - Treat fallback text inspection failures as "no annotation" instead of raising
|
|
128
|
+
*/
|
|
105
129
|
}
|
|
106
130
|
return false;
|
|
107
131
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* Get a readable name for a given AST node.
|
|
27
27
|
*
|
|
28
28
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
29
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
29
|
+
* @req REQ-ANNOTATION-REQUIRED - Provide a unified way to obtain a stable, human-readable name from AST nodes
|
|
30
30
|
*
|
|
31
31
|
* @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
|
|
32
32
|
* @returns The resolved name string, or null if a stable name cannot be determined.
|
|
@@ -29,7 +29,7 @@ exports.getNodeName = getNodeName;
|
|
|
29
29
|
* Check for identifier-like nodes and return their name when available.
|
|
30
30
|
*
|
|
31
31
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
32
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
32
|
+
* @req REQ-ANNOTATION-REQUIRED - Recognize Identifier/JSXIdentifier nodes and return their name
|
|
33
33
|
*
|
|
34
34
|
* @param node - AST node to inspect
|
|
35
35
|
* @returns the identifier name or null
|
|
@@ -47,7 +47,7 @@ function isIdentifierLike(node) {
|
|
|
47
47
|
* Convert a Literal node to a string when it represents a stable primitive (string/number/boolean).
|
|
48
48
|
*
|
|
49
49
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
50
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
50
|
+
* @req REQ-ANNOTATION-REQUIRED - Convert simple Literal nodes into stable string names when possible
|
|
51
51
|
*
|
|
52
52
|
* @param node - AST node expected to be a Literal
|
|
53
53
|
* @returns the literal as string or null if not stable/resolvable
|
|
@@ -67,7 +67,7 @@ function literalToString(node) {
|
|
|
67
67
|
* Convert a TemplateLiteral node to a string if it contains no expressions.
|
|
68
68
|
*
|
|
69
69
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
70
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
70
|
+
* @req REQ-ANNOTATION-REQUIRED - Support simple, expression-free TemplateLiteral names for reporting
|
|
71
71
|
*
|
|
72
72
|
* @param node - AST node expected to be a TemplateLiteral
|
|
73
73
|
* @returns the cooked/raw concatenated template string or null if it contains expressions
|
|
@@ -95,7 +95,7 @@ function templateLiteralToString(node) {
|
|
|
95
95
|
* Resolve a MemberExpression / TSQualifiedName / JSXMemberExpression-like node to a name when non-computed.
|
|
96
96
|
*
|
|
97
97
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
98
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
98
|
+
* @req REQ-ANNOTATION-REQUIRED - Resolve non-computed member-like nodes into property names when safe
|
|
99
99
|
*
|
|
100
100
|
* @param node - AST node to inspect
|
|
101
101
|
* @returns resolved member/property name or null
|
|
@@ -121,7 +121,7 @@ function memberExpressionName(node) {
|
|
|
121
121
|
* Extract the key name from Property/ObjectProperty nodes.
|
|
122
122
|
*
|
|
123
123
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
124
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
124
|
+
* @req REQ-ANNOTATION-REQUIRED - Extract key names from Property/ObjectProperty nodes used in function containers
|
|
125
125
|
*
|
|
126
126
|
* @param node - AST node expected to be Property/ObjectProperty
|
|
127
127
|
* @returns the resolved key name or null
|
|
@@ -141,7 +141,7 @@ function propertyKeyName(node) {
|
|
|
141
141
|
* Branch-level traceability: prefer direct .key.name early (common on variable declarators, properties)
|
|
142
142
|
*
|
|
143
143
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
144
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
144
|
+
* @req REQ-ANNOTATION-REQUIRED - Prefer direct id/key names before falling back to deeper AST inspection
|
|
145
145
|
*
|
|
146
146
|
* @param node - AST node to inspect for direct .id/.key name
|
|
147
147
|
* @returns the resolved direct name or null
|
|
@@ -173,7 +173,7 @@ function directName(node) {
|
|
|
173
173
|
* Get a readable name for a given AST node.
|
|
174
174
|
*
|
|
175
175
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
176
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
176
|
+
* @req REQ-ANNOTATION-REQUIRED - Provide a unified way to obtain a stable, human-readable name from AST nodes
|
|
177
177
|
*
|
|
178
178
|
* @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
|
|
179
179
|
* @returns The resolved name string, or null if a stable name cannot be determined.
|
|
@@ -13,6 +13,12 @@ const require_story_helpers_1 = require("./require-story-helpers");
|
|
|
13
13
|
* @req REQ-BUILD-VISITORS-FNDECL - Provide visitor for FunctionDeclaration
|
|
14
14
|
*/
|
|
15
15
|
function buildFunctionDeclarationVisitor(context, sourceCode, options) {
|
|
16
|
+
/**
|
|
17
|
+
* Debug flag for optional visitor logging.
|
|
18
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
19
|
+
* @req REQ-DEBUG-LOG-TOGGLE - Allow opt-in debug logging via TRACEABILITY_DEBUG
|
|
20
|
+
*/
|
|
21
|
+
const debugEnabled = process.env.TRACEABILITY_DEBUG === "1";
|
|
16
22
|
/**
|
|
17
23
|
* Handle FunctionDeclaration nodes.
|
|
18
24
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -24,9 +30,11 @@ function buildFunctionDeclarationVisitor(context, sourceCode, options) {
|
|
|
24
30
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
25
31
|
* @req REQ-DEBUG-LOG - Provide debug logging for visitor entry
|
|
26
32
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
if (debugEnabled) {
|
|
34
|
+
console.debug("require-story-annotation:FunctionDeclaration", typeof context.getFilename === "function"
|
|
35
|
+
? context.getFilename()
|
|
36
|
+
: "<unknown>", node && node.id ? node.id.name : "<anonymous>");
|
|
37
|
+
}
|
|
30
38
|
if (!options.shouldProcessNode(node))
|
|
31
39
|
return;
|
|
32
40
|
const target = (0, require_story_helpers_1.resolveTargetNode)(sourceCode, node);
|
|
@@ -63,15 +63,25 @@ const rule = {
|
|
|
63
63
|
const opts = (context.options && context.options[0]) || {};
|
|
64
64
|
const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
|
|
65
65
|
const exportPriority = opts.exportPriority || "all";
|
|
66
|
+
/**
|
|
67
|
+
* Environment-gated debug logging to avoid leaking file paths unless
|
|
68
|
+
* explicitly enabled.
|
|
69
|
+
*
|
|
70
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
71
|
+
* @req REQ-DEBUG-LOG
|
|
72
|
+
*/
|
|
73
|
+
const debugEnabled = process.env.TRACEABILITY_DEBUG === "1";
|
|
66
74
|
/**
|
|
67
75
|
* Debug log at the start of create to help diagnose rule activation in tests.
|
|
68
76
|
*
|
|
69
77
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
70
78
|
* @req REQ-DEBUG-LOG
|
|
71
79
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
if (debugEnabled) {
|
|
81
|
+
console.debug("require-story-annotation:create", typeof context.getFilename === "function"
|
|
82
|
+
? context.getFilename()
|
|
83
|
+
: "<unknown>");
|
|
84
|
+
}
|
|
75
85
|
// Local closure that binds configured scope and export priority to the helper.
|
|
76
86
|
const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority);
|
|
77
87
|
// Delegate visitor construction to helper to keep this file concise.
|
|
@@ -36,18 +36,37 @@ function validateStoryPath(opts) {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Analyze candidate paths against the project boundary, returning whether any
|
|
40
|
+
* are within the project and whether any are outside.
|
|
41
|
+
*
|
|
42
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
43
|
+
* @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
|
|
44
|
+
* @req REQ-CONFIGURABLE-PATHS - Respect configured storyDirectories while enforcing project boundaries
|
|
45
|
+
*/
|
|
46
|
+
function analyzeCandidateBoundaries(candidates, cwd) {
|
|
47
|
+
let hasInProjectCandidate = false;
|
|
48
|
+
let hasOutOfProjectCandidate = false;
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(candidate, cwd);
|
|
51
|
+
if (boundary.isWithinProject) {
|
|
52
|
+
hasInProjectCandidate = true;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
hasOutOfProjectCandidate = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { hasInProjectCandidate, hasOutOfProjectCandidate };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Handle existence status and report appropriate diagnostics for missing
|
|
62
|
+
* or filesystem-error conditions, assuming project-boundary checks have
|
|
63
|
+
* already been applied.
|
|
42
64
|
*
|
|
43
65
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
44
66
|
* @req REQ-FILE-EXISTENCE - Ensure referenced files exist
|
|
45
67
|
* @req REQ-ERROR-HANDLING - Differentiate missing files from filesystem errors
|
|
46
68
|
*/
|
|
47
|
-
function
|
|
48
|
-
const { storyPath, commentNode, context, cwd, storyDirs } = opts;
|
|
49
|
-
const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
|
|
50
|
-
const existenceResult = result.existence;
|
|
69
|
+
function reportExistenceStatus(existenceResult, storyPath, commentNode, context) {
|
|
51
70
|
if (!existenceResult || existenceResult.status === "exists") {
|
|
52
71
|
return;
|
|
53
72
|
}
|
|
@@ -81,6 +100,48 @@ function reportExistenceProblems(opts) {
|
|
|
81
100
|
});
|
|
82
101
|
}
|
|
83
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Report any problems related to the existence or accessibility of the
|
|
105
|
+
* referenced story file. Filesystem and I/O errors are surfaced with a
|
|
106
|
+
* dedicated diagnostic that differentiates them from missing files.
|
|
107
|
+
*
|
|
108
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
109
|
+
* @req REQ-FILE-EXISTENCE - Ensure referenced files exist
|
|
110
|
+
* @req REQ-ERROR-HANDLING - Differentiate missing files from filesystem errors
|
|
111
|
+
* @req REQ-PROJECT-BOUNDARY - Ensure resolved candidate paths remain within the project root
|
|
112
|
+
* @req REQ-CONFIGURABLE-PATHS - Respect configured storyDirectories while enforcing project boundaries
|
|
113
|
+
*/
|
|
114
|
+
function reportExistenceProblems(opts) {
|
|
115
|
+
const { storyPath, commentNode, context, cwd, storyDirs } = opts;
|
|
116
|
+
const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
|
|
117
|
+
const existenceResult = result.existence;
|
|
118
|
+
const candidates = result.candidates || [];
|
|
119
|
+
if (candidates.length > 0) {
|
|
120
|
+
const { hasInProjectCandidate, hasOutOfProjectCandidate } = analyzeCandidateBoundaries(candidates, cwd);
|
|
121
|
+
if (hasOutOfProjectCandidate && !hasInProjectCandidate) {
|
|
122
|
+
context.report({
|
|
123
|
+
node: commentNode,
|
|
124
|
+
messageId: "invalidPath",
|
|
125
|
+
data: { path: storyPath },
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (existenceResult &&
|
|
131
|
+
existenceResult.status === "exists" &&
|
|
132
|
+
existenceResult.matchedPath) {
|
|
133
|
+
const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(existenceResult.matchedPath, cwd);
|
|
134
|
+
if (!boundary.isWithinProject) {
|
|
135
|
+
context.report({
|
|
136
|
+
node: commentNode,
|
|
137
|
+
messageId: "invalidPath",
|
|
138
|
+
data: { path: storyPath },
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
reportExistenceStatus(existenceResult, storyPath, commentNode, context);
|
|
144
|
+
}
|
|
84
145
|
/**
|
|
85
146
|
* Process and validate the story path for security, extension, and existence.
|
|
86
147
|
* Filesystem and I/O errors are handled inside the underlying utilities
|
|
@@ -103,8 +164,10 @@ function processStoryPath(opts) {
|
|
|
103
164
|
messageId: "invalidPath",
|
|
104
165
|
data: { path: storyPath },
|
|
105
166
|
});
|
|
167
|
+
return;
|
|
106
168
|
}
|
|
107
|
-
|
|
169
|
+
// When absolute paths are allowed, we still enforce extension and
|
|
170
|
+
// project-boundary checks below via the existence phase.
|
|
108
171
|
}
|
|
109
172
|
// Path traversal check
|
|
110
173
|
if ((0, storyReferenceUtils_1.containsPathTraversal)(storyPath)) {
|
|
@@ -223,7 +286,7 @@ exports.default = {
|
|
|
223
286
|
],
|
|
224
287
|
},
|
|
225
288
|
create(context) {
|
|
226
|
-
const cwd = process.cwd();
|
|
289
|
+
const cwd = context.cwd ?? process.cwd();
|
|
227
290
|
const opts = context.options[0];
|
|
228
291
|
const storyDirs = opts?.storyDirectories || defaultStoryDirs;
|
|
229
292
|
const allowAbsolute = opts?.allowAbsolutePaths || false;
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
|
|
7
7
|
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
8
8
|
* @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
|
|
9
|
+
* @param context - ESLint rule context used to obtain source and report problems
|
|
10
|
+
* @param node - Function-like AST node whose surrounding comments should be inspected
|
|
11
|
+
* @param options - Optional configuration controlling behaviour (e.g., enableFix)
|
|
12
|
+
* @returns void
|
|
9
13
|
*/
|
|
10
14
|
export declare function checkReqAnnotation(context: any, node: any, options?: {
|
|
11
15
|
enableFix?: boolean;
|
|
@@ -45,19 +45,30 @@ function commentContainsReq(c) {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Line-based helper adapted from linesBeforeHasStory to detect @req.
|
|
48
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
49
|
+
* @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in preceding source lines
|
|
48
50
|
*/
|
|
49
51
|
function linesBeforeHasReq(sourceCode, node) {
|
|
50
52
|
const lines = sourceCode && sourceCode.lines;
|
|
51
53
|
const startLine = node && node.loc && typeof node.loc.start?.line === "number"
|
|
52
54
|
? node.loc.start.line
|
|
53
55
|
: null;
|
|
56
|
+
// Guard against missing or malformed source/loc information before scanning.
|
|
57
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
58
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Avoid false positives when sourceCode/loc is incomplete
|
|
54
59
|
if (!Array.isArray(lines) || typeof startLine !== "number") {
|
|
55
60
|
return false;
|
|
56
61
|
}
|
|
57
62
|
const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
|
|
58
63
|
const to = Math.max(0, startLine - 1);
|
|
64
|
+
// Scan each physical line in the configured lookback window for an @req marker.
|
|
65
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
66
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Search preceding lines for @req text
|
|
59
67
|
for (let i = from; i < to; i++) {
|
|
60
68
|
const text = lines[i];
|
|
69
|
+
// When a line contains @req we treat the function as already annotated.
|
|
70
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
71
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in raw source lines
|
|
61
72
|
if (typeof text === "string" && text.includes("@req")) {
|
|
62
73
|
return true;
|
|
63
74
|
}
|
|
@@ -66,20 +77,29 @@ function linesBeforeHasReq(sourceCode, node) {
|
|
|
66
77
|
}
|
|
67
78
|
/**
|
|
68
79
|
* Parent-chain helper adapted from parentChainHasStory to detect @req.
|
|
80
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
81
|
+
* @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in parent-chain comments
|
|
69
82
|
*/
|
|
70
83
|
function parentChainHasReq(sourceCode, node) {
|
|
71
84
|
let p = node && node.parent;
|
|
85
|
+
// Walk up the parent chain and inspect comments attached to each ancestor.
|
|
86
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
87
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Traverse parent nodes when local comments are absent
|
|
72
88
|
while (p) {
|
|
73
89
|
const pComments = typeof sourceCode?.getCommentsBefore === "function"
|
|
74
90
|
? sourceCode.getCommentsBefore(p) || []
|
|
75
91
|
: [];
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
// Look for @req in comments immediately preceding each parent node.
|
|
93
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
94
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent comments
|
|
95
|
+
if (Array.isArray(pComments) && pComments.some(commentContainsReq)) {
|
|
78
96
|
return true;
|
|
79
97
|
}
|
|
80
98
|
const pLeading = p.leadingComments || [];
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
// Also inspect leadingComments attached directly to the parent node.
|
|
100
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
101
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent leadingComments
|
|
102
|
+
if (Array.isArray(pLeading) && pLeading.some(commentContainsReq)) {
|
|
83
103
|
return true;
|
|
84
104
|
}
|
|
85
105
|
p = p.parent;
|
|
@@ -88,24 +108,38 @@ function parentChainHasReq(sourceCode, node) {
|
|
|
88
108
|
}
|
|
89
109
|
/**
|
|
90
110
|
* Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
|
|
111
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
112
|
+
* @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in fallback text window before node
|
|
91
113
|
*/
|
|
92
114
|
function fallbackTextBeforeHasReq(sourceCode, node) {
|
|
115
|
+
// Guard against unsupported sourceCode or nodes without a usable range.
|
|
116
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
117
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Ensure we only inspect text when range information is available
|
|
93
118
|
if (typeof sourceCode?.getText !== "function" ||
|
|
94
119
|
!Array.isArray((node && node.range) || [])) {
|
|
95
120
|
return false;
|
|
96
121
|
}
|
|
97
122
|
const range = node.range;
|
|
123
|
+
// Guard when the node range cannot provide a numeric start index.
|
|
124
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
125
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Avoid scanning when range start is not a number
|
|
98
126
|
if (!Array.isArray(range) || typeof range[0] !== "number") {
|
|
99
127
|
return false;
|
|
100
128
|
}
|
|
101
129
|
try {
|
|
102
130
|
const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
|
|
103
131
|
const textBefore = sourceCode.getText().slice(start, range[0]);
|
|
132
|
+
// Detect @req in the bounded text window immediately preceding the node.
|
|
133
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
134
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in fallback text window
|
|
104
135
|
if (typeof textBefore === "string" && textBefore.includes("@req")) {
|
|
105
136
|
return true;
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
catch {
|
|
140
|
+
// Swallow detection errors to avoid breaking lint runs due to malformed source.
|
|
141
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
142
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Treat IO/detection failures as "no annotation" instead of throwing
|
|
109
143
|
/* noop */
|
|
110
144
|
}
|
|
111
145
|
return false;
|
|
@@ -120,6 +154,9 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
|
120
154
|
const sourceCode = context && typeof context.getSourceCode === "function"
|
|
121
155
|
? context.getSourceCode()
|
|
122
156
|
: undefined;
|
|
157
|
+
// Prefer robust, location-based heuristics when sourceCode and node are available.
|
|
158
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
159
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Use multiple heuristics to detect @req markers around the node
|
|
123
160
|
if (sourceCode && node) {
|
|
124
161
|
if (linesBeforeHasReq(sourceCode, node) ||
|
|
125
162
|
parentChainHasReq(sourceCode, node) ||
|
|
@@ -130,6 +167,8 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
|
130
167
|
}
|
|
131
168
|
catch {
|
|
132
169
|
// Swallow detection errors and fall through to simple checks.
|
|
170
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
171
|
+
// @req REQ-ANNOTATION-REQ-DETECTION - Fail gracefully when advanced detection heuristics throw
|
|
133
172
|
}
|
|
134
173
|
// BRANCH @req detection on JSDoc or comments
|
|
135
174
|
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -148,19 +187,28 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
|
148
187
|
*/
|
|
149
188
|
function getFixTargetNode(node) {
|
|
150
189
|
const parent = node && node.parent;
|
|
190
|
+
// When there is no parent, attach the annotation directly to the node itself.
|
|
191
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
192
|
+
// @req REQ-ANNOTATION-AUTOFIX - Default to annotating the node when it has no parent
|
|
151
193
|
if (!parent) {
|
|
152
194
|
return node;
|
|
153
195
|
}
|
|
154
196
|
// If the node is part of a class/obj method definition, attach to the MethodDefinition
|
|
197
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
198
|
+
// @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the MethodDefinition wrapper for methods
|
|
155
199
|
if (parent.type === "MethodDefinition") {
|
|
156
200
|
return parent;
|
|
157
201
|
}
|
|
158
202
|
// If the node is the init of a variable declarator, attach to the VariableDeclarator
|
|
203
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
204
|
+
// @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the VariableDeclarator for function initializers
|
|
159
205
|
if (parent.type === "VariableDeclarator" && parent.init === node) {
|
|
160
206
|
return parent;
|
|
161
207
|
}
|
|
162
208
|
// If the parent is an expression statement (e.g. IIFE or assigned via expression),
|
|
163
209
|
// attach to the outer ExpressionStatement.
|
|
210
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
211
|
+
// @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the ExpressionStatement wrapper for IIFEs
|
|
164
212
|
if (parent.type === "ExpressionStatement") {
|
|
165
213
|
return parent;
|
|
166
214
|
}
|
|
@@ -174,6 +222,11 @@ function getFixTargetNode(node) {
|
|
|
174
222
|
*/
|
|
175
223
|
function createMissingReqFix(node) {
|
|
176
224
|
const target = getFixTargetNode(node);
|
|
225
|
+
/**
|
|
226
|
+
* Fixer used to insert a default @req annotation before the chosen target node.
|
|
227
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
228
|
+
* @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
|
|
229
|
+
*/
|
|
177
230
|
return function missingReqFix(fixer) {
|
|
178
231
|
return fixer.insertTextBefore(target, "/** @req <REQ-ID> */\n");
|
|
179
232
|
};
|
|
@@ -202,6 +255,9 @@ function reportMissing(context, node, enableFix = true) {
|
|
|
202
255
|
messageId: "missingReq",
|
|
203
256
|
data: { name, functionName: name },
|
|
204
257
|
};
|
|
258
|
+
// Conditionally attach an autofix only when enabled in the rule options.
|
|
259
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
260
|
+
// @req REQ-ANNOTATION-AUTOFIX - Only provide autofix suggestions when explicitly enabled
|
|
205
261
|
if (enableFix) {
|
|
206
262
|
reportOptions.fix = createMissingReqFix(node);
|
|
207
263
|
}
|
|
@@ -215,6 +271,10 @@ function reportMissing(context, node, enableFix = true) {
|
|
|
215
271
|
* @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
|
|
216
272
|
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
217
273
|
* @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
|
|
274
|
+
* @param context - ESLint rule context used to obtain source and report problems
|
|
275
|
+
* @param node - Function-like AST node whose surrounding comments should be inspected
|
|
276
|
+
* @param options - Optional configuration controlling behaviour (e.g., enableFix)
|
|
277
|
+
* @returns void
|
|
218
278
|
*/
|
|
219
279
|
function checkReqAnnotation(context, node, options) {
|
|
220
280
|
const { enableFix = true } = options ?? {};
|
|
@@ -31,6 +31,34 @@ export interface StoryExistenceResult {
|
|
|
31
31
|
matchedPath?: string;
|
|
32
32
|
error?: unknown;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Result of validating that a candidate path stays within the project boundary.
|
|
36
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
37
|
+
* @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
|
|
38
|
+
*/
|
|
39
|
+
export interface ProjectBoundaryCheckResult {
|
|
40
|
+
candidate: string;
|
|
41
|
+
isWithinProject: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate that a candidate path stays within the project boundary.
|
|
45
|
+
* This compares the resolved candidate path against the normalized cwd
|
|
46
|
+
* prefix, ensuring that even when storyDirectories are misconfigured, we
|
|
47
|
+
* never treat files outside the project as valid story references.
|
|
48
|
+
*
|
|
49
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
50
|
+
* @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
|
|
51
|
+
*/
|
|
52
|
+
export declare function enforceProjectBoundary(candidate: string, cwd: string): ProjectBoundaryCheckResult;
|
|
53
|
+
/**
|
|
54
|
+
* Internal helper to reset the filesystem existence cache. This is primarily
|
|
55
|
+
* intended for tests that need to run multiple scenarios with different
|
|
56
|
+
* mocked filesystem behavior without carrying over cached results.
|
|
57
|
+
*
|
|
58
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
59
|
+
* @req REQ-PERFORMANCE-OPTIMIZATION - Allow safe cache reset in tests to avoid stale entries
|
|
60
|
+
*/
|
|
61
|
+
export declare function __resetStoryExistenceCacheForTests(): void;
|
|
34
62
|
/**
|
|
35
63
|
* Build candidate file paths for a given story path.
|
|
36
64
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.enforceProjectBoundary = enforceProjectBoundary;
|
|
7
|
+
exports.__resetStoryExistenceCacheForTests = __resetStoryExistenceCacheForTests;
|
|
6
8
|
exports.buildStoryCandidates = buildStoryCandidates;
|
|
7
9
|
exports.getStoryExistence = getStoryExistence;
|
|
8
10
|
exports.storyExists = storyExists;
|
|
@@ -22,6 +24,36 @@ exports.isUnsafeStoryPath = isUnsafeStoryPath;
|
|
|
22
24
|
*/
|
|
23
25
|
const fs_1 = __importDefault(require("fs"));
|
|
24
26
|
const path_1 = __importDefault(require("path"));
|
|
27
|
+
/**
|
|
28
|
+
* Validate that a candidate path stays within the project boundary.
|
|
29
|
+
* This compares the resolved candidate path against the normalized cwd
|
|
30
|
+
* prefix, ensuring that even when storyDirectories are misconfigured, we
|
|
31
|
+
* never treat files outside the project as valid story references.
|
|
32
|
+
*
|
|
33
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
34
|
+
* @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
|
|
35
|
+
*/
|
|
36
|
+
function enforceProjectBoundary(candidate, cwd) {
|
|
37
|
+
const normalizedCwd = path_1.default.resolve(cwd);
|
|
38
|
+
const normalizedCandidate = path_1.default.resolve(candidate);
|
|
39
|
+
const isWithinProject = normalizedCandidate === normalizedCwd ||
|
|
40
|
+
normalizedCandidate.startsWith(normalizedCwd + path_1.default.sep);
|
|
41
|
+
return {
|
|
42
|
+
candidate: normalizedCandidate,
|
|
43
|
+
isWithinProject,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Internal helper to reset the filesystem existence cache. This is primarily
|
|
48
|
+
* intended for tests that need to run multiple scenarios with different
|
|
49
|
+
* mocked filesystem behavior without carrying over cached results.
|
|
50
|
+
*
|
|
51
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
52
|
+
* @req REQ-PERFORMANCE-OPTIMIZATION - Allow safe cache reset in tests to avoid stale entries
|
|
53
|
+
*/
|
|
54
|
+
function __resetStoryExistenceCacheForTests() {
|
|
55
|
+
fileExistStatusCache.clear();
|
|
56
|
+
}
|
|
25
57
|
/**
|
|
26
58
|
* Build candidate file paths for a given story path.
|
|
27
59
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
@@ -29,11 +61,20 @@ const path_1 = __importDefault(require("path"));
|
|
|
29
61
|
*/
|
|
30
62
|
function buildStoryCandidates(storyPath, cwd, storyDirs) {
|
|
31
63
|
const candidates = [];
|
|
64
|
+
// When the story path is already explicitly relative, resolve it only against the current working directory.
|
|
65
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
66
|
+
// @req REQ-PATH-RESOLUTION - Preserve explicit relative story path semantics when building candidate locations
|
|
32
67
|
if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
|
|
33
68
|
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
34
69
|
}
|
|
35
70
|
else {
|
|
71
|
+
// For bare paths, first try resolving directly under the current working directory.
|
|
72
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
73
|
+
// @req REQ-PATH-RESOLUTION - Attempt direct resolution from cwd before probing configured story directories
|
|
36
74
|
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
75
|
+
// Probe each configured story directory for a matching story filename.
|
|
76
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
77
|
+
// @req REQ-PATH-RESOLUTION - Expand search across configured storyDirectories while staying within project
|
|
37
78
|
for (const dir of storyDirs) {
|
|
38
79
|
candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
|
|
39
80
|
}
|
|
@@ -59,17 +100,26 @@ const fileExistStatusCache = new Map();
|
|
|
59
100
|
*/
|
|
60
101
|
function checkSingleCandidate(candidate) {
|
|
61
102
|
const cached = fileExistStatusCache.get(candidate);
|
|
103
|
+
// Reuse any cached filesystem result to avoid redundant disk IO for the same candidate.
|
|
104
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
105
|
+
// @req REQ-PERFORMANCE-OPTIMIZATION - Short-circuit on cached existence checks
|
|
62
106
|
if (cached) {
|
|
63
107
|
return cached;
|
|
64
108
|
}
|
|
65
109
|
let result;
|
|
66
110
|
try {
|
|
67
111
|
const exists = fs_1.default.existsSync(candidate);
|
|
112
|
+
// When the path does not exist at all, record a simple "missing" status.
|
|
113
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
114
|
+
// @req REQ-FILE-EXISTENCE - Distinguish non-existent story paths from other failure modes
|
|
68
115
|
if (!exists) {
|
|
69
116
|
result = { path: candidate, status: "missing" };
|
|
70
117
|
}
|
|
71
118
|
else {
|
|
72
119
|
const stat = fs_1.default.statSync(candidate);
|
|
120
|
+
// Treat existing regular files as valid story candidates; other entry types are considered missing.
|
|
121
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
122
|
+
// @req REQ-FILE-EXISTENCE - Only regular files may satisfy a story path reference
|
|
73
123
|
if (stat.isFile()) {
|
|
74
124
|
result = { path: candidate, status: "exists" };
|
|
75
125
|
}
|
|
@@ -80,7 +130,9 @@ function checkSingleCandidate(candidate) {
|
|
|
80
130
|
}
|
|
81
131
|
}
|
|
82
132
|
catch (error) {
|
|
83
|
-
// Any filesystem error is captured and surfaced as fs-error.
|
|
133
|
+
// Any filesystem error is captured and surfaced as an fs-error status instead of throwing.
|
|
134
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
135
|
+
// @req REQ-ERROR-HANDLING - Represent filesystem failures as fs-error results while keeping callers resilient
|
|
84
136
|
result = { path: candidate, status: "fs-error", error };
|
|
85
137
|
}
|
|
86
138
|
fileExistStatusCache.set(candidate, result);
|
|
@@ -104,6 +156,9 @@ function getStoryExistence(candidates) {
|
|
|
104
156
|
let firstFsError;
|
|
105
157
|
for (const candidate of candidates) {
|
|
106
158
|
const res = checkSingleCandidate(candidate);
|
|
159
|
+
// As soon as a candidate file is confirmed to exist, return a successful existence result.
|
|
160
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
161
|
+
// @req REQ-FILE-EXISTENCE - Prefer the first positively-matched story file
|
|
107
162
|
if (res.status === "exists") {
|
|
108
163
|
return {
|
|
109
164
|
candidates,
|
|
@@ -111,10 +166,16 @@ function getStoryExistence(candidates) {
|
|
|
111
166
|
matchedPath: res.path,
|
|
112
167
|
};
|
|
113
168
|
}
|
|
169
|
+
// Remember the first filesystem error so callers can inspect a representative failure if no files exist.
|
|
170
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
171
|
+
// @req REQ-ERROR-HANDLING - Surface a single representative filesystem error without failing fast
|
|
114
172
|
if (res.status === "fs-error" && !firstFsError) {
|
|
115
173
|
firstFsError = res;
|
|
116
174
|
}
|
|
117
175
|
}
|
|
176
|
+
// Prefer reporting a filesystem error over a generic missing status when at least one candidate failed to read.
|
|
177
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
178
|
+
// @req REQ-ERROR-HANDLING - Distinguish IO failures from simple "missing" results in existence checks
|
|
118
179
|
if (firstFsError) {
|
|
119
180
|
return {
|
|
120
181
|
candidates,
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -16,6 +49,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
49
|
const eslint_1 = require("eslint");
|
|
17
50
|
const valid_story_reference_1 = __importDefault(require("../../src/rules/valid-story-reference"));
|
|
18
51
|
const storyReferenceUtils_1 = require("../../src/utils/storyReferenceUtils");
|
|
52
|
+
const path = __importStar(require("path"));
|
|
19
53
|
const ruleTester = new eslint_1.RuleTester({
|
|
20
54
|
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
21
55
|
});
|
|
@@ -73,6 +107,233 @@ describe("Valid Story Reference Rule (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
|
73
107
|
],
|
|
74
108
|
});
|
|
75
109
|
});
|
|
110
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
111
|
+
// @req REQ-CONFIGURABLE-PATHS - Verify custom storyDirectories behavior
|
|
112
|
+
const configurablePathsTester = new eslint_1.RuleTester({
|
|
113
|
+
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
114
|
+
});
|
|
115
|
+
configurablePathsTester.run("valid-story-reference", valid_story_reference_1.default, {
|
|
116
|
+
valid: [
|
|
117
|
+
{
|
|
118
|
+
name: "[REQ-CONFIGURABLE-PATHS] honors custom storyDirectories using docs/stories",
|
|
119
|
+
code: `// @story 001.0-DEV-PLUGIN-SETUP.story.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
|
|
120
|
+
options: [{ storyDirectories: ["docs/stories"] }],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
invalid: [],
|
|
124
|
+
});
|
|
125
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
126
|
+
// @req REQ-CONFIGURABLE-PATHS - Verify allowAbsolutePaths behavior
|
|
127
|
+
const allowAbsolutePathsTester = new eslint_1.RuleTester({
|
|
128
|
+
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
129
|
+
});
|
|
130
|
+
const absoluteStoryPath = path.resolve(process.cwd(), "docs/stories/001.0-DEV-PLUGIN-SETUP.story.md");
|
|
131
|
+
allowAbsolutePathsTester.run("valid-story-reference", valid_story_reference_1.default, {
|
|
132
|
+
valid: [
|
|
133
|
+
{
|
|
134
|
+
name: "[REQ-CONFIGURABLE-PATHS] allowAbsolutePaths accepts existing absolute .story.md inside project",
|
|
135
|
+
code: `// @story ${absoluteStoryPath}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
|
|
136
|
+
options: [
|
|
137
|
+
{
|
|
138
|
+
allowAbsolutePaths: true,
|
|
139
|
+
storyDirectories: ["docs/stories"],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
invalid: [
|
|
145
|
+
{
|
|
146
|
+
name: "[REQ-CONFIGURABLE-PATHS] disallows absolute paths when allowAbsolutePaths is false",
|
|
147
|
+
code: `// @story ${absoluteStoryPath}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
|
|
148
|
+
options: [
|
|
149
|
+
{
|
|
150
|
+
allowAbsolutePaths: false,
|
|
151
|
+
storyDirectories: ["docs/stories"],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
errors: [
|
|
155
|
+
{
|
|
156
|
+
messageId: "invalidPath",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
163
|
+
// @req REQ-CONFIGURABLE-PATHS - Verify requireStoryExtension behavior
|
|
164
|
+
const relaxedExtensionTester = new eslint_1.RuleTester({
|
|
165
|
+
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
166
|
+
});
|
|
167
|
+
relaxedExtensionTester.run("valid-story-reference", valid_story_reference_1.default, {
|
|
168
|
+
valid: [
|
|
169
|
+
{
|
|
170
|
+
name: "[REQ-CONFIGURABLE-PATHS] accepts .story.md story path when requireStoryExtension is false (still valid and existing)",
|
|
171
|
+
code: `// @story docs/stories/001.0-DEV-PLUGIN-SETUP.story.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
|
|
172
|
+
options: [
|
|
173
|
+
{
|
|
174
|
+
storyDirectories: ["docs/stories"],
|
|
175
|
+
requireStoryExtension: false,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
invalid: [],
|
|
181
|
+
});
|
|
182
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
183
|
+
// @req REQ-PROJECT-BOUNDARY - Verify project boundary handling
|
|
184
|
+
const projectBoundaryTester = new eslint_1.RuleTester({
|
|
185
|
+
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
186
|
+
});
|
|
187
|
+
projectBoundaryTester.run("valid-story-reference", valid_story_reference_1.default, {
|
|
188
|
+
valid: [],
|
|
189
|
+
invalid: [
|
|
190
|
+
{
|
|
191
|
+
name: "[REQ-PROJECT-BOUNDARY] story reference outside project root is rejected when discovered via absolute path",
|
|
192
|
+
code: `// @story ${path.resolve(path.sep, "outside-project", "001.0-DEV-PLUGIN-SETUP.story.md")}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
|
|
193
|
+
options: [
|
|
194
|
+
{
|
|
195
|
+
allowAbsolutePaths: true,
|
|
196
|
+
storyDirectories: [path.resolve(path.sep, "outside-project")],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
errors: [
|
|
200
|
+
{
|
|
201
|
+
messageId: "invalidPath",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
describe("Valid Story Reference Rule Configuration and Boundaries (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
208
|
+
const fs = require("fs");
|
|
209
|
+
const pathModule = require("path");
|
|
210
|
+
let tempDirs = [];
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
for (const dir of tempDirs) {
|
|
213
|
+
try {
|
|
214
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// ignore cleanup errors
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
tempDirs = [];
|
|
221
|
+
(0, storyReferenceUtils_1.__resetStoryExistenceCacheForTests)();
|
|
222
|
+
jest.restoreAllMocks();
|
|
223
|
+
});
|
|
224
|
+
it("[REQ-CONFIGURABLE-PATHS] uses storyDirectories when resolving relative paths (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
225
|
+
const storyPath = pathModule.join(process.cwd(), "docs/stories/001.0-DEV-PLUGIN-SETUP.story.md");
|
|
226
|
+
jest.spyOn(fs, "existsSync").mockImplementation((...args) => {
|
|
227
|
+
const p = args[0];
|
|
228
|
+
return p === storyPath;
|
|
229
|
+
});
|
|
230
|
+
jest.spyOn(fs, "statSync").mockImplementation((...args) => {
|
|
231
|
+
const p = args[0];
|
|
232
|
+
if (p === storyPath) {
|
|
233
|
+
return {
|
|
234
|
+
isFile: () => true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
|
238
|
+
});
|
|
239
|
+
const diagnostics = runRuleOnCode(`// @story 001.0-DEV-PLUGIN-SETUP.story.md`, [{ storyDirectories: ["docs/stories"] }]);
|
|
240
|
+
// When storyDirectories is configured, the underlying resolution should
|
|
241
|
+
// treat the path as valid; absence of errors is asserted via RuleTester
|
|
242
|
+
// above. Here we just ensure no crash path via storyExists cache reset.
|
|
243
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
it("[REQ-CONFIGURABLE-PATHS] allowAbsolutePaths permits absolute paths inside project when enabled (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
246
|
+
const absPath = pathModule.resolve(process.cwd(), "docs/stories/001.0-DEV-PLUGIN-SETUP.story.md");
|
|
247
|
+
const diagnostics = runRuleOnCode(`// @story ${absPath}`, [
|
|
248
|
+
{
|
|
249
|
+
allowAbsolutePaths: true,
|
|
250
|
+
storyDirectories: ["docs/stories"],
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
// Detailed behavior is verified by RuleTester above; this Jest test
|
|
254
|
+
// ensures helper path construction does not throw and diagnostics are collected.
|
|
255
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
it("[REQ-PROJECT-BOUNDARY] storyDirectories cannot escape project even when normalize resolves outside cwd (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
258
|
+
const ruleModule = require("../../src/rules/valid-story-reference");
|
|
259
|
+
const originalCreate = ruleModule.default.create || ruleModule.create;
|
|
260
|
+
// Spy on create to intercept normalizeStoryPath behavior indirectly if needed
|
|
261
|
+
expect(typeof originalCreate).toBe("function");
|
|
262
|
+
const diagnostics = runRuleOnCode(`// @story ../outside-boundary.story.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`);
|
|
263
|
+
// Behavior of reporting invalidPath for outside project is ensured
|
|
264
|
+
// in RuleTester projectBoundaryTester above; here ensure diagnostics collected.
|
|
265
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
/**
|
|
268
|
+
* @req REQ-PROJECT-BOUNDARY - Verify misconfigured storyDirectories pointing outside
|
|
269
|
+
* the project cannot cause external files to be treated as valid, and invalidPath is reported.
|
|
270
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
271
|
+
*/
|
|
272
|
+
it("[REQ-PROJECT-BOUNDARY] misconfigured storyDirectories outside project cannot validate external files", () => {
|
|
273
|
+
const fs = require("fs");
|
|
274
|
+
const pathModule = require("path");
|
|
275
|
+
const outsideDir = pathModule.resolve(pathModule.sep, "tmp", "outside");
|
|
276
|
+
const outsideFile = pathModule.join(outsideDir, "external-story.story.md");
|
|
277
|
+
jest.spyOn(fs, "existsSync").mockImplementation((...args) => {
|
|
278
|
+
const p = args[0];
|
|
279
|
+
return p === outsideFile;
|
|
280
|
+
});
|
|
281
|
+
jest.spyOn(fs, "statSync").mockImplementation((...args) => {
|
|
282
|
+
const p = args[0];
|
|
283
|
+
if (p === outsideFile) {
|
|
284
|
+
return {
|
|
285
|
+
isFile: () => true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const err = new Error("ENOENT");
|
|
289
|
+
err.code = "ENOENT";
|
|
290
|
+
throw err;
|
|
291
|
+
});
|
|
292
|
+
const diagnostics = runRuleOnCode(`// @story ${outsideFile}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`, [
|
|
293
|
+
{
|
|
294
|
+
allowAbsolutePaths: true,
|
|
295
|
+
storyDirectories: [outsideDir],
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
299
|
+
const invalidPathDiagnostics = diagnostics.filter((d) => d.messageId === "invalidPath");
|
|
300
|
+
expect(invalidPathDiagnostics.length).toBeGreaterThan(0);
|
|
301
|
+
});
|
|
302
|
+
/**
|
|
303
|
+
* @req REQ-CONFIGURABLE-PATHS - Verify requireStoryExtension: false allows .md story
|
|
304
|
+
* files that do not end with .story.md when they exist in storyDirectories.
|
|
305
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
306
|
+
*/
|
|
307
|
+
it("[REQ-CONFIGURABLE-PATHS] requireStoryExtension=false accepts existing .md story file", () => {
|
|
308
|
+
const fs = require("fs");
|
|
309
|
+
const pathModule = require("path");
|
|
310
|
+
const storyPath = pathModule.join(process.cwd(), "docs/stories/developer-story.map.md");
|
|
311
|
+
jest.spyOn(fs, "existsSync").mockImplementation((...args) => {
|
|
312
|
+
const p = args[0];
|
|
313
|
+
return p === storyPath;
|
|
314
|
+
});
|
|
315
|
+
jest.spyOn(fs, "statSync").mockImplementation((...args) => {
|
|
316
|
+
const p = args[0];
|
|
317
|
+
if (p === storyPath) {
|
|
318
|
+
return {
|
|
319
|
+
isFile: () => true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const err = new Error("ENOENT");
|
|
323
|
+
err.code = "ENOENT";
|
|
324
|
+
throw err;
|
|
325
|
+
});
|
|
326
|
+
const diagnostics = runRuleOnCode(`// @story docs/stories/developer-story.map.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`, [
|
|
327
|
+
{
|
|
328
|
+
storyDirectories: ["docs/stories"],
|
|
329
|
+
requireStoryExtension: false,
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
333
|
+
const invalidExtensionDiagnostics = diagnostics.filter((d) => d.messageId === "invalidExtension");
|
|
334
|
+
expect(invalidExtensionDiagnostics.length).toBe(0);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
76
337
|
/**
|
|
77
338
|
* Helper to run the valid-story-reference rule against a single source string
|
|
78
339
|
* and collect reported diagnostics.
|
|
@@ -80,7 +341,7 @@ describe("Valid Story Reference Rule (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
|
80
341
|
* @req REQ-ERROR-HANDLING - Used to verify fileAccessError reporting behavior
|
|
81
342
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
82
343
|
*/
|
|
83
|
-
function runRuleOnCode(code) {
|
|
344
|
+
function runRuleOnCode(code, options = []) {
|
|
84
345
|
const messages = [];
|
|
85
346
|
const context = {
|
|
86
347
|
report: (descriptor) => {
|
|
@@ -95,7 +356,7 @@ function runRuleOnCode(code) {
|
|
|
95
356
|
},
|
|
96
357
|
],
|
|
97
358
|
}),
|
|
98
|
-
options
|
|
359
|
+
options,
|
|
99
360
|
parserOptions: { ecmaVersion: 2020 },
|
|
100
361
|
};
|
|
101
362
|
const listeners = valid_story_reference_1.default.create(context);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|