eslint-plugin-traceability 1.4.9 → 1.4.10

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.
@@ -35,16 +35,63 @@ function validateStoryPath(opts) {
35
35
  requireExt,
36
36
  });
37
37
  }
38
+ /**
39
+ * Report any problems related to the existence or accessibility of the
40
+ * referenced story file. Filesystem and I/O errors are surfaced with a
41
+ * dedicated diagnostic that differentiates them from missing files.
42
+ *
43
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
44
+ * @req REQ-FILE-EXISTENCE - Ensure referenced files exist
45
+ * @req REQ-ERROR-HANDLING - Differentiate missing files from filesystem errors
46
+ */
47
+ function reportExistenceProblems(opts) {
48
+ const { storyPath, commentNode, context, cwd, storyDirs } = opts;
49
+ const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
50
+ const existenceResult = result.existence;
51
+ if (!existenceResult || existenceResult.status === "exists") {
52
+ return;
53
+ }
54
+ if (existenceResult.status === "missing") {
55
+ context.report({
56
+ node: commentNode,
57
+ messageId: "fileMissing",
58
+ data: { path: storyPath },
59
+ });
60
+ return;
61
+ }
62
+ if (existenceResult.status === "fs-error") {
63
+ const rawError = existenceResult.error;
64
+ let errorMessage;
65
+ if (rawError == null) {
66
+ errorMessage = "Unknown filesystem error";
67
+ }
68
+ else if (rawError instanceof Error) {
69
+ errorMessage = rawError.message;
70
+ }
71
+ else {
72
+ errorMessage = String(rawError);
73
+ }
74
+ context.report({
75
+ node: commentNode,
76
+ messageId: "fileAccessError",
77
+ data: {
78
+ path: storyPath,
79
+ error: errorMessage,
80
+ },
81
+ });
82
+ }
83
+ }
38
84
  /**
39
85
  * Process and validate the story path for security, extension, and existence.
40
86
  * Filesystem and I/O errors are handled inside the underlying utilities
41
- * (e.g. storyExists) and surfaced as missing-file diagnostics where appropriate.
87
+ * (e.g. storyExists) and surfaced as missing-file or filesystem-error
88
+ * diagnostics where appropriate.
42
89
  *
43
90
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
44
91
  * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
45
92
  * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
46
93
  * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
47
- * @req REQ-ERROR-HANDLING - Delegate filesystem and I/O error handling to utilities
94
+ * @req REQ-ERROR-HANDLING - Delegate filesystem and I/O error handling to utilities and differentiate error types
48
95
  */
49
96
  function processStoryPath(opts) {
50
97
  const { storyPath, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
@@ -80,16 +127,22 @@ function processStoryPath(opts) {
80
127
  });
81
128
  return;
82
129
  }
83
- // Existence check (filesystem and I/O errors are swallowed by utilities
84
- // and treated as non-existent files)
85
- const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
86
- if (!result.exists) {
87
- context.report({
88
- node: commentNode,
89
- messageId: "fileMissing",
90
- data: { path: storyPath },
91
- });
92
- }
130
+ /**
131
+ * Existence check:
132
+ * - Distinguish between missing files and filesystem errors.
133
+ * - Filesystem and I/O errors are surfaced with a dedicated diagnostic.
134
+ *
135
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
136
+ * @req REQ-FILE-EXISTENCE - Ensure referenced files exist
137
+ * @req REQ-ERROR-HANDLING - Differentiate missing files from filesystem errors
138
+ */
139
+ reportExistenceProblems({
140
+ storyPath,
141
+ commentNode,
142
+ context,
143
+ cwd,
144
+ storyDirs,
145
+ });
93
146
  }
94
147
  /**
95
148
  * Handle a single comment node by processing its lines.
@@ -130,6 +183,11 @@ exports.default = {
130
183
  fileMissing: "Story file '{{path}}' not found",
131
184
  invalidExtension: "Invalid story file extension for '{{path}}', expected '.story.md'",
132
185
  invalidPath: "Invalid story path '{{path}}'",
186
+ /**
187
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
188
+ * @req REQ-ERROR-HANDLING - Provide clear diagnostics for filesystem errors
189
+ */
190
+ fileAccessError: "Could not validate story file '{{path}}' due to a filesystem error: {{error}}. Please check file existence and permissions.",
133
191
  },
