eslint-plugin-traceability 1.6.2 → 1.6.3
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.
|
@@ -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;
|
|
@@ -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
|
|
@@ -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.3",
|
|
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",
|