eslint-plugin-traceability 1.4.3 → 1.4.5
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 +18 -0
- 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 +107 -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 -268
- package/lib/src/rules/valid-req-reference.js +9 -0
- package/lib/src/rules/valid-story-reference.js +4 -0
- package/lib/src/utils/branch-annotation-helpers.js +91 -31
- package/lib/tests/rules/require-story-core.branches.test.d.ts +1 -0
- package/lib/tests/rules/require-story-core.branches.test.js +69 -0
- package/lib/tests/rules/require-story-core.test.d.ts +1 -0
- package/lib/tests/rules/require-story-core.test.js +58 -0
- 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,99 +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
|
-
*/
|
|
251
|
-
FunctionExpression(node) {
|
|
252
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
253
|
-
return;
|
|
254
|
-
if (node.parent && node.parent.type === "MethodDefinition")
|
|
255
|
-
return;
|
|
256
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
257
|
-
reportMissing(context, sourceCode, node, target);
|
|
258
|
-
},
|
|
259
|
-
/**
|
|
260
|
-
* Handle ArrowFunctionExpression nodes
|
|
261
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
262
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
263
|
-
* @param {any} node - ArrowFunctionExpression AST node
|
|
264
|
-
*/
|
|
265
|
-
ArrowFunctionExpression(node) {
|
|
266
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
267
|
-
return;
|
|
268
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
269
|
-
reportMissing(context, sourceCode, node, target);
|
|
270
|
-
},
|
|
271
|
-
/**
|
|
272
|
-
* Handle TSDeclareFunction nodes
|
|
273
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
274
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
275
|
-
* @param {any} node - TSDeclareFunction AST node
|
|
276
|
-
*/
|
|
277
|
-
TSDeclareFunction(node) {
|
|
278
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
279
|
-
return;
|
|
280
|
-
reportMissing(context, sourceCode, node, node);
|
|
281
|
-
},
|
|
282
|
-
/**
|
|
283
|
-
* Handle TSMethodSignature nodes
|
|
284
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
285
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
286
|
-
* @param {any} node - TSMethodSignature AST node
|
|
287
|
-
*/
|
|
288
|
-
TSMethodSignature(node) {
|
|
289
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
290
|
-
return;
|
|
291
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
292
|
-
reportMissing(context, sourceCode, node, target);
|
|
293
|
-
},
|
|
294
|
-
/**
|
|
295
|
-
* Handle MethodDefinition nodes
|
|
296
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
297
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
298
|
-
* @param {any} node - MethodDefinition AST node
|
|
299
|
-
*/
|
|
300
|
-
MethodDefinition(node) {
|
|
301
|
-
if (!shouldProcessNode(node, scope, exportPriority))
|
|
302
|
-
return;
|
|
303
|
-
reportMethod(context, sourceCode, node);
|
|
304
|
-
},
|
|
305
|
-
};
|
|
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
|
+
});
|
|
306
65
|
},
|
|
307
66
|
};
|
|
308
67
|
exports.default = rule;
|
|
@@ -162,6 +162,15 @@ function programListener(context) {
|
|
|
162
162
|
let rawStoryPath = null;
|
|
163
163
|
return function Program() {
|
|
164
164
|
const comments = sourceCode.getAllComments() || [];
|
|
165
|
+
/**
|
|
166
|
+
* Process each comment to handle story and requirement annotations.
|
|
167
|
+
*
|
|
168
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
169
|
+
* @req REQ-DEEP-PARSE - Parse annotations from comment blocks
|
|
170
|
+
* @req REQ-DEEP-MATCH - Validate @req references found in comments
|
|
171
|
+
* @req REQ-DEEP-CACHE - Use cache for parsed story files to avoid repeated IO
|
|
172
|
+
* @req REQ-DEEP-PATH - Enforce path validation when resolving story files
|
|
173
|
+
*/
|
|
165
174
|
comments.forEach((comment) => {
|
|
166
175
|
rawStoryPath = handleComment({
|
|
167
176
|
comment,
|
|
@@ -94,6 +94,10 @@ function handleComment(opts) {
|
|
|
94
94
|
const { commentNode, context, cwd, storyDirs, allowAbsolute, requireExt } = opts;
|
|
95
95
|
const lines = commentNode.value
|
|
96
96
|
.split(/\r?\n/)
|
|
97
|
+
/**
|
|
98
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
99
|
+
* @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
|
|
100
|
+
*/
|
|
97
101
|
.map((l) => l.replace(/^[^@]*/, "").trim());
|
|
98
102
|
for (const line of lines) {
|
|
99
103
|
if (line.startsWith("@story")) {
|
|
@@ -31,28 +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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
*/
|
|
38
54
|
if (invalidTypes.length > 0) {
|
|
39
55
|
/**
|
|
40
56
|
* Program listener produced when configuration is invalid.
|
|
41
57
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
42
58
|
* @req REQ-TRACEABILITY-PROGRAM-LISTENER - Trace Program listener reporting invalid config values
|
|
43
59
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(", ")}`,
|
|
53
70
|
});
|
|
54
|
-
}
|
|
55
|
-
|
|
71
|
+
}
|
|
72
|
+
invalidTypes.forEach(reportInvalidType);
|
|
73
|
+
}
|
|
74
|
+
return { Program: ProgramHandler };
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
return Array.isArray(options.branchTypes)
|
|
@@ -65,6 +84,11 @@ function validateBranchTypes(context) {
|
|
|
65
84
|
* @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
|
|
66
85
|
*/
|
|
67
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
|
+
*/
|
|
68
92
|
if (node.type === "SwitchCase") {
|
|
69
93
|
const lines = sourceCode.lines;
|
|
70
94
|
const startLine = node.loc.start.line;
|
|
@@ -79,9 +103,15 @@ function gatherBranchCommentText(sourceCode, node) {
|
|
|
79
103
|
return comments.join(" ");
|
|
80
104
|
}
|
|
81
105
|
const comments = sourceCode.getCommentsBefore(node) || [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(" ");
|
|
85
115
|
}
|
|
86
116
|
/**
|
|
87
117
|
* Report missing @story annotation on a branch node.
|
|
@@ -90,15 +120,25 @@ function gatherBranchCommentText(sourceCode, node) {
|
|
|
90
120
|
*/
|
|
91
121
|
function reportMissingStory(context, node, options) {
|
|
92
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
|
+
*/
|
|
93
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
|
+
}
|
|
94
137
|
context.report({
|
|
95
138
|
node,
|
|
96
139
|
messageId: "missingAnnotation",
|
|
97
140
|
data: { missing: "@story" },
|
|
98
|
-
fix:
|
|
99
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
100
|
-
// @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer arrow function used to insert missing @story
|
|
101
|
-
(fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`),
|
|
141
|
+
fix: insertStoryFixer,
|
|
102
142
|
});
|
|
103
143
|
storyFixCountRef.count++;
|
|
104
144
|
}
|
|
@@ -117,15 +157,25 @@ function reportMissingStory(context, node, options) {
|
|
|
117
157
|
*/
|
|
118
158
|
function reportMissingReq(context, node, options) {
|
|
119
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
|
+
*/
|
|
120
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
|
+
}
|
|
121
174
|
context.report({
|
|
122
175
|
node,
|
|
123
176
|
messageId: "missingAnnotation",
|
|
124
177
|
data: { missing: "@req" },
|
|
125
|
-
fix:
|
|
126
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
127
|
-
// @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer arrow function used to insert missing @req
|
|
128
|
-
(fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`),
|
|
178
|
+
fix: insertReqFixer,
|
|
129
179
|
});
|
|
130
180
|
}
|
|
131
181
|
else {
|
|
@@ -163,10 +213,20 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
|
163
213
|
args: [context, node, { indent, insertPos, missingStory }],
|
|
164
214
|
},
|
|
165
215
|
];
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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);
|
|
172
232
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Branch 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-AUTOFIX - Cover additional branch cases in require-story-core (addStoryFixer/reportMissing)
|
|
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 Core branches (Story 003.0)", () => {
|
|
11
|
+
test("createAddStoryFix falls back to 0 when target is falsy", () => {
|
|
12
|
+
const fixer = {
|
|
13
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
14
|
+
};
|
|
15
|
+
const fixFn = (0, require_story_core_1.createAddStoryFix)(null);
|
|
16
|
+
const res = fixFn(fixer);
|
|
17
|
+
expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
|
|
18
|
+
const args = fixer.insertTextBeforeRange.mock.calls[0];
|
|
19
|
+
expect(args[0]).toEqual([0, 0]);
|
|
20
|
+
expect(args[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n`);
|
|
21
|
+
expect(res).toEqual({ r: [0, 0], t: `${require_story_helpers_1.ANNOTATION}\n` });
|
|
22
|
+
});
|
|
23
|
+
test("createAddStoryFix uses target.range when parent not export and parent.range missing", () => {
|
|
24
|
+
const target = {
|
|
25
|
+
type: "FunctionDeclaration",
|
|
26
|
+
range: [21, 33],
|
|
27
|
+
parent: { type: "ClassBody" },
|
|
28
|
+
};
|
|
29
|
+
const fixer = {
|
|
30
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
31
|
+
};
|
|
32
|
+
const fixFn = (0, require_story_core_1.createAddStoryFix)(target);
|
|
33
|
+
const res = fixFn(fixer);
|
|
34
|
+
expect(fixer.insertTextBeforeRange.mock.calls[0][0]).toEqual([21, 21]);
|
|
35
|
+
expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(`${require_story_helpers_1.ANNOTATION}\n`);
|
|
36
|
+
expect(res).toEqual({ r: [21, 21], t: `${require_story_helpers_1.ANNOTATION}\n` });
|
|
37
|
+
});
|
|
38
|
+
test("createAddStoryFix prefers ExportDefaultDeclaration parent.range when present", () => {
|
|
39
|
+
const target = {
|
|
40
|
+
type: "FunctionDeclaration",
|
|
41
|
+
range: [50, 70],
|
|
42
|
+
parent: { type: "ExportDefaultDeclaration", range: [5, 100] },
|
|
43
|
+
};
|
|
44
|
+
const fixer = {
|
|
45
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
46
|
+
};
|
|
47
|
+
const fixFn = (0, require_story_core_1.createAddStoryFix)(target);
|
|
48
|
+
const res = fixFn(fixer);
|
|
49
|
+
expect(fixer.insertTextBeforeRange.mock.calls[0][0]).toEqual([5, 5]);
|
|
50
|
+
expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(`${require_story_helpers_1.ANNOTATION}\n`);
|
|
51
|
+
expect(res).toEqual({ r: [5, 5], t: `${require_story_helpers_1.ANNOTATION}\n` });
|
|
52
|
+
});
|
|
53
|
+
test("reportMissing uses context.getSourceCode fallback when sourceCode not provided and still reports", () => {
|
|
54
|
+
const node = {
|
|
55
|
+
type: "FunctionDeclaration",
|
|
56
|
+
id: { name: "fnX" },
|
|
57
|
+
range: [0, 10],
|
|
58
|
+
};
|
|
59
|
+
const fakeSource = {
|
|
60
|
+
/* intentionally missing getJSDocComment to exercise branch */ getText: () => "",
|
|
61
|
+
};
|
|
62
|
+
const context = { getSourceCode: () => fakeSource, report: jest.fn() };
|
|
63
|
+
(0, require_story_core_1.reportMissing)(context, undefined, node, node);
|
|
64
|
+
expect(context.report).toHaveBeenCalledTimes(1);
|
|
65
|
+
const call = context.report.mock.calls[0][0];
|
|
66
|
+
expect(call.node).toBe(node);
|
|
67
|
+
expect(call.messageId).toBe("missingStory");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
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-AUTOFIX - Verify createMethodFix and reportMethod behaviors
|
|
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 Core (Story 003.0)", () => {
|
|
11
|
+
test("createMethodFix uses parent range start when parent is export", () => {
|
|
12
|
+
const node = {
|
|
13
|
+
type: "MethodDefinition",
|
|
14
|
+
range: [30, 60],
|
|
15
|
+
parent: { type: "ExportNamedDeclaration", range: [12, 90] },
|
|
16
|
+
};
|
|
17
|
+
const fixer = {
|
|
18
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
19
|
+
};
|
|
20
|
+
const fixFn = (0, require_story_core_1.createMethodFix)(node);
|
|
21
|
+
const result = fixFn(fixer);
|
|
22
|
+
expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
|
|
23
|
+
const calledArgs = fixer.insertTextBeforeRange.mock.calls[0];
|
|
24
|
+
expect(calledArgs[0]).toEqual([12, 12]);
|
|
25
|
+
expect(calledArgs[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n `);
|
|
26
|
+
expect(result).toEqual({ r: [12, 12], t: `${require_story_helpers_1.ANNOTATION}\n ` });
|
|
27
|
+
});
|
|
28
|
+
test("reportMethod calls context.report with proper data and suggest.fix works", () => {
|
|
29
|
+
const node = {
|
|
30
|
+
type: "MethodDefinition",
|
|
31
|
+
key: { name: "myMethod" },
|
|
32
|
+
range: [40, 80],
|
|
33
|
+
parent: { type: "ClassBody" },
|
|
34
|
+
};
|
|
35
|
+
const fakeSource = { getText: () => "" };
|
|
36
|
+
const context = {
|
|
37
|
+
getSourceCode: () => fakeSource,
|
|
38
|
+
report: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
(0, require_story_core_1.reportMethod)(context, fakeSource, node, node);
|
|
41
|
+
expect(context.report).toHaveBeenCalledTimes(1);
|
|
42
|
+
const call = context.report.mock.calls[0][0];
|
|
43
|
+
expect(call.messageId).toBe("missingStory");
|
|
44
|
+
expect(call.data).toEqual({ name: "myMethod" });
|
|
45
|
+
// The suggest fix should be a function; exercise it with a mock fixer
|
|
46
|
+
expect(Array.isArray(call.suggest)).toBe(true);
|
|
47
|
+
expect(typeof call.suggest[0].fix).toBe("function");
|
|
48
|
+
const fixer = {
|
|
49
|
+
insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
|
|
50
|
+
};
|
|
51
|
+
const fixResult = call.suggest[0].fix(fixer);
|
|
52
|
+
expect(fixer.insertTextBeforeRange).toHaveBeenCalled();
|
|
53
|
+
const args = fixer.insertTextBeforeRange.mock.calls[0];
|
|
54
|
+
expect(args[0]).toEqual([40, 40]);
|
|
55
|
+
expect(args[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n `);
|
|
56
|
+
expect(fixResult).toEqual({ r: [40, 40], t: `${require_story_helpers_1.ANNOTATION}\n ` });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|