eslint-plugin-traceability 1.4.2 → 1.4.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/batch.js +1 -0
- package/lib/src/maintenance/detect.js +4 -0
- package/lib/src/maintenance/report.js +2 -0
- package/lib/src/maintenance/update.js +40 -1
- package/lib/src/maintenance/utils.js +33 -2
- package/lib/src/rules/helpers/require-story-core.d.ts +35 -0
- package/lib/src/rules/helpers/require-story-core.js +139 -0
- package/lib/src/rules/helpers/require-story-helpers.d.ts +124 -0
- package/lib/src/rules/helpers/require-story-helpers.js +293 -0
- package/lib/src/rules/helpers/require-story-io.d.ts +35 -0
- package/lib/src/rules/helpers/require-story-io.js +97 -0
- package/lib/src/rules/helpers/require-story-visitors.d.ts +13 -0
- package/lib/src/rules/helpers/require-story-visitors.js +163 -0
- package/lib/src/rules/require-story-annotation.d.ts +15 -0
- package/lib/src/rules/require-story-annotation.js +27 -238
- package/lib/src/utils/branch-annotation-helpers.js +98 -14
- package/lib/tests/maintenance/detect-isolated.test.js +19 -6
- package/lib/tests/rules/require-story-helpers.test.d.ts +1 -0
- package/lib/tests/rules/require-story-helpers.test.js +173 -0
- package/lib/tests/rules/require-story-io.edgecases.test.d.ts +6 -0
- package/lib/tests/rules/require-story-io.edgecases.test.js +50 -0
- package/package.json +3 -1
|
@@ -1,198 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
"FunctionDeclaration",
|
|
6
|
-
"FunctionExpression",
|
|
7
|
-
"ArrowFunctionExpression",
|
|
8
|
-
"MethodDefinition",
|
|
9
|
-
"TSDeclareFunction",
|
|
10
|
-
"TSMethodSignature",
|
|
11
|
-
];
|
|
12
|
-
const EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
|
|
3
|
+
const require_story_visitors_1 = require("./helpers/require-story-visitors");
|
|
4
|
+
const require_story_helpers_1 = require("./helpers/require-story-helpers");
|
|
13
5
|
/**
|
|
14
|
-
*
|
|
6
|
+
* ESLint rule to require @story annotations on functions/methods.
|
|
15
7
|
*
|
|
16
8
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
17
9
|
* @req REQ-ANNOTATION-REQUIRED
|
|
18
|
-
* @param {any} node - AST node to check for export ancestry
|
|
19
|
-
* @returns {boolean} true if node is within an export declaration
|
|
20
10
|
*/
|
|
21
|
-
function isExportedNode(node) {
|
|
22
|
-
let p = node.parent;
|
|
23
|
-
while (p) {
|
|
24
|
-
if (p.type === "ExportNamedDeclaration" ||
|
|
25
|
-
p.type === "ExportDefaultDeclaration") {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
p = p.parent;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
// Path to the story file for annotations
|
|
33
|
-
const STORY_PATH = "docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md";
|
|
34
|
-
const ANNOTATION = `/** @story ${STORY_PATH} */`;
|
|
35
|
-
/**
|
|
36
|
-
* Check if @story annotation already present in JSDoc or preceding comments
|
|
37
|
-
*
|
|
38
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
39
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
40
|
-
* @param {any} sourceCode - ESLint sourceCode object
|
|
41
|
-
* @param {any} node - AST node to inspect for existing annotations
|
|
42
|
-
* @returns {boolean} true if @story annotation already present
|
|
43
|
-
*/
|
|
44
|
-
function hasStoryAnnotation(sourceCode, node) {
|
|
45
|
-
const jsdoc = sourceCode.getJSDocComment(node);
|
|
46
|
-
if (jsdoc?.value.includes("@story")) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
const comments = sourceCode.getCommentsBefore(node) || [];
|
|
50
|
-
return comments.some((c) => c.value.includes("@story"));
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Get the name of the function-like node
|
|
54
|
-
*
|
|
55
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
56
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
57
|
-
* @param {any} node - AST node representing a function-like construct
|
|
58
|
-
* @returns {string} the resolved name or "<unknown>"
|
|
59
|
-
*/
|
|
60
|
-
function getNodeName(node) {
|
|
61
|
-
let current = node;
|
|
62
|
-
while (current) {
|
|
63
|
-
if (current.type === "VariableDeclarator" &&
|
|
64
|
-
current.id &&
|
|
65
|
-
typeof current.id.name === "string") {
|
|
66
|
-
return current.id.name;
|
|
67
|
-
}
|
|
68
|
-
if ((current.type === "FunctionDeclaration" ||
|
|
69
|
-
current.type === "TSDeclareFunction") &&
|
|
70
|
-
current.id &&
|
|
71
|
-
typeof current.id.name === "string") {
|
|
72
|
-
return current.id.name;
|
|
73
|
-
}
|
|
74
|
-
if ((current.type === "MethodDefinition" ||
|
|
75
|
-
current.type === "TSMethodSignature") &&
|
|
76
|
-
current.key &&
|
|
77
|
-
typeof current.key.name === "string") {
|
|
78
|
-
return current.key.name;
|
|
79
|
-
}
|
|
80
|
-
current = current.parent;
|
|
81
|
-
}
|
|
82
|
-
return "<unknown>";
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Determine AST node where annotation should be inserted
|
|
86
|
-
*
|
|
87
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
88
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
89
|
-
* @param {any} sourceCode - ESLint sourceCode object (unused but kept for parity)
|
|
90
|
-
* @param {any} node - function-like AST node to resolve target for
|
|
91
|
-
* @returns {any} AST node that should receive the annotation
|
|
92
|
-
*/
|
|
93
|
-
function resolveTargetNode(sourceCode, node) {
|
|
94
|
-
if (node.type === "TSMethodSignature") {
|
|
95
|
-
// Interface method signature -> insert on interface
|
|
96
|
-
return node.parent.parent;
|
|
97
|
-
}
|
|
98
|
-
if (node.type === "FunctionExpression" ||
|
|
99
|
-
node.type === "ArrowFunctionExpression") {
|
|
100
|
-
const parent = node.parent;
|
|
101
|
-
if (parent.type === "VariableDeclarator") {
|
|
102
|
-
const varDecl = parent.parent;
|
|
103
|
-
if (varDecl.parent && varDecl.parent.type === "ExportNamedDeclaration") {
|
|
104
|
-
return varDecl.parent;
|
|
105
|
-
}
|
|
106
|
-
return varDecl;
|
|
107
|
-
}
|
|
108
|
-
if (parent.type === "ExportNamedDeclaration") {
|
|
109
|
-
return parent;
|
|
110
|
-
}
|
|
111
|
-
if (parent.type === "ExpressionStatement") {
|
|
112
|
-
return parent;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return node;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Report missing @story annotation on function or method
|
|
119
|
-
*
|
|
120
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
121
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
122
|
-
* @param {Rule.RuleContext} context - ESLint rule context
|
|
123
|
-
* @param {any} sourceCode - ESLint sourceCode object
|
|
124
|
-
* @param {any} node - function AST node missing annotation
|
|
125
|
-
* @param {any} target - AST node where annotation should be inserted
|
|
126
|
-
*/
|
|
127
|
-
function reportMissing(context, sourceCode, node, target) {
|
|
128
|
-
if (hasStoryAnnotation(sourceCode, node) ||
|
|
129
|
-
hasStoryAnnotation(sourceCode, target)) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
let name = getNodeName(node);
|
|
133
|
-
if (node.type === "TSDeclareFunction" && node.id && node.id.name) {
|
|
134
|
-
name = node.id.name;
|
|
135
|
-
}
|
|
136
|
-
context.report({
|
|
137
|
-
node,
|
|
138
|
-
messageId: "missingStory",
|
|
139
|
-
data: { name },
|
|
140
|
-
suggest: [
|
|
141
|
-
{
|
|
142
|
-
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${ANNOTATION}`,
|
|
143
|
-
fix: (fixer) => fixer.insertTextBefore(target, `${ANNOTATION}\n`),
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Report missing @story annotation on class methods
|
|
150
|
-
*
|
|
151
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
152
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
153
|
-
* @param {Rule.RuleContext} context - ESLint rule context
|
|
154
|
-
* @param {any} sourceCode - ESLint sourceCode object
|
|
155
|
-
* @param {any} node - MethodDefinition AST node
|
|
156
|
-
*/
|
|
157
|
-
function reportMethod(context, sourceCode, node) {
|
|
158
|
-
if (hasStoryAnnotation(sourceCode, node)) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
context.report({
|
|
162
|
-
node,
|
|
163
|
-
messageId: "missingStory",
|
|
164
|
-
data: { name: getNodeName(node) },
|
|
165
|
-
suggest: [
|
|
166
|
-
{
|
|
167
|
-
desc: `Add JSDoc @story annotation for function '${getNodeName(node)}', e.g., ${ANNOTATION}`,
|
|
168
|
-
fix: (fixer) => fixer.insertTextBefore(node, `${ANNOTATION}\n `),
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Check if this node is within scope and matches exportPriority
|
|
175
|
-
*
|
|
176
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
177
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
178
|
-
* @param {any} node - AST node to evaluate
|
|
179
|
-
* @param {string[]} scope - allowed node types
|
|
180
|
-
* @param {string} exportPriority - 'all' | 'exported' | 'non-exported'
|
|
181
|
-
* @returns {boolean} whether node should be processed
|
|
182
|
-
*/
|
|
183
|
-
function shouldProcessNode(node, scope, exportPriority) {
|
|
184
|
-
if (!scope.includes(node.type)) {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
const exported = isExportedNode(node);
|
|
188
|
-
if (exportPriority === "exported" && !exported) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
if (exportPriority === "non-exported" && exported) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
11
|
const rule = {
|
|
197
12
|
meta: {
|
|
198
13
|
type: "problem",
|
|
@@ -210,69 +25,43 @@ const rule = {
|
|
|
210
25
|
properties: {
|
|
211
26
|
scope: {
|
|
212
27
|
type: "array",
|
|
213
|
-
items: { type: "string", enum: DEFAULT_SCOPE },
|
|
28
|
+
items: { type: "string", enum: require_story_helpers_1.DEFAULT_SCOPE },
|
|
214
29
|
uniqueItems: true,
|
|
215
30
|
},
|
|
216
|
-
exportPriority: { type: "string", enum: EXPORT_PRIORITY_VALUES },
|
|
31
|
+
exportPriority: { type: "string", enum: require_story_helpers_1.EXPORT_PRIORITY_VALUES },
|
|
217
32
|
},
|
|
218
33
|
additionalProperties: false,
|
|
219
34
|
},
|
|
220
35
|
],
|
|
221
36
|
},
|
|
222
37
|
/**
|
|
223
|
-
* Create the rule visitor functions
|
|
38
|
+
* Create the rule visitor functions.
|
|
39
|
+
*
|
|
224
40
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
225
|
-
* @req REQ-CREATE-HOOK
|
|
41
|
+
* @req REQ-CREATE-HOOK
|
|
226
42
|
*/
|
|
227
43
|
create(context) {
|
|
228
44
|
const sourceCode = context.getSourceCode();
|
|
229
|
-
const opts = context.options[0] ||
|
|
230
|
-
|
|
231
|
-
const scope = opts.scope || DEFAULT_SCOPE;
|
|
45
|
+
const opts = (context.options && context.options[0]) || {};
|
|
46
|
+
const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
|
|
232
47
|
const exportPriority = opts.exportPriority || "all";
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
251
|
-
reportMissing(context, sourceCode, node, target);
|
|
252
|
-
},
|
|
253
|
-
ArrowFunctionExpression(node) {
|
|
254
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
255
|
-
return;
|
|
256
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
257
|
-
reportMissing(context, sourceCode, node, target);
|
|
258
|
-
},
|
|
259
|
-
TSDeclareFunction(node) {
|
|
260
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
261
|
-
return;
|
|
262
|
-
reportMissing(context, sourceCode, node, node);
|
|
263
|
-
},
|
|
264
|
-
TSMethodSignature(node) {
|
|
265
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
266
|
-
return;
|
|
267
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
268
|
-
reportMissing(context, sourceCode, node, target);
|
|
269
|
-
},
|
|
270
|
-
MethodDefinition(node) {
|
|
271
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
272
|
-
return;
|
|
273
|
-
reportMethod(context, sourceCode, node);
|
|
274
|
-
},
|
|
275
|
-
};
|
|
48
|
+
/**
|
|
49
|
+
* Debug log at the start of create to help diagnose rule activation in tests.
|
|
50
|
+
*
|
|
51
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
52
|
+
* @req REQ-DEBUG-LOG
|
|
53
|
+
*/
|
|
54
|
+
console.debug("require-story-annotation:create", typeof context.getFilename === "function"
|
|
55
|
+
? context.getFilename()
|
|
56
|
+
: "<unknown>");
|
|
57
|
+
// Local closure that binds configured scope and export priority to the helper.
|
|
58
|
+
const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority);
|
|
59
|
+
// Delegate visitor construction to helper to keep this file concise.
|
|
60
|
+
return (0, require_story_visitors_1.buildVisitors)(context, sourceCode, {
|
|
61
|
+
shouldProcessNode: should,
|
|
62
|
+
scope,
|
|
63
|
+
exportPriority,
|
|
64
|
+
});
|
|
276
65
|
},
|
|
277
66
|
};
|
|
278
67
|
exports.default = rule;
|
|
@@ -31,19 +31,47 @@ exports.DEFAULT_BRANCH_TYPES = [
|
|
|
31
31
|
*/
|
|
32
32
|
function validateBranchTypes(context) {
|
|
33
33
|
const options = context.options[0] || {};
|
|
34
|
+
/**
|
|
35
|
+
* Conditional branch checking whether branchTypes option was provided.
|
|
36
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
37
|
+
* @req REQ-TRACEABILITY-CONDITIONAL - Trace configuration branch existence check
|
|
38
|
+
*/
|
|
34
39
|
if (Array.isArray(options.branchTypes)) {
|
|
35
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Predicate to determine whether a provided branch type is invalid.
|
|
42
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
43
|
+
* @req REQ-TRACEABILITY-FILTER-CALLBACK - Trace filter callback for invalid branch type detection
|
|
44
|
+
*/
|
|
45
|
+
function isInvalidType(t) {
|
|
46
|
+
return !exports.DEFAULT_BRANCH_TYPES.includes(t);
|
|
47
|
+
}
|
|
48
|
+
const invalidTypes = options.branchTypes.filter(isInvalidType);
|
|
49
|
+
/**
|
|
50
|
+
* Conditional branch checking whether any invalid types were found.
|
|
51
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
52
|
+
* @req REQ-TRACEABILITY-INVALID-DETECTION - Trace handling when invalid types are detected
|
|
53
|
+
*/
|
|
36
54
|
if (invalidTypes.length > 0) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Program listener produced when configuration is invalid.
|
|
57
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
58
|
+
* @req REQ-TRACEABILITY-PROGRAM-LISTENER - Trace Program listener reporting invalid config values
|
|
59
|
+
*/
|
|
60
|
+
function ProgramHandler(node) {
|
|
61
|
+
/**
|
|
62
|
+
* Report a single invalid type for the given Program node.
|
|
63
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
64
|
+
* @req REQ-TRACEABILITY-FOR-EACH-CALLBACK - Trace reporting for each invalid type
|
|
65
|
+
*/
|
|
66
|
+
function reportInvalidType(t) {
|
|
67
|
+
context.report({
|
|
68
|
+
node,
|
|
69
|
+
message: `Value "${t}" should be equal to one of the allowed values: ${exports.DEFAULT_BRANCH_TYPES.join(", ")}`,
|
|
44
70
|
});
|
|
45
|
-
}
|
|
46
|
-
|
|
71
|
+
}
|
|
72
|
+
invalidTypes.forEach(reportInvalidType);
|
|
73
|
+
}
|
|
74
|
+
return { Program: ProgramHandler };
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
77
|
return Array.isArray(options.branchTypes)
|
|
@@ -56,11 +84,18 @@ function validateBranchTypes(context) {
|
|
|
56
84
|
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
57
85
|
*/
|
|
58
86
|
function gatherBranchCommentText(sourceCode, node) {
|
|
87
|
+
/**
|
|
88
|
+
* Conditional branch for SwitchCase nodes that may include inline comments.
|
|
89
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
90
|
+
* @req REQ-TRACEABILITY-SWITCHCASE-COMMENTS - Trace collection of preceding comments for SwitchCase
|
|
91
|
+
*/
|
|
59
92
|
if (node.type === "SwitchCase") {
|
|
60
93
|
const lines = sourceCode.lines;
|
|
61
94
|
const startLine = node.loc.start.line;
|
|
62
95
|
let i = startLine - PRE_COMMENT_OFFSET;
|
|
63
96
|
const comments = [];
|
|
97
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
98
|
+
// @req REQ-TRACEABILITY-WHILE - Trace while loop that collects preceding comments for SwitchCase
|
|
64
99
|
while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
|
|
65
100
|
comments.unshift(lines[i].trim());
|
|
66
101
|
i--;
|
|
@@ -68,7 +103,15 @@ function gatherBranchCommentText(sourceCode, node) {
|
|
|
68
103
|
return comments.join(" ");
|
|
69
104
|
}
|
|
70
105
|
const comments = sourceCode.getCommentsBefore(node) || [];
|
|
71
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Mapper to extract the text value from a comment node.
|
|
108
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
109
|
+
* @req REQ-TRACEABILITY-MAP-CALLBACK - Trace mapping of comment nodes to their text values
|
|
110
|
+
*/
|
|
111
|
+
function commentToValue(c) {
|
|
112
|
+
return c.value;
|
|
113
|
+
}
|
|
114
|
+
return comments.map(commentToValue).join(" ");
|
|
72
115
|
}
|
|
73
116
|
/**
|
|
74
117
|
* Report missing @story annotation on a branch node.
|
|
@@ -77,12 +120,25 @@ function gatherBranchCommentText(sourceCode, node) {
|
|
|
77
120
|
*/
|
|
78
121
|
function reportMissingStory(context, node, options) {
|
|
79
122
|
const { indent, insertPos, storyFixCountRef } = options;
|
|
123
|
+
/**
|
|
124
|
+
* Conditional branch deciding whether to offer an auto-fix for the missing story.
|
|
125
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
126
|
+
* @req REQ-TRACEABILITY-FIX-DECISION - Trace decision to provide fixer for missing @story
|
|
127
|
+
*/
|
|
80
128
|
if (storyFixCountRef.count === 0) {
|
|
129
|
+
/**
|
|
130
|
+
* Fixer that inserts a default @story annotation above the branch.
|
|
131
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
132
|
+
* @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer function used to insert missing @story
|
|
133
|
+
*/
|
|
134
|
+
function insertStoryFixer(fixer) {
|
|
135
|
+
return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`);
|
|
136
|
+
}
|
|
81
137
|
context.report({
|
|
82
138
|
node,
|
|
83
139
|
messageId: "missingAnnotation",
|
|
84
140
|
data: { missing: "@story" },
|
|
85
|
-
fix:
|
|
141
|
+
fix: insertStoryFixer,
|
|
86
142
|
});
|
|
87
143
|
storyFixCountRef.count++;
|
|
88
144
|
}
|
|
@@ -101,12 +157,25 @@ function reportMissingStory(context, node, options) {
|
|
|
101
157
|
*/
|
|
102
158
|
function reportMissingReq(context, node, options) {
|
|
103
159
|
const { indent, insertPos, missingStory } = options;
|
|
160
|
+
/**
|
|
161
|
+
* Conditional branch deciding whether to offer an auto-fix for the missing req.
|
|
162
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
163
|
+
* @req REQ-TRACEABILITY-FIX-DECISION - Trace decision to provide fixer for missing @req
|
|
164
|
+
*/
|
|
104
165
|
if (!missingStory) {
|
|
166
|
+
/**
|
|
167
|
+
* Fixer that inserts a default @req annotation above the branch.
|
|
168
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
169
|
+
* @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer function used to insert missing @req
|
|
170
|
+
*/
|
|
171
|
+
function insertReqFixer(fixer) {
|
|
172
|
+
return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`);
|
|
173
|
+
}
|
|
105
174
|
context.report({
|
|
106
175
|
node,
|
|
107
176
|
messageId: "missingAnnotation",
|
|
108
177
|
data: { missing: "@req" },
|
|
109
|
-
fix:
|
|
178
|
+
fix: insertReqFixer,
|
|
110
179
|
});
|
|
111
180
|
}
|
|
112
181
|
else {
|
|
@@ -144,5 +213,20 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
|
144
213
|
args: [context, node, { indent, insertPos, missingStory }],
|
|
145
214
|
},
|
|
146
215
|
];
|
|
147
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Process a single action from the actions array.
|
|
218
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
219
|
+
* @req REQ-TRACEABILITY-ACTIONS-FOREACH - Trace processing of actions array to report missing annotations
|
|
220
|
+
*/
|
|
221
|
+
function processAction(item) {
|
|
222
|
+
/**
|
|
223
|
+
* Callback invoked for each action to decide and execute reporting.
|
|
224
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
225
|
+
* @req REQ-TRACEABILITY-FOR-EACH-CALLBACK - Trace callback handling for each action item
|
|
226
|
+
*/
|
|
227
|
+
if (item.missing) {
|
|
228
|
+
item.fn(...item.args);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
actions.forEach(processAction);
|
|
148
232
|
}
|
|
@@ -87,11 +87,24 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
|
|
|
87
87
|
`;
|
|
88
88
|
fs.writeFileSync(filePath, content, "utf8");
|
|
89
89
|
// Remove read permission
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
try {
|
|
91
|
+
fs.chmodSync(dir, 0o000);
|
|
92
|
+
expect(() => (0, detect_1.detectStaleAnnotations)(tmpDir2)).toThrow();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
// Restore permissions and cleanup temporary directory, ignoring errors during cleanup
|
|
96
|
+
try {
|
|
97
|
+
fs.chmodSync(dir, 0o700);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
fs.rmSync(tmpDir2, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// ignore
|
|
107
|
+
}
|
|
108
|
+
}
|
|
96
109
|
});
|
|
97
110
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
6
|
+
* @req REQ-ANNOTATION-REQUIRED - Verify helper functions in require-story helpers produce correct fixes and reporting behavior
|
|
7
|
+
*/
|
|
8
|
+
const require_story_core_1 = require("../../src/rules/helpers/require-story-core");
|
|
9
|
+
const require_story_helpers_1 = require("../../src/rules/helpers/require-story-helpers");
|
|
10
|
+
describe("Require Story Helpers (Story 003.0)", () => {
|
|
11
|
+
test("createAddStoryFix uses parent range start when available", () => {
|
|
12
|
+
const target = {
|
|
13
|
+
type: "FunctionDeclaration",
|
|
14
|
+
range: [20, 40],
|
|
15
|
+
parent: { type: "ExportNamedDeclaration", range: [10, 50] },
|
|
16
|
+
};
|
|
17
|
+
const fixer = {
|
|
18
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
19
|
+
};
|
|
20
|
+
const fixFn = (0, require_story_core_1.createAddStoryFix)(target);
|
|
21
|
+
const result = fixFn(fixer);
|
|
22
|
+
expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
|
|
23
|
+
const calledArgs = fixer.insertTextBeforeRange.mock.calls[0];
|
|
24
|
+
expect(calledArgs[0]).toEqual([10, 10]);
|
|
25
|
+
expect(calledArgs[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n`);
|
|
26
|
+
expect(result).toEqual({ r: [10, 10], t: `${require_story_helpers_1.ANNOTATION}\n` });
|
|
27
|
+
});
|
|
28
|
+
test("createMethodFix falls back to node.range when parent not export", () => {
|
|
29
|
+
const node = {
|
|
30
|
+
type: "MethodDefinition",
|
|
31
|
+
range: [30, 60],
|
|
32
|
+
parent: { type: "ClassBody" },
|
|
33
|
+
};
|
|
34
|
+
const fixer = {
|
|
35
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
36
|
+
};
|
|
37
|
+
const fixFn = (0, require_story_core_1.createMethodFix)(node);
|
|
38
|
+
const res = fixFn(fixer);
|
|
39
|
+
expect(fixer.insertTextBeforeRange.mock.calls[0][0]).toEqual([30, 30]);
|
|
40
|
+
expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(`${require_story_helpers_1.ANNOTATION}\n `);
|
|
41
|
+
expect(res).toEqual({ r: [30, 30], t: `${require_story_helpers_1.ANNOTATION}\n ` });
|
|
42
|
+
});
|
|
43
|
+
test("reportMissing does not call context.report if JSDoc contains @story", () => {
|
|
44
|
+
const node = {
|
|
45
|
+
type: "FunctionDeclaration",
|
|
46
|
+
id: { name: "fn" },
|
|
47
|
+
range: [0, 10],
|
|
48
|
+
};
|
|
49
|
+
const fakeSource = {
|
|
50
|
+
getJSDocComment: () => ({
|
|
51
|
+
value: "@story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md",
|
|
52
|
+
}),
|
|
53
|
+
getText: () => "",
|
|
54
|
+
};
|
|
55
|
+
const context = {
|
|
56
|
+
getSourceCode: () => fakeSource,
|
|
57
|
+
report: jest.fn(),
|
|
58
|
+
};
|
|
59
|
+
(0, require_story_core_1.reportMissing)(context, fakeSource, node, node);
|
|
60
|
+
expect(context.report).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
test("reportMissing calls context.report when no JSDoc story present", () => {
|
|
63
|
+
const node = {
|
|
64
|
+
type: "FunctionDeclaration",
|
|
65
|
+
id: { name: "fn2" },
|
|
66
|
+
range: [0, 10],
|
|
67
|
+
};
|
|
68
|
+
const fakeSource = {
|
|
69
|
+
getJSDocComment: () => null,
|
|
70
|
+
getText: () => "",
|
|
71
|
+
};
|
|
72
|
+
const context = {
|
|
73
|
+
getSourceCode: () => fakeSource,
|
|
74
|
+
report: jest.fn(),
|
|
75
|
+
};
|
|
76
|
+
(0, require_story_core_1.reportMissing)(context, fakeSource, node, node);
|
|
77
|
+
expect(context.report).toHaveBeenCalledTimes(1);
|
|
78
|
+
const call = context.report.mock.calls[0][0];
|
|
79
|
+
expect(call.node).toBe(node);
|
|
80
|
+
expect(call.messageId).toBe("missingStory");
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Additional helper tests for story annotations and IO helpers
|
|
84
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
85
|
+
* @req REQ-ANNOTATION-REQUIRED - Verify resolveTargetNode/getNodeName/shouldProcessNode and IO helpers
|
|
86
|
+
*/
|
|
87
|
+
test("resolveTargetNode prefers parent when parent is ExportNamedDeclaration", () => {
|
|
88
|
+
const fakeSource = { getText: () => "" };
|
|
89
|
+
const node = {
|
|
90
|
+
type: "FunctionExpression",
|
|
91
|
+
range: [5, 10],
|
|
92
|
+
parent: { type: "ExportNamedDeclaration", range: [1, 20] },
|
|
93
|
+
};
|
|
94
|
+
const resolved = (0, require_story_helpers_1.resolveTargetNode)(fakeSource, node);
|
|
95
|
+
expect(resolved).toBe(node.parent);
|
|
96
|
+
});
|
|
97
|
+
test("resolveTargetNode falls back to node when parent is not an export", () => {
|
|
98
|
+
const fakeSource = { getText: () => "" };
|
|
99
|
+
const node = {
|
|
100
|
+
type: "FunctionDeclaration",
|
|
101
|
+
range: [5, 10],
|
|
102
|
+
parent: { type: "ClassBody", range: [1, 20] },
|
|
103
|
+
};
|
|
104
|
+
const resolved = (0, require_story_helpers_1.resolveTargetNode)(fakeSource, node);
|
|
105
|
+
expect(resolved).toBe(node);
|
|
106
|
+
});
|
|
107
|
+
test("getNodeName extracts names from common node shapes", () => {
|
|
108
|
+
const funcNode = {
|
|
109
|
+
type: "FunctionDeclaration",
|
|
110
|
+
id: { name: "myFunc" },
|
|
111
|
+
};
|
|
112
|
+
const propNode = { type: "MethodDefinition", key: { name: "myProp" } };
|
|
113
|
+
expect((0, require_story_helpers_1.getNodeName)(funcNode)).toBe("myFunc");
|
|
114
|
+
expect((0, require_story_helpers_1.getNodeName)(propNode)).toBe("myProp");
|
|
115
|
+
});
|
|
116
|
+
test("shouldProcessNode returns booleans for typical node types", () => {
|
|
117
|
+
const funcDecl = { type: "FunctionDeclaration" };
|
|
118
|
+
const varDecl = { type: "VariableDeclaration" };
|
|
119
|
+
expect((0, require_story_helpers_1.shouldProcessNode)(funcDecl, require_story_helpers_1.DEFAULT_SCOPE)).toBeTruthy();
|
|
120
|
+
expect((0, require_story_helpers_1.shouldProcessNode)(varDecl, require_story_helpers_1.DEFAULT_SCOPE)).toBeFalsy();
|
|
121
|
+
});
|
|
122
|
+
test("linesBeforeHasStory detects preceding JSDoc story text", () => {
|
|
123
|
+
const jsdoc = "/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\n";
|
|
124
|
+
const rest = "function fn() {}\n";
|
|
125
|
+
const full = jsdoc + rest;
|
|
126
|
+
const fakeSource = {
|
|
127
|
+
getText: () => full,
|
|
128
|
+
getJSDocComment: () => ({
|
|
129
|
+
value: "@story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md",
|
|
130
|
+
}),
|
|
131
|
+
lines: full.split(/\r?\n/),
|
|
132
|
+
};
|
|
133
|
+
const nodeLine = fakeSource.lines.findIndex((l) => l.includes("function fn() {}")) + 1;
|
|
134
|
+
const node = {
|
|
135
|
+
type: "FunctionDeclaration",
|
|
136
|
+
range: [full.indexOf("function"), full.length],
|
|
137
|
+
loc: { start: { line: nodeLine } },
|
|
138
|
+
};
|
|
139
|
+
const has = (0, require_story_helpers_1.linesBeforeHasStory)(fakeSource, node);
|
|
140
|
+
expect(has).toBeTruthy();
|
|
141
|
+
});
|
|
142
|
+
test("fallbackTextBeforeHasStory returns boolean when called with source text and range", () => {
|
|
143
|
+
const jsdoc = "/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\n";
|
|
144
|
+
const rest = "function fnB() {}\n";
|
|
145
|
+
const full = jsdoc + rest;
|
|
146
|
+
const fakeSource = {
|
|
147
|
+
getText: () => full,
|
|
148
|
+
};
|
|
149
|
+
const node = {
|
|
150
|
+
type: "FunctionDeclaration",
|
|
151
|
+
range: [full.indexOf("function"), full.length],
|
|
152
|
+
};
|
|
153
|
+
const res = (0, require_story_helpers_1.fallbackTextBeforeHasStory)(fakeSource, node);
|
|
154
|
+
expect(typeof res).toBe("boolean");
|
|
155
|
+
expect(res).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
test("parentChainHasStory returns true when ancestors have JSDoc story", () => {
|
|
158
|
+
const fakeSource = {
|
|
159
|
+
getCommentsBefore: () => [
|
|
160
|
+
{
|
|
161
|
+
type: "Block",
|
|
162
|
+
value: "@story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
const node = {
|
|
167
|
+
type: "Identifier",
|
|
168
|
+
parent: { parent: { type: "ExportNamedDeclaration" } },
|
|
169
|
+
};
|
|
170
|
+
const res = (0, require_story_helpers_1.parentChainHasStory)(fakeSource, node);
|
|
171
|
+
expect(res).toBeTruthy();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
3
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
|
+
* @req REQ-ANNOTATION-REQUIRED - Edge case tests for IO helpers (linesBeforeHasStory/fallbackTextBeforeHasStory/parentChainHasStory)
|
|
5
|
+
*/
|
|
6
|
+
export {};
|