134
192
  schema: [
135
193
  {
@@ -153,7 +211,7 @@ exports.default = {
153
211
  /**
154
212
  * Program-level handler: iterate comments and validate @story annotations.
155
213
  * Filesystem and I/O errors are handled by underlying utilities and
156
- * surfaced as missing-file diagnostics where appropriate.
214
+ * surfaced as missing-file or filesystem-error diagnostics where appropriate.
157
215
  *
158
216
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
159
217
  * @req REQ-ANNOTATION-VALIDATION - Discover and dispatch @story annotations for validation
@@ -1,22 +1,87 @@
1
+ /**
2
+ * Describes the possible existence states for a checked path.
3
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
4
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
5
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
6
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
7
+ */
8
+ export type StoryExistenceStatus = "exists" | "missing" | "fs-error";
9
+ /**
10
+ * Result of checking a single candidate path.
11
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
12
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
13
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
14
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
15
+ */
16
+ export interface StoryPathCheckResult {
17
+ path: string;
18
+ status: StoryExistenceStatus;
19
+ error?: unknown;
20
+ }
21
+ /**
22
+ * Aggregated existence result across multiple candidate paths.
23
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
24
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
25
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
26
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
27
+ */
28
+ export interface StoryExistenceResult {
29
+ candidates: string[];
30
+ status: StoryExistenceStatus;
31
+ matchedPath?: string;
32
+ error?: unknown;
33
+ }
1
34
  /**
2
35
  * Build candidate file paths for a given story path.
3
36
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
4
37
  * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
5
38
  */
6
39
  export declare function buildStoryCandidates(storyPath: string, cwd: string, storyDirs: string[]): string[];
40
+ /**
41
+ * Aggregate existence status across multiple candidate paths.
42
+ * Returns the first successful match (`exists`), or, if none exist,
43
+ * the first filesystem error encountered. If there are only missing
44
+ * candidates, returns a missing status.
45
+ *
46
+ * This function never throws and is the preferred richer API for callers
47
+ * that need more than a boolean.
48
+ *
49
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
50
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
51
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
52
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
53
+ */
54
+ export declare function getStoryExistence(candidates: string[]): StoryExistenceResult;
55
+ /**
56
+ * Check if any of the provided file paths exist.
57
+ * Handles filesystem errors (e.g., EACCES) gracefully by treating them as non-existent
58
+ * and never throwing.
59
+ *
60
+ * Internally delegates to the richer status-based helper while preserving the
61
+ * original boolean-only API for backwards compatibility.
62
+ *
63
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
64
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
65
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
66
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
67
+ */
7
68
  export declare function storyExists(paths: string[]): boolean;
8
69
  /**
9
70
  * Normalize a story path to candidate absolute paths and check existence.
10
- * Filesystem errors are handled via `storyExists`, which suppresses exceptions
11
- * and treats such cases as non-existent.
71
+ * Filesystem errors are handled via the status-aware helper, which suppresses
72
+ * exceptions and treats such cases as non-existent for the boolean flag while
73
+ * still surfacing error details in the status field.
74
+ *
12
75
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
13
76
  * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
14
77
  * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
15
78
  * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
79
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
16
80
  */
17
81
  export declare function normalizeStoryPath(storyPath: string, cwd: string, storyDirs: string[]): {
18
82
  candidates: string[];
19
83
  exists: boolean;
84
+ existence: StoryExistenceResult;
20
85
  };
21
86
  /**
22
87
  * Check if the provided path is absolute.
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildStoryCandidates = buildStoryCandidates;
7
+ exports.getStoryExistence = getStoryExistence;
7
8
  exports.storyExists = storyExists;
8
9
  exports.normalizeStoryPath = normalizeStoryPath;
9
10
  exports.isAbsolutePath = isAbsolutePath;
@@ -40,45 +41,126 @@ function buildStoryCandidates(storyPath, cwd, storyDirs) {
40
41
  return candidates;
41
42
  }
42
43
  /**
43
- * Check if any of the provided file paths exist.
44
- * Handles filesystem errors (e.g., EACCES) gracefully by treating them as non-existent
45
- * and never throwing.
44
+ * Cache of filesystem existence checks keyed by absolute path.
46
45
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
47
46
  * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
48
47
  * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
48
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
49
49
  */
50
- const fileExistCache = new Map();
51
- function storyExists(paths) {
52
- for (const candidate of paths) {
53
- let ok = fileExistCache.get(candidate);
54
- if (ok === undefined) {
55
- try {
56
- ok = fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile();
50
+ const fileExistStatusCache = new Map();
51
+ /**
52
+ * Check a single candidate path, with caching and robust error handling.
53
+ * All filesystem interactions are wrapped in try/catch and never throw.
54
+ *
55
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
56
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
57
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
58
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
59
+ */
60
+ function checkSingleCandidate(candidate) {
61
+ const cached = fileExistStatusCache.get(candidate);
62
+ if (cached) {
63
+ return cached;
64
+ }
65
+ let result;
66
+ try {
67
+ const exists = fs_1.default.existsSync(candidate);
68
+ if (!exists) {
69
+ result = { path: candidate, status: "missing" };
70
+ }
71
+ else {
72
+ const stat = fs_1.default.statSync(candidate);
73
+ if (stat.isFile()) {
74
+ result = { path: candidate, status: "exists" };
57
75
  }
58
- catch {
59
- ok = false;
76
+ else {
77
+ // Path exists but is not a file; treat as missing for story purposes.
78
+ result = { path: candidate, status: "missing" };
60
79
  }
61
- fileExistCache.set(candidate, ok);
62
80
  }
63
- if (ok) {
64
- return true;
81
+ }
82
+ catch (error) {
83
+ // Any filesystem error is captured and surfaced as fs-error.
84
+ result = { path: candidate, status: "fs-error", error };
85
+ }
86
+ fileExistStatusCache.set(candidate, result);
87
+ return result;
88
+ }
89
+ /**
90
+ * Aggregate existence status across multiple candidate paths.
91
+ * Returns the first successful match (`exists`), or, if none exist,
92
+ * the first filesystem error encountered. If there are only missing
93
+ * candidates, returns a missing status.
94
+ *
95
+ * This function never throws and is the preferred richer API for callers
96
+ * that need more than a boolean.
97
+ *
98
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
99
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
100
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
101
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
102
+ */
103
+ function getStoryExistence(candidates) {
104
+ let firstFsError;
105
+ for (const candidate of candidates) {
106
+ const res = checkSingleCandidate(candidate);
107
+ if (res.status === "exists") {
108
+ return {
109
+ candidates,
110
+ status: "exists",
111
+ matchedPath: res.path,
112
+ };
113
+ }
114
+ if (res.status === "fs-error" && !firstFsError) {
115
+ firstFsError = res;
65
116
  }
66
117
  }
67
- return false;
118
+ if (firstFsError) {
119
+ return {
120
+ candidates,
121
+ status: "fs-error",
122
+ error: firstFsError.error,
123
+ };
124
+ }
125
+ return {
126
+ candidates,
127
+ status: "missing",
128
+ };
129
+ }
130
+ /**
131
+ * Check if any of the provided file paths exist.
132
+ * Handles filesystem errors (e.g., EACCES) gracefully by treating them as non-existent
133
+ * and never throwing.
134
+ *
135
+ * Internally delegates to the richer status-based helper while preserving the
136
+ * original boolean-only API for backwards compatibility.
137
+ *
138
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
139
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
140
+ * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
141
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
142
+ */
143
+ function storyExists(paths) {
144
+ const result = getStoryExistence(paths);
145
+ return result.status === "exists";
68
146
  }
69
147
  /**
70
148
  * Normalize a story path to candidate absolute paths and check existence.
71
- * Filesystem errors are handled via `storyExists`, which suppresses exceptions
72
- * and treats such cases as non-existent.
149
+ * Filesystem errors are handled via the status-aware helper, which suppresses
150
+ * exceptions and treats such cases as non-existent for the boolean flag while
151
+ * still surfacing error details in the status field.
152
+ *
73
153
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
74
154
  * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
75
155
  * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
76
156
  * @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
157
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Cache filesystem checks to avoid redundant work
77
158
  */
78
159
  function normalizeStoryPath(storyPath, cwd, storyDirs) {
79
160
  const candidates = buildStoryCandidates(storyPath, cwd, storyDirs);
80
- const exists = storyExists(candidates);
81
- return { candidates, exists };
161
+ const existence = getStoryExistence(candidates);
162
+ const exists = existence.status === "exists";
163
+ return { candidates, exists, existence };
82
164
  }
83
165
  /**
84
166
  * Check if the provided path is absolute.
@@ -68,6 +68,37 @@ describe("Valid Story Reference Rule (Story 006.0-DEV-FILE-VALIDATION)", () => {
68
68
  ],
69
69
  });
70
70
  });
71
+ /**
72
+ * Helper to run the valid-story-reference rule against a single source string
73
+ * and collect reported diagnostics.
74
+ *
75
+ * @req REQ-ERROR-HANDLING - Used to verify fileAccessError reporting behavior
76
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
77
+ */
78
+ function runRuleOnCode(code) {
79
+ const messages = [];
80
+ const context = {
81
+ report: (descriptor) => {
82
+ messages.push(descriptor);
83
+ },
84
+ getSourceCode: () => ({
85
+ text: code,
86
+ getAllComments: () => [
87
+ {
88
+ type: "Line",
89
+ value: code.replace(/^\/\//, "").trim(),
90
+ },
91
+ ],
92
+ }),
93
+ options: [],
94
+ parserOptions: { ecmaVersion: 2020 },
95
+ };
96
+ const listeners = valid_story_reference_1.default.create(context);
97
+ if (typeof listeners.Program === "function") {
98
+ listeners.Program({});
99
+ }
100
+ return messages;
101
+ }
71
102
  describe("Valid Story Reference Rule Error Handling (Story 006.0-DEV-FILE-VALIDATION)", () => {
72
103
  /**
73
104
  * @req REQ-ERROR-HANDLING - Verify storyExists swallows fs errors and returns false
@@ -92,4 +123,26 @@ describe("Valid Story Reference Rule Error Handling (Story 006.0-DEV-FILE-VALIDA
92
123
  expect(() => (0, storyReferenceUtils_1.storyExists)(["docs/stories/permission-denied.story.md"])).not.toThrow();
93
124
  expect((0, storyReferenceUtils_1.storyExists)(["docs/stories/permission-denied.story.md"])).toBe(false);
94
125
  });
126
+ /**
127
+ * @req REQ-ERROR-HANDLING - Verify rule reports fileAccessError when filesystem operations fail
128
+ * instead of treating it as a missing file.
129
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
130
+ */
131
+ it("[REQ-ERROR-HANDLING] rule reports fileAccessError when fs throws", () => {
132
+ const accessError = new Error("EACCES: permission denied while accessing");
133
+ accessError.code = "EACCES";
134
+ jest.spyOn(fs, "existsSync").mockImplementation(() => {
135
+ throw accessError;
136
+ });
137
+ jest.spyOn(fs, "statSync").mockImplementation(() => {
138
+ throw accessError;
139
+ });
140
+ const diagnostics = runRuleOnCode(`// @story docs/stories/fs-error.story.md`);
141
+ expect(diagnostics.length).toBeGreaterThan(0);
142
+ const fileAccessDiagnostics = diagnostics.filter((d) => d.messageId === "fileAccessError");
143
+ expect(fileAccessDiagnostics.length).toBeGreaterThan(0);
144
+ const errorData = fileAccessDiagnostics[0].data;
145
+ expect(errorData).toBeDefined();
146
+ expect(String(errorData.error)).toMatch(/EACCES/i);
147
+ });
95
148
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.4.9",
3
+ "version": "1.4.10",
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",