eslint-plugin-traceability 1.4.7 → 1.4.9
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/rules/helpers/require-story-helpers.d.ts +11 -9
- package/lib/src/rules/helpers/require-story-helpers.js +45 -35
- package/lib/src/rules/helpers/require-story-utils.d.ts +34 -0
- package/lib/src/rules/helpers/require-story-utils.js +221 -0
- package/lib/src/rules/valid-story-reference.js +12 -2
- package/lib/src/utils/storyReferenceUtils.d.ts +3 -0
- package/lib/src/utils/storyReferenceUtils.js +13 -1
- package/lib/tests/rules/{require-story-core.branches.test.js → require-story-core-edgecases.test.js} +2 -2
- package/lib/tests/rules/{require-story-io.branches.test.d.ts → require-story-helpers-edgecases.test.d.ts} +1 -1
- package/lib/tests/rules/{require-story-helpers.branches.test.js → require-story-helpers-edgecases.test.js} +2 -2
- package/lib/tests/rules/{require-story-helpers.branches.test.d.ts → require-story-io-behavior.test.d.ts} +1 -1
- package/lib/tests/rules/{require-story-io.branches.test.js → require-story-io-behavior.test.js} +2 -2
- package/lib/tests/rules/{require-story-visitors.branches.test.d.ts → require-story-visitors-edgecases.test.d.ts} +1 -1
- package/lib/tests/rules/{require-story-visitors.branches.test.js → require-story-visitors-edgecases.test.js} +2 -2
- package/lib/tests/rules/valid-story-reference.test.js +26 -0
- package/package.json +3 -2
- /package/lib/tests/rules/{require-story-core.branches.test.d.ts → require-story-core-edgecases.test.d.ts} +0 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Rule } from "eslint";
|
|
7
7
|
import { linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory } from "./require-story-io";
|
|
8
|
+
import { getNodeName } from "./require-story-utils";
|
|
8
9
|
import { DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES } from "./require-story-core";
|
|
9
10
|
/**
|
|
10
11
|
* Path to the story file for annotations
|
|
@@ -67,14 +68,6 @@ declare function leadingCommentsHasStory(node: any): boolean;
|
|
|
67
68
|
* @returns {boolean} true if @story annotation already present
|
|
68
69
|
*/
|
|
69
70
|
declare function hasStoryAnnotation(sourceCode: any, node: any): boolean;
|
|
70
|
-
/**
|
|
71
|
-
* Get the name of the function-like node
|
|
72
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
73
|
-
* @req REQ-ANNOTATION-REQUIRED - Resolve a human-friendly name for a function-like AST node
|
|
74
|
-
* @param {any} node - AST node representing a function-like construct
|
|
75
|
-
* @returns {string} the resolved name or "<unknown>"
|
|
76
|
-
*/
|
|
77
|
-
declare function getNodeName(node: any): string;
|
|
78
71
|
/**
|
|
79
72
|
* Determine AST node where annotation should be inserted
|
|
80
73
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -84,6 +77,15 @@ declare function getNodeName(node: any): string;
|
|
|
84
77
|
* @returns {any} AST node that should receive the annotation
|
|
85
78
|
*/
|
|
86
79
|
declare function resolveTargetNode(sourceCode: any, node: any): any;
|
|
80
|
+
/**
|
|
81
|
+
* Small utility to walk the node and its parents to extract an Identifier or key name.
|
|
82
|
+
* Walks up the parent chain and inspects common properties (id, key, name, Identifier nodes).
|
|
83
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
84
|
+
* @req REQ-ANNOTATION-REQUIRED - Walk node and parents to find Identifier/Key name
|
|
85
|
+
* @param {any} node - AST node to extract a name from
|
|
86
|
+
* @returns {string} extracted name or "(anonymous)" when no name found
|
|
87
|
+
*/
|
|
88
|
+
declare function extractName(node: any): string;
|
|
87
89
|
/**
|
|
88
90
|
* Check if this node is within scope and matches exportPriority
|
|
89
91
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -121,4 +123,4 @@ declare function reportMethod(context: Rule.RuleContext, sourceCode: any, node:
|
|
|
121
123
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
122
124
|
* @req REQ-ANNOTATION-REQUIRED - Explicitly export helper functions and constants used by requiring modules
|
|
123
125
|
*/
|
|
124
|
-
export { STORY_PATH, ANNOTATION, LOOKBACK_LINES, FALLBACK_WINDOW, isExportedNode, jsdocHasStory, commentsBeforeHasStory, leadingCommentsHasStory, hasStoryAnnotation, getNodeName, resolveTargetNode, shouldProcessNode, reportMissing, reportMethod, DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory, };
|
|
126
|
+
export { STORY_PATH, ANNOTATION, LOOKBACK_LINES, FALLBACK_WINDOW, isExportedNode, jsdocHasStory, commentsBeforeHasStory, leadingCommentsHasStory, hasStoryAnnotation, getNodeName, extractName, resolveTargetNode, shouldProcessNode, reportMissing, reportMethod, DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory, };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.FALLBACK_WINDOW = exports.LOOKBACK_LINES = exports.ANNOTATION = exports.STORY_PATH = void 0;
|
|
3
|
+
exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.getNodeName = exports.FALLBACK_WINDOW = exports.LOOKBACK_LINES = exports.ANNOTATION = exports.STORY_PATH = void 0;
|
|
4
4
|
exports.isExportedNode = isExportedNode;
|
|
5
5
|
exports.jsdocHasStory = jsdocHasStory;
|
|
6
6
|
exports.commentsBeforeHasStory = commentsBeforeHasStory;
|
|
7
7
|
exports.leadingCommentsHasStory = leadingCommentsHasStory;
|
|
8
8
|
exports.hasStoryAnnotation = hasStoryAnnotation;
|
|
9
|
-
exports.
|
|
9
|
+
exports.extractName = extractName;
|
|
10
10
|
exports.resolveTargetNode = resolveTargetNode;
|
|
11
11
|
exports.shouldProcessNode = shouldProcessNode;
|
|
12
12
|
exports.reportMissing = reportMissing;
|
|
@@ -15,6 +15,8 @@ const require_story_io_1 = require("./require-story-io");
|
|
|
15
15
|
Object.defineProperty(exports, "linesBeforeHasStory", { enumerable: true, get: function () { return require_story_io_1.linesBeforeHasStory; } });
|
|
16
16
|
Object.defineProperty(exports, "parentChainHasStory", { enumerable: true, get: function () { return require_story_io_1.parentChainHasStory; } });
|
|
17
17
|
Object.defineProperty(exports, "fallbackTextBeforeHasStory", { enumerable: true, get: function () { return require_story_io_1.fallbackTextBeforeHasStory; } });
|
|
18
|
+
const require_story_utils_1 = require("./require-story-utils");
|
|
19
|
+
Object.defineProperty(exports, "getNodeName", { enumerable: true, get: function () { return require_story_utils_1.getNodeName; } });
|
|
18
20
|
const require_story_core_1 = require("./require-story-core");
|
|
19
21
|
Object.defineProperty(exports, "DEFAULT_SCOPE", { enumerable: true, get: function () { return require_story_core_1.DEFAULT_SCOPE; } });
|
|
20
22
|
Object.defineProperty(exports, "EXPORT_PRIORITY_VALUES", { enumerable: true, get: function () { return require_story_core_1.EXPORT_PRIORITY_VALUES; } });
|
|
@@ -139,37 +141,6 @@ function hasStoryAnnotation(sourceCode, node) {
|
|
|
139
141
|
}
|
|
140
142
|
return false;
|
|
141
143
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Get the name of the function-like node
|
|
144
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
145
|
-
* @req REQ-ANNOTATION-REQUIRED - Resolve a human-friendly name for a function-like AST node
|
|
146
|
-
* @param {any} node - AST node representing a function-like construct
|
|
147
|
-
* @returns {string} the resolved name or "<unknown>"
|
|
148
|
-
*/
|
|
149
|
-
function getNodeName(node) {
|
|
150
|
-
let current = node;
|
|
151
|
-
while (current) {
|
|
152
|
-
if (current.type === "VariableDeclarator" &&
|
|
153
|
-
current.id &&
|
|
154
|
-
typeof current.id.name === "string") {
|
|
155
|
-
return current.id.name;
|
|
156
|
-
}
|
|
157
|
-
if ((current.type === "FunctionDeclaration" ||
|
|
158
|
-
current.type === "TSDeclareFunction") &&
|
|
159
|
-
current.id &&
|
|
160
|
-
typeof current.id.name === "string") {
|
|
161
|
-
return current.id.name;
|
|
162
|
-
}
|
|
163
|
-
if ((current.type === "MethodDefinition" ||
|
|
164
|
-
current.type === "TSMethodSignature") &&
|
|
165
|
-
current.key &&
|
|
166
|
-
typeof current.key.name === "string") {
|
|
167
|
-
return current.key.name;
|
|
168
|
-
}
|
|
169
|
-
current = current.parent;
|
|
170
|
-
}
|
|
171
|
-
return "<unknown>";
|
|
172
|
-
}
|
|
173
144
|
/**
|
|
174
145
|
* Determine AST node where annotation should be inserted
|
|
175
146
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -202,6 +173,45 @@ function resolveTargetNode(sourceCode, node) {
|
|
|
202
173
|
}
|
|
203
174
|
return node;
|
|
204
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Small utility to walk the node and its parents to extract an Identifier or key name.
|
|
178
|
+
* Walks up the parent chain and inspects common properties (id, key, name, Identifier nodes).
|
|
179
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
180
|
+
* @req REQ-ANNOTATION-REQUIRED - Walk node and parents to find Identifier/Key name
|
|
181
|
+
* @param {any} node - AST node to extract a name from
|
|
182
|
+
* @returns {string} extracted name or "(anonymous)" when no name found
|
|
183
|
+
*/
|
|
184
|
+
function extractName(node) {
|
|
185
|
+
let n = node;
|
|
186
|
+
while (n) {
|
|
187
|
+
// Direct Identifier node
|
|
188
|
+
if (n.type === "Identifier" && typeof n.name === "string") {
|
|
189
|
+
return n.name;
|
|
190
|
+
}
|
|
191
|
+
// id property (FunctionDeclaration, etc.)
|
|
192
|
+
if (n.id && n.id.type === "Identifier" && typeof n.id.name === "string") {
|
|
193
|
+
return n.id.name;
|
|
194
|
+
}
|
|
195
|
+
// key property (Property, MethodDefinition, etc.)
|
|
196
|
+
if (n.key &&
|
|
197
|
+
n.key.type === "Identifier" &&
|
|
198
|
+
typeof n.key.name === "string") {
|
|
199
|
+
return n.key.name;
|
|
200
|
+
}
|
|
201
|
+
// name property (some nodes may have a 'name' string directly)
|
|
202
|
+
if (typeof n.name === "string" && n.name.length > 0) {
|
|
203
|
+
return n.name;
|
|
204
|
+
}
|
|
205
|
+
// computed keys may have an Identifier inside
|
|
206
|
+
if (n.key &&
|
|
207
|
+
n.key.type === "Literal" &&
|
|
208
|
+
typeof n.key.value === "string") {
|
|
209
|
+
return n.key.value;
|
|
210
|
+
}
|
|
211
|
+
n = n.parent;
|
|
212
|
+
}
|
|
213
|
+
return "(anonymous)";
|
|
214
|
+
}
|
|
205
215
|
/**
|
|
206
216
|
* Check if this node is within scope and matches exportPriority
|
|
207
217
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -236,7 +246,7 @@ function shouldProcessNode(node, scope, exportPriority = "all") {
|
|
|
236
246
|
*/
|
|
237
247
|
function reportMissing(context, sourceCode, node, passedTarget) {
|
|
238
248
|
try {
|
|
239
|
-
const functionName =
|
|
249
|
+
const functionName = extractName(node && (node.id || node.key) ? node.id || node.key : node);
|
|
240
250
|
if (hasStoryAnnotation(sourceCode, node)) {
|
|
241
251
|
return;
|
|
242
252
|
}
|
|
@@ -274,7 +284,7 @@ function reportMethod(context, sourceCode, node, passedTarget) {
|
|
|
274
284
|
return;
|
|
275
285
|
}
|
|
276
286
|
const resolvedTarget = passedTarget ?? resolveTargetNode(sourceCode, node);
|
|
277
|
-
const name =
|
|
287
|
+
const name = extractName(node);
|
|
278
288
|
context.report({
|
|
279
289
|
node,
|
|
280
290
|
messageId: "missingStory",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/rules/helpers/require-story-utils.ts
|
|
3
|
+
*
|
|
4
|
+
* Utility: getNodeName
|
|
5
|
+
*
|
|
6
|
+
* Traceability:
|
|
7
|
+
* - File: src/rules/helpers/require-story-utils.ts
|
|
8
|
+
* - Purpose: Helper used by rules to obtain a readable "name" from various AST node shapes.
|
|
9
|
+
* - Created to centralize logic for extracting identifier/property/literal names from ESTree / TSESTree nodes.
|
|
10
|
+
*
|
|
11
|
+
* Docs/Annotations:
|
|
12
|
+
* - Story: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
13
|
+
* - Requirement annotation: REQ-ANNOTATION-REQUIRED
|
|
14
|
+
*
|
|
15
|
+
* Behavior:
|
|
16
|
+
* - Accepts common AST node shapes (Identifier, Literal, Property, MemberExpression, TemplateLiteral, JSXIdentifier, etc.)
|
|
17
|
+
* - Returns a string name when it can be reasonably determined, otherwise returns null.
|
|
18
|
+
*
|
|
19
|
+
* Notes:
|
|
20
|
+
* - This helper is intentionally defensive and conservative: when the node is computed or contains expressions
|
|
21
|
+
* that cannot be resolved statically, it returns null.
|
|
22
|
+
* - The function uses weak typing for the node parameter to maximize compatibility with different AST types
|
|
23
|
+
* (ESTree, TSESTree, JSX variations).
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Get a readable name for a given AST node.
|
|
27
|
+
*
|
|
28
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
29
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
30
|
+
*
|
|
31
|
+
* @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
|
|
32
|
+
* @returns The resolved name string, or null if a stable name cannot be determined.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getNodeName(node: any): string | null;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/rules/helpers/require-story-utils.ts
|
|
4
|
+
*
|
|
5
|
+
* Utility: getNodeName
|
|
6
|
+
*
|
|
7
|
+
* Traceability:
|
|
8
|
+
* - File: src/rules/helpers/require-story-utils.ts
|
|
9
|
+
* - Purpose: Helper used by rules to obtain a readable "name" from various AST node shapes.
|
|
10
|
+
* - Created to centralize logic for extracting identifier/property/literal names from ESTree / TSESTree nodes.
|
|
11
|
+
*
|
|
12
|
+
* Docs/Annotations:
|
|
13
|
+
* - Story: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
14
|
+
* - Requirement annotation: REQ-ANNOTATION-REQUIRED
|
|
15
|
+
*
|
|
16
|
+
* Behavior:
|
|
17
|
+
* - Accepts common AST node shapes (Identifier, Literal, Property, MemberExpression, TemplateLiteral, JSXIdentifier, etc.)
|
|
18
|
+
* - Returns a string name when it can be reasonably determined, otherwise returns null.
|
|
19
|
+
*
|
|
20
|
+
* Notes:
|
|
21
|
+
* - This helper is intentionally defensive and conservative: when the node is computed or contains expressions
|
|
22
|
+
* that cannot be resolved statically, it returns null.
|
|
23
|
+
* - The function uses weak typing for the node parameter to maximize compatibility with different AST types
|
|
24
|
+
* (ESTree, TSESTree, JSX variations).
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.getNodeName = getNodeName;
|
|
28
|
+
/**
|
|
29
|
+
* Check for identifier-like nodes and return their name when available.
|
|
30
|
+
*
|
|
31
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
32
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
33
|
+
*
|
|
34
|
+
* @param node - AST node to inspect
|
|
35
|
+
* @returns the identifier name or null
|
|
36
|
+
*/
|
|
37
|
+
function isIdentifierLike(node) {
|
|
38
|
+
if (!node)
|
|
39
|
+
return null;
|
|
40
|
+
const type = node.type;
|
|
41
|
+
if (type === "Identifier" || type === "JSXIdentifier") {
|
|
42
|
+
return typeof node.name === "string" ? node.name : null;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a Literal node to a string when it represents a stable primitive (string/number/boolean).
|
|
48
|
+
*
|
|
49
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
50
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
51
|
+
*
|
|
52
|
+
* @param node - AST node expected to be a Literal
|
|
53
|
+
* @returns the literal as string or null if not stable/resolvable
|
|
54
|
+
*/
|
|
55
|
+
function literalToString(node) {
|
|
56
|
+
if (!node || node.type !== "Literal")
|
|
57
|
+
return null;
|
|
58
|
+
const val = node.value;
|
|
59
|
+
if (val === null)
|
|
60
|
+
return null;
|
|
61
|
+
const t = typeof val;
|
|
62
|
+
return t === "string" || t === "number" || t === "boolean"
|
|
63
|
+
? String(val)
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Convert a TemplateLiteral node to a string if it contains no expressions.
|
|
68
|
+
*
|
|
69
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
70
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
71
|
+
*
|
|
72
|
+
* @param node - AST node expected to be a TemplateLiteral
|
|
73
|
+
* @returns the cooked/raw concatenated template string or null if it contains expressions
|
|
74
|
+
*/
|
|
75
|
+
function templateLiteralToString(node) {
|
|
76
|
+
if (!node || node.type !== "TemplateLiteral")
|
|
77
|
+
return null;
|
|
78
|
+
const expressions = node.expressions || [];
|
|
79
|
+
if (expressions.length !== 0)
|
|
80
|
+
return null;
|
|
81
|
+
const quasis = node.quasis || [];
|
|
82
|
+
return quasis
|
|
83
|
+
.map((q) => {
|
|
84
|
+
if (!q || !q.value)
|
|
85
|
+
return "";
|
|
86
|
+
if (typeof q.value.cooked === "string")
|
|
87
|
+
return q.value.cooked;
|
|
88
|
+
if (typeof q.value.raw === "string")
|
|
89
|
+
return q.value.raw;
|
|
90
|
+
return "";
|
|
91
|
+
})
|
|
92
|
+
.join("");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a MemberExpression / TSQualifiedName / JSXMemberExpression-like node to a name when non-computed.
|
|
96
|
+
*
|
|
97
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
98
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
99
|
+
*
|
|
100
|
+
* @param node - AST node to inspect
|
|
101
|
+
* @returns resolved member/property name or null
|
|
102
|
+
*/
|
|
103
|
+
function memberExpressionName(node) {
|
|
104
|
+
if (!node)
|
|
105
|
+
return null;
|
|
106
|
+
const type = node.type;
|
|
107
|
+
if (type !== "MemberExpression" &&
|
|
108
|
+
type !== "TSQualifiedName" &&
|
|
109
|
+
type !== "JSXMemberExpression") {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// For TSQualifiedName and JSXMemberExpression, treat like non-computed access.
|
|
113
|
+
if (type === "TSQualifiedName" ||
|
|
114
|
+
type === "JSXMemberExpression" ||
|
|
115
|
+
node.computed === false) {
|
|
116
|
+
return getNodeName(node.property || node.right);
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract the key name from Property/ObjectProperty nodes.
|
|
122
|
+
*
|
|
123
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
124
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
125
|
+
*
|
|
126
|
+
* @param node - AST node expected to be Property/ObjectProperty
|
|
127
|
+
* @returns the resolved key name or null
|
|
128
|
+
*/
|
|
129
|
+
function propertyKeyName(node) {
|
|
130
|
+
if (!node)
|
|
131
|
+
return null;
|
|
132
|
+
if (node.type === "Property" || node.type === "ObjectProperty") {
|
|
133
|
+
return getNodeName(node.key);
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extract direct-level names from common container fields (.id / .key) to reduce branching in getNodeName.
|
|
139
|
+
*
|
|
140
|
+
* Branch-level traceability: prefer direct .id.name when available (common on function/class declarations)
|
|
141
|
+
* Branch-level traceability: prefer direct .key.name early (common on variable declarators, properties)
|
|
142
|
+
*
|
|
143
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
144
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
145
|
+
*
|
|
146
|
+
* @param node - AST node to inspect for direct .id/.key name
|
|
147
|
+
* @returns the resolved direct name or null
|
|
148
|
+
*/
|
|
149
|
+
function directName(node) {
|
|
150
|
+
if (!node)
|
|
151
|
+
return null;
|
|
152
|
+
// Prefer direct .id.name when available
|
|
153
|
+
if (node.id && typeof node.id.name === "string") {
|
|
154
|
+
return node.id.name;
|
|
155
|
+
}
|
|
156
|
+
if (node.id) {
|
|
157
|
+
const idName = getNodeName(node.id);
|
|
158
|
+
if (idName !== null)
|
|
159
|
+
return idName;
|
|
160
|
+
}
|
|
161
|
+
// Prefer direct .key.name early
|
|
162
|
+
if (node.key && typeof node.key.name === "string") {
|
|
163
|
+
return node.key.name;
|
|
164
|
+
}
|
|
165
|
+
if (node.key) {
|
|
166
|
+
const keyName = getNodeName(node.key);
|
|
167
|
+
if (keyName !== null)
|
|
168
|
+
return keyName;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get a readable name for a given AST node.
|
|
174
|
+
*
|
|
175
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
176
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
177
|
+
*
|
|
178
|
+
* @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
|
|
179
|
+
* @returns The resolved name string, or null if a stable name cannot be determined.
|
|
180
|
+
*/
|
|
181
|
+
function getNodeName(node) {
|
|
182
|
+
if (!node)
|
|
183
|
+
return null;
|
|
184
|
+
// Delegate direct-level id/key resolution to helper to reduce cyclomatic complexity
|
|
185
|
+
const direct = directName(node);
|
|
186
|
+
if (direct !== null)
|
|
187
|
+
return direct;
|
|
188
|
+
// Identifier-like nodes
|
|
189
|
+
const idName = isIdentifierLike(node);
|
|
190
|
+
if (idName !== null)
|
|
191
|
+
return idName;
|
|
192
|
+
// Literal nodes
|
|
193
|
+
const lit = literalToString(node);
|
|
194
|
+
if (lit !== null)
|
|
195
|
+
return lit;
|
|
196
|
+
// TemplateLiteral nodes
|
|
197
|
+
const tpl = templateLiteralToString(node);
|
|
198
|
+
if (tpl !== null)
|
|
199
|
+
return tpl;
|
|
200
|
+
// Property-like nodes
|
|
201
|
+
const prop = propertyKeyName(node);
|
|
202
|
+
if (prop !== null)
|
|
203
|
+
return prop;
|
|
204
|
+
// Member/qualified/member-expression-like nodes
|
|
205
|
+
const member = memberExpressionName(node);
|
|
206
|
+
if (member !== null)
|
|
207
|
+
return member;
|
|
208
|
+
// TypeScript literal wrapper
|
|
209
|
+
if (node.type === "TSLiteralType" && node.literal) {
|
|
210
|
+
return getNodeName(node.literal);
|
|
211
|
+
}
|
|
212
|
+
// JSX namespaced name
|
|
213
|
+
if (node.type === "JSXNamespacedName") {
|
|
214
|
+
return getNodeName(node.name);
|
|
215
|
+
}
|
|
216
|
+
// Generic fallback: try .key if present on other shapes
|
|
217
|
+
if (node.key) {
|
|
218
|
+
return getNodeName(node.key);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
@@ -37,10 +37,14 @@ function validateStoryPath(opts) {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Process and validate the story path for security, extension, and existence.
|
|
40
|
+
* Filesystem and I/O errors are handled inside the underlying utilities
|
|
41
|
+
* (e.g. storyExists) and surfaced as missing-file diagnostics where appropriate.
|
|
42
|
+
*
|
|
40
43
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
41
44
|
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
42
45
|
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
43
46
|
* @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
|
|
44
48
|
*/
|
|
45
49
|
function processStoryPath(opts) {
|
|
46
50
|
const { storyPath, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
|
|
@@ -76,8 +80,10 @@ function processStoryPath(opts) {
|
|
|
76
80
|
});
|
|
77
81
|
return;
|
|
78
82
|
}
|
|
79
|
-
// Existence check
|
|
80
|
-
|
|
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) {
|
|
81
87
|
context.report({
|
|
82
88
|
node: commentNode,
|
|
83
89
|
messageId: "fileMissing",
|
|
@@ -146,10 +152,14 @@ exports.default = {
|
|
|
146
152
|
return {
|
|
147
153
|
/**
|
|
148
154
|
* Program-level handler: iterate comments and validate @story annotations.
|
|
155
|
+
* Filesystem and I/O errors are handled by underlying utilities and
|
|
156
|
+
* surfaced as missing-file diagnostics where appropriate.
|
|
157
|
+
*
|
|
149
158
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
150
159
|
* @req REQ-ANNOTATION-VALIDATION - Discover and dispatch @story annotations for validation
|
|
151
160
|
* @req REQ-FILE-EXISTENCE - Ensure referenced files exist
|
|
152
161
|
* @req REQ-PATH-RESOLUTION - Resolve using cwd and configured story directories
|
|
162
|
+
* @req REQ-ERROR-HANDLING - Delegate filesystem and I/O error handling to utilities
|
|
153
163
|
*/
|
|
154
164
|
Program() {
|
|
155
165
|
const comments = context.getSourceCode().getAllComments() || [];
|
|
@@ -7,9 +7,12 @@ export declare function buildStoryCandidates(storyPath: string, cwd: string, sto
|
|
|
7
7
|
export declare function storyExists(paths: string[]): boolean;
|
|
8
8
|
/**
|
|
9
9
|
* 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.
|
|
10
12
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
11
13
|
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
12
14
|
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
15
|
+
* @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
|
|
13
16
|
*/
|
|
14
17
|
export declare function normalizeStoryPath(storyPath: string, cwd: string, storyDirs: string[]): {
|
|
15
18
|
candidates: string[];
|
|
@@ -17,6 +17,7 @@ exports.isUnsafeStoryPath = isUnsafeStoryPath;
|
|
|
17
17
|
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
18
18
|
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
19
19
|
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
20
|
+
* @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
|
|
20
21
|
*/
|
|
21
22
|
const fs_1 = __importDefault(require("fs"));
|
|
22
23
|
const path_1 = __importDefault(require("path"));
|
|
@@ -40,15 +41,23 @@ function buildStoryCandidates(storyPath, cwd, storyDirs) {
|
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
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.
|
|
43
46
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
44
47
|
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
48
|
+
* @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
|
|
45
49
|
*/
|
|
46
50
|
const fileExistCache = new Map();
|
|
47
51
|
function storyExists(paths) {
|
|
48
52
|
for (const candidate of paths) {
|
|
49
53
|
let ok = fileExistCache.get(candidate);
|
|
50
54
|
if (ok === undefined) {
|
|
51
|
-
|
|
55
|
+
try {
|
|
56
|
+
ok = fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
ok = false;
|
|
60
|
+
}
|
|
52
61
|
fileExistCache.set(candidate, ok);
|
|
53
62
|
}
|
|
54
63
|
if (ok) {
|
|
@@ -59,9 +68,12 @@ function storyExists(paths) {
|
|
|
59
68
|
}
|
|
60
69
|
/**
|
|
61
70
|
* 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.
|
|
62
73
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
63
74
|
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
64
75
|
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
76
|
+
* @req REQ-ERROR-HANDLING - Handle filesystem errors gracefully without throwing
|
|
65
77
|
*/
|
|
66
78
|
function normalizeStoryPath(storyPath, cwd, storyDirs) {
|
|
67
79
|
const candidates = buildStoryCandidates(storyPath, cwd, storyDirs);
|
package/lib/tests/rules/{require-story-core.branches.test.js → require-story-core-edgecases.test.js}
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Edge-case tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
5
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
6
6
|
* @req REQ-AUTOFIX - Cover additional branch cases in require-story-core (addStoryFixer/reportMissing)
|
|
7
7
|
*/
|
|
8
8
|
const require_story_core_1 = require("../../src/rules/helpers/require-story-core");
|
|
9
9
|
const require_story_helpers_1 = require("../../src/rules/helpers/require-story-helpers");
|
|
10
|
-
describe("Require Story Core (Story 003.0)", () => {
|
|
10
|
+
describe("Require Story Core - edge cases (Story 003.0)", () => {
|
|
11
11
|
test("createAddStoryFix falls back to 0 when target is falsy", () => {
|
|
12
12
|
const fixer = {
|
|
13
13
|
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
3
3
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
|
-
* @req REQ-
|
|
4
|
+
* @req REQ-HELPERS-EDGE-CASES - Edge-case behavior tests for helpers in require-story-helpers.ts
|
|
5
5
|
*/
|
|
6
6
|
export {};
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
4
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
|
-
* @req REQ-
|
|
5
|
+
* @req REQ-HELPERS-EDGE-CASES - Edge-case behavior tests for helpers in require-story-helpers.ts
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
const require_story_helpers_1 = require("../../src/rules/helpers/require-story-helpers");
|
|
9
|
-
describe("Require Story Helpers -
|
|
9
|
+
describe("Require Story Helpers - edge cases (Story 003.0)", () => {
|
|
10
10
|
test("jsdocHasStory returns false when JSDoc exists but value is not a string", () => {
|
|
11
11
|
const fakeSource = { getJSDocComment: () => ({ value: 123 }) };
|
|
12
12
|
const res = (0, require_story_helpers_1.jsdocHasStory)(fakeSource, {});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
3
3
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
|
-
* @req REQ-
|
|
4
|
+
* @req REQ-IO-BEHAVIOR-EDGE-CASES - Edge-case behavior tests for IO helpers in require-story-io.ts
|
|
5
5
|
*/
|
|
6
6
|
export {};
|
package/lib/tests/rules/{require-story-io.branches.test.js → require-story-io-behavior.test.js}
RENAMED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
4
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
|
-
* @req REQ-
|
|
5
|
+
* @req REQ-IO-BEHAVIOR-EDGE-CASES - Edge-case behavior tests for IO helpers in require-story-io.ts
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
const require_story_io_1 = require("../../src/rules/helpers/require-story-io");
|
|
9
|
-
describe("Require Story IO helpers -
|
|
9
|
+
describe("Require Story IO helpers - additional behavior (Story 003.0)", () => {
|
|
10
10
|
test("parentChainHasStory returns false when sourceCode.getCommentsBefore is not a function", () => {
|
|
11
11
|
const fakeSource = {}; // no getCommentsBefore function
|
|
12
12
|
const node = { parent: { parent: null } };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
3
3
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
|
-
* @req REQ-
|
|
4
|
+
* @req REQ-VISITORS-BEHAVIOR - Behavior tests for visitors in require-story-visitors.ts
|
|
5
5
|
*/
|
|
6
6
|
export {};
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
4
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
|
-
* @req REQ-
|
|
5
|
+
* @req REQ-VISITORS-BEHAVIOR - Behavior tests for visitors in require-story-visitors.ts
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
const require_story_visitors_1 = require("../../src/rules/helpers/require-story-visitors");
|
|
9
|
-
describe("Require Story Visitors -
|
|
9
|
+
describe("Require Story Visitors - behavior (Story 003.0)", () => {
|
|
10
10
|
test("build visitors returns handlers for FunctionDeclaration and ArrowFunctionExpression", () => {
|
|
11
11
|
const fakeContext = { getFilename: () => "file.ts" };
|
|
12
12
|
const fakeSource = { getText: () => "" };
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
*/
|
|
11
11
|
const eslint_1 = require("eslint");
|
|
12
12
|
const valid_story_reference_1 = __importDefault(require("../../src/rules/valid-story-reference"));
|
|
13
|
+
const storyReferenceUtils_1 = require("../../src/utils/storyReferenceUtils");
|
|
13
14
|
const ruleTester = new eslint_1.RuleTester({
|
|
14
15
|
languageOptions: { parserOptions: { ecmaVersion: 2020 } },
|
|
15
16
|
});
|
|
@@ -67,3 +68,28 @@ describe("Valid Story Reference Rule (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
|
67
68
|
],
|
|
68
69
|
});
|
|
69
70
|
});
|
|
71
|
+
describe("Valid Story Reference Rule Error Handling (Story 006.0-DEV-FILE-VALIDATION)", () => {
|
|
72
|
+
/**
|
|
73
|
+
* @req REQ-ERROR-HANDLING - Verify storyExists swallows fs errors and returns false
|
|
74
|
+
* instead of throwing when filesystem operations fail.
|
|
75
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
76
|
+
*/
|
|
77
|
+
const fs = require("fs");
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
jest.restoreAllMocks();
|
|
80
|
+
});
|
|
81
|
+
it("[REQ-ERROR-HANDLING] storyExists returns false when fs throws", () => {
|
|
82
|
+
jest.spyOn(fs, "existsSync").mockImplementation(() => {
|
|
83
|
+
const err = new Error("EACCES: permission denied");
|
|
84
|
+
err.code = "EACCES";
|
|
85
|
+
throw err;
|
|
86
|
+
});
|
|
87
|
+
jest.spyOn(fs, "statSync").mockImplementation(() => {
|
|
88
|
+
const err = new Error("EACCES: permission denied");
|
|
89
|
+
err.code = "EACCES";
|
|
90
|
+
throw err;
|
|
91
|
+
});
|
|
92
|
+
expect(() => (0, storyReferenceUtils_1.storyExists)(["docs/stories/permission-denied.story.md"])).not.toThrow();
|
|
93
|
+
expect((0, storyReferenceUtils_1.storyExists)(["docs/stories/permission-denied.story.md"])).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
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",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"lint": "eslint --config eslint.config.js \"src/**/*.{js,ts}\" \"tests/**/*.{js,ts}\" --max-warnings=0",
|
|
23
23
|
"test": "jest --ci --bail",
|
|
24
24
|
"ci-verify": "npm run type-check && npm run lint && npm run format:check && npm run duplication && npm run check:traceability && npm test && npm run audit:ci && npm run safety:deps",
|
|
25
|
-
"ci-verify:
|
|
25
|
+
"ci-verify:full": "npm run check:traceability && npm run safety:deps && npm run audit:ci && npm run build && npm run type-check && npm run lint-plugin-check && npm run lint -- --max-warnings=0 && npm run duplication && npm run test -- --coverage && npm run format:check && npm audit --production --audit-level=high && npm run audit:dev-high",
|
|
26
|
+
"ci-verify:fast": "npm run type-check && npm run check:traceability && npm run duplication && jest --ci --bail --passWithNoTests --testPathPatterns 'tests/(unit|fast)'",
|
|
26
27
|
"format": "prettier --write .",
|
|
27
28
|
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
28
29
|
"duplication": "jscpd src tests --reporters console --threshold 3 --ignore tests/utils/**",
|
|
File without changes
|