eslint-plugin-traceability 1.3.0 → 1.4.1
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/README.md +2 -1
- package/lib/src/index.d.ts +12 -17
- package/lib/src/index.js +69 -24
- package/lib/src/maintenance/utils.js +5 -0
- package/lib/src/rules/require-branch-annotation.js +27 -147
- package/lib/src/rules/require-req-annotation.d.ts +5 -0
- package/lib/src/rules/require-req-annotation.js +20 -0
- package/lib/src/rules/require-story-annotation.d.ts +3 -12
- package/lib/src/rules/require-story-annotation.js +192 -162
- package/lib/src/rules/valid-annotation-format.js +11 -0
- package/lib/src/rules/valid-req-reference.js +65 -25
- package/lib/src/rules/valid-story-reference.js +55 -58
- package/lib/src/utils/annotation-checker.js +80 -13
- package/lib/src/utils/branch-annotation-helpers.d.ts +54 -0
- package/lib/src/utils/branch-annotation-helpers.js +148 -0
- package/lib/src/utils/storyReferenceUtils.d.ts +47 -0
- package/lib/src/utils/storyReferenceUtils.js +111 -0
- package/lib/tests/cli-error-handling.test.d.ts +1 -0
- package/lib/tests/cli-error-handling.test.js +44 -0
- package/lib/tests/integration/cli-integration.test.js +3 -5
- package/lib/tests/maintenance/index.test.d.ts +1 -0
- package/lib/tests/maintenance/index.test.js +25 -0
- package/lib/tests/plugin-setup-error.test.d.ts +5 -0
- package/lib/tests/plugin-setup-error.test.js +37 -0
- package/lib/tests/rules/error-reporting.test.d.ts +1 -0
- package/lib/tests/rules/error-reporting.test.js +47 -0
- package/lib/tests/rules/require-story-annotation.test.js +101 -25
- package/lib/tests/utils/branch-annotation-helpers.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-helpers.test.js +46 -0
- package/package.json +4 -3
|
@@ -1,184 +1,206 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Rule to enforce @story annotation on functions, function expressions, arrow functions, and methods
|
|
4
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
|
-
* @req REQ-ANNOTATION-REQUIRED - Require @story annotation on functions
|
|
6
|
-
* @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
|
|
7
|
-
* @req REQ-EXPORT-PRIORITY - Add exportPriority option to target exported or non-exported
|
|
8
|
-
* @req REQ-UNIFIED-CHECK - Implement unified checkNode for all supported node types
|
|
9
|
-
* @req REQ-FUNCTION-DETECTION - Detect function declarations, expressions, arrow functions, and methods
|
|
10
|
-
* @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
|
|
11
|
-
*/
|
|
12
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Default node types to check for function annotations
|
|
4
|
+
const DEFAULT_SCOPE = [
|
|
5
|
+
"FunctionDeclaration",
|
|
6
|
+
"FunctionExpression",
|
|
7
|
+
"ArrowFunctionExpression",
|
|
8
|
+
"MethodDefinition",
|
|
9
|
+
"TSDeclareFunction",
|
|
10
|
+
"TSMethodSignature",
|
|
11
|
+
];
|
|
12
|
+
const EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
|
|
13
13
|
/**
|
|
14
|
-
* Determine if a node is
|
|
14
|
+
* Determine if a node is in an export declaration
|
|
15
|
+
*
|
|
15
16
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
16
|
-
* @req REQ-
|
|
17
|
+
* @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
|
|
17
20
|
*/
|
|
18
21
|
function isExportedNode(node) {
|
|
19
|
-
let
|
|
20
|
-
while (
|
|
21
|
-
if (
|
|
22
|
-
|
|
22
|
+
let p = node.parent;
|
|
23
|
+
while (p) {
|
|
24
|
+
if (p.type === "ExportNamedDeclaration" ||
|
|
25
|
+
p.type === "ExportDefaultDeclaration") {
|
|
23
26
|
return true;
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
p = p.parent;
|
|
26
29
|
}
|
|
27
30
|
return false;
|
|
28
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} */`;
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
36
|
+
* Check if @story annotation already present in JSDoc or preceding comments
|
|
37
|
+
*
|
|
31
38
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
32
|
-
* @req REQ-
|
|
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
|
|
33
43
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return current;
|
|
39
|
-
}
|
|
40
|
-
current = current.parent;
|
|
44
|
+
function hasStoryAnnotation(sourceCode, node) {
|
|
45
|
+
const jsdoc = sourceCode.getJSDocComment(node);
|
|
46
|
+
if (jsdoc?.value.includes("@story")) {
|
|
47
|
+
return true;
|
|
41
48
|
}
|
|
42
|
-
|
|
49
|
+
const comments = sourceCode.getCommentsBefore(node) || [];
|
|
50
|
+
return comments.some((c) => c.value.includes("@story"));
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
|
-
*
|
|
53
|
+
* Get the name of the function-like node
|
|
54
|
+
*
|
|
46
55
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
47
|
-
* @req REQ-
|
|
48
|
-
* @
|
|
49
|
-
* @
|
|
56
|
+
* @req REQ-ANNOTATION-REQUIRED
|
|
57
|
+
* @param {any} node - AST node representing a function-like construct
|
|
58
|
+
* @returns {string} the resolved name or "<unknown>"
|
|
50
59
|
*/
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
63
81
|
}
|
|
64
|
-
return
|
|
82
|
+
return "<unknown>";
|
|
65
83
|
}
|
|
66
84
|
/**
|
|
67
|
-
*
|
|
85
|
+
* Determine AST node where annotation should be inserted
|
|
86
|
+
*
|
|
68
87
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
69
|
-
* @req REQ-
|
|
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
|
|
70
92
|
*/
|
|
71
93
|
function resolveTargetNode(sourceCode, node) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"ExportNamedDeclaration",
|
|
76
|
-
"ExportDefaultDeclaration",
|
|
77
|
-
]);
|
|
78
|
-
if (exp) {
|
|
79
|
-
target = exp;
|
|
80
|
-
}
|
|
94
|
+
if (node.type === "TSMethodSignature") {
|
|
95
|
+
// Interface method signature -> insert on interface
|
|
96
|
+
return node.parent.parent;
|
|
81
97
|
}
|
|
82
|
-
|
|
98
|
+
if (node.type === "FunctionExpression" ||
|
|
83
99
|
node.type === "ArrowFunctionExpression") {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
target = exp;
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
const anc = findAncestorNode(node, [
|
|
93
|
-
"VariableDeclaration",
|
|
94
|
-
"ExpressionStatement",
|
|
95
|
-
]);
|
|
96
|
-
if (anc) {
|
|
97
|
-
target = anc;
|
|
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;
|
|
98
105
|
}
|
|
106
|
+
return varDecl;
|
|
99
107
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
108
|
+
if (parent.type === "ExportNamedDeclaration") {
|
|
109
|
+
return parent;
|
|
110
|
+
}
|
|
111
|
+
if (parent.type === "ExpressionStatement") {
|
|
112
|
+
return parent;
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
|
-
|
|
108
|
-
target = node;
|
|
109
|
-
}
|
|
110
|
-
return target;
|
|
115
|
+
return node;
|
|
111
116
|
}
|
|
112
117
|
/**
|
|
113
|
-
*
|
|
118
|
+
* Report missing @story annotation on function or method
|
|
119
|
+
*
|
|
114
120
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
115
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
|
|
116
126
|
*/
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
127
|
+
function reportMissing(context, sourceCode, node, target) {
|
|
128
|
+
if (hasStoryAnnotation(sourceCode, node) ||
|
|
129
|
+
hasStoryAnnotation(sourceCode, target)) {
|
|
130
|
+
return;
|
|
121
131
|
}
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
});
|
|
124
147
|
}
|
|
125
148
|
/**
|
|
126
|
-
*
|
|
149
|
+
* Report missing @story annotation on class methods
|
|
150
|
+
*
|
|
127
151
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
128
|
-
* @req REQ-UNIFIED-CHECK
|
|
129
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
|
|
130
156
|
*/
|
|
131
|
-
function
|
|
132
|
-
if (
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
// Special handling for TSMethodSignature: allow annotation on the method itself
|
|
136
|
-
if (node.type === "TSMethodSignature") {
|
|
137
|
-
// If annotated on the method signature, skip
|
|
138
|
-
if (hasStoryAnnotation(sourceCode, node)) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
// Otherwise, check on interface declaration
|
|
142
|
-
const intf = resolveTargetNode(sourceCode, node);
|
|
143
|
-
if (hasStoryAnnotation(sourceCode, intf)) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
// Missing annotation: report on the interface declaration
|
|
147
|
-
context.report({
|
|
148
|
-
node: intf,
|
|
149
|
-
messageId: "missingStory",
|
|
150
|
-
fix(fixer) {
|
|
151
|
-
const indentLevel = intf.loc.start.column;
|
|
152
|
-
const indent = " ".repeat(indentLevel);
|
|
153
|
-
const insertPos = intf.range[0] - indentLevel;
|
|
154
|
-
return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}/** @story <story-file>.story.md */\n`);
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const target = resolveTargetNode(sourceCode, node);
|
|
160
|
-
if (hasStoryAnnotation(sourceCode, target)) {
|
|
157
|
+
function reportMethod(context, sourceCode, node) {
|
|
158
|
+
if (hasStoryAnnotation(sourceCode, node)) {
|
|
161
159
|
return;
|
|
162
160
|
}
|
|
163
161
|
context.report({
|
|
164
162
|
node,
|
|
165
163
|
messageId: "missingStory",
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
],
|
|
172
171
|
});
|
|
173
172
|
}
|
|
174
|
-
|
|
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
|
+
const rule = {
|
|
175
197
|
meta: {
|
|
176
198
|
type: "problem",
|
|
177
199
|
docs: {
|
|
178
|
-
description: "Require @story annotations on
|
|
200
|
+
description: "Require @story annotations on functions",
|
|
179
201
|
recommended: "error",
|
|
180
202
|
},
|
|
181
|
-
|
|
203
|
+
hasSuggestions: true,
|
|
182
204
|
messages: {
|
|
183
205
|
missingStory: "Missing @story annotation (REQ-ANNOTATION-REQUIRED)",
|
|
184
206
|
},
|
|
@@ -188,61 +210,69 @@ exports.default = {
|
|
|
188
210
|
properties: {
|
|
189
211
|
scope: {
|
|
190
212
|
type: "array",
|
|
191
|
-
items: {
|
|
192
|
-
enum: [
|
|
193
|
-
"FunctionDeclaration",
|
|
194
|
-
"FunctionExpression",
|
|
195
|
-
"ArrowFunctionExpression",
|
|
196
|
-
"MethodDefinition",
|
|
197
|
-
"TSDeclareFunction",
|
|
198
|
-
"TSMethodSignature",
|
|
199
|
-
],
|
|
200
|
-
},
|
|
213
|
+
items: { type: "string", enum: DEFAULT_SCOPE },
|
|
201
214
|
uniqueItems: true,
|
|
202
215
|
},
|
|
203
|
-
exportPriority: {
|
|
204
|
-
enum: ["all", "exported", "non-exported"],
|
|
205
|
-
},
|
|
216
|
+
exportPriority: { type: "string", enum: EXPORT_PRIORITY_VALUES },
|
|
206
217
|
},
|
|
207
218
|
additionalProperties: false,
|
|
208
219
|
},
|
|
209
220
|
],
|
|
210
221
|
},
|
|
222
|
+
/**
|
|
223
|
+
* Create the rule visitor functions for require-story-annotation.
|
|
224
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
225
|
+
* @req REQ-CREATE-HOOK - Provide create(context) hook for rule behavior
|
|
226
|
+
*/
|
|
211
227
|
create(context) {
|
|
212
228
|
const sourceCode = context.getSourceCode();
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"ArrowFunctionExpression",
|
|
218
|
-
"MethodDefinition",
|
|
219
|
-
"TSDeclareFunction",
|
|
220
|
-
"TSMethodSignature",
|
|
221
|
-
];
|
|
222
|
-
const exportPriority = options.exportPriority || "all";
|
|
229
|
+
const opts = context.options[0] ||
|
|
230
|
+
{};
|
|
231
|
+
const scope = opts.scope || DEFAULT_SCOPE;
|
|
232
|
+
const exportPriority = opts.exportPriority || "all";
|
|
223
233
|
return {
|
|
224
234
|
FunctionDeclaration(node) {
|
|
225
|
-
|
|
235
|
+
if (!shouldProcessNode(node, scope, exportPriority))
|
|
236
|
+
return;
|
|
237
|
+
let target = node;
|
|
238
|
+
if (node.parent &&
|
|
239
|
+
(node.parent.type === "ExportNamedDeclaration" ||
|
|
240
|
+
node.parent.type === "ExportDefaultDeclaration")) {
|
|
241
|
+
target = node.parent;
|
|
242
|
+
}
|
|
243
|
+
reportMissing(context, sourceCode, node, target);
|
|
226
244
|
},
|
|
227
245
|
FunctionExpression(node) {
|
|
228
|
-
|
|
246
|
+
if (!shouldProcessNode(node, scope, exportPriority))
|
|
247
|
+
return;
|
|
248
|
+
if (node.parent && node.parent.type === "MethodDefinition")
|
|
249
|
+
return;
|
|
250
|
+
const target = resolveTargetNode(sourceCode, node);
|
|
251
|
+
reportMissing(context, sourceCode, node, target);
|
|
229
252
|
},
|
|
230
253
|
ArrowFunctionExpression(node) {
|
|
231
|
-
|
|
254
|
+
if (!shouldProcessNode(node, scope, exportPriority))
|
|
255
|
+
return;
|
|
256
|
+
const target = resolveTargetNode(sourceCode, node);
|
|
257
|
+
reportMissing(context, sourceCode, node, target);
|
|
232
258
|
},
|
|
233
|
-
MethodDefinition(node) {
|
|
234
|
-
checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
|
|
235
|
-
},
|
|
236
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
237
|
-
// @req REQ-FUNCTION-DETECTION - Detect TS-specific function syntax
|
|
238
259
|
TSDeclareFunction(node) {
|
|
239
|
-
|
|
260
|
+
if (!shouldProcessNode(node, scope, exportPriority))
|
|
261
|
+
return;
|
|
262
|
+
reportMissing(context, sourceCode, node, node);
|
|
240
263
|
},
|
|
241
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
242
|
-
// @req REQ-FUNCTION-DETECTION - Detect TS-specific function syntax
|
|
243
264
|
TSMethodSignature(node) {
|
|
244
|
-
|
|
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);
|
|
245
274
|
},
|
|
246
275
|
};
|
|
247
276
|
},
|
|
248
277
|
};
|
|
278
|
+
exports.default = rule;
|
|
@@ -21,9 +21,20 @@ exports.default = {
|
|
|
21
21
|
},
|
|
22
22
|
schema: [],
|
|
23
23
|
},
|
|
24
|
+
/**
|
|
25
|
+
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
26
|
+
* @req REQ-SYNTAX-VALIDATION - Ensure rule create function validates annotations syntax
|
|
27
|
+
* @req REQ-FORMAT-SPECIFICATION - Implement formatting checks per specification
|
|
28
|
+
*/
|
|
24
29
|
create(context) {
|
|
25
30
|
const sourceCode = context.getSourceCode();
|
|
26
31
|
return {
|
|
32
|
+
/**
|
|
33
|
+
* Program-level handler that inspects all comments for @story and @req tags
|
|
34
|
+
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
35
|
+
* @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
|
|
36
|
+
* @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
|
|
37
|
+
*/
|
|
27
38
|
Program() {
|
|
28
39
|
const comments = sourceCode.getAllComments() || [];
|
|
29
40
|
comments.forEach((comment) => {
|
|
@@ -19,6 +19,9 @@ const path_1 = __importDefault(require("path"));
|
|
|
19
19
|
* Parses comment.value lines for @story annotation.
|
|
20
20
|
* @param comment any JSDoc comment node
|
|
21
21
|
* @returns story path or null if not found
|
|
22
|
+
*
|
|
23
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
24
|
+
* @req REQ-DEEP-PARSE - Extracts @story annotation from comment content
|
|
22
25
|
*/
|
|
23
26
|
function extractStoryPath(comment) {
|
|
24
27
|
const rawLines = comment.value.split(/\r?\n/);
|
|
@@ -34,14 +37,17 @@ function extractStoryPath(comment) {
|
|
|
34
37
|
/**
|
|
35
38
|
* Validate a @req annotation line against the extracted story content.
|
|
36
39
|
* Performs path validation, file reading, caching, and requirement existence checks.
|
|
37
|
-
*
|
|
38
|
-
* @param
|
|
39
|
-
*
|
|
40
|
-
* @
|
|
41
|
-
* @
|
|
42
|
-
* @
|
|
40
|
+
*
|
|
41
|
+
* @param opts options bag
|
|
42
|
+
*
|
|
43
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
44
|
+
* @req REQ-DEEP-PATH - Validates and protects against path traversal and absolute paths
|
|
45
|
+
* @req REQ-DEEP-CACHE - Caches parsed story files to avoid repeated IO
|
|
46
|
+
* @req REQ-DEEP-MATCH - Ensures referenced requirement IDs exist in the story file
|
|
47
|
+
* @req REQ-DEEP-PARSE - Parses story file content to find REQ- identifiers
|
|
43
48
|
*/
|
|
44
|
-
function validateReqLine(
|
|
49
|
+
function validateReqLine(opts) {
|
|
50
|
+
const { comment, context, line, storyPath, cwd, reqCache } = opts;
|
|
45
51
|
const parts = line.split(/\s+/);
|
|
46
52
|
const reqId = parts[1];
|
|
47
53
|
if (!reqId || !storyPath) {
|
|
@@ -93,43 +99,62 @@ function validateReqLine(comment, context, line, storyPath, cwd, reqCache) {
|
|
|
93
99
|
* Handle a single annotation line.
|
|
94
100
|
* @story Updates the current story path when encountering an @story annotation
|
|
95
101
|
* @req Validates the requirement reference against the current story content
|
|
96
|
-
*
|
|
97
|
-
* @param
|
|
98
|
-
*
|
|
99
|
-
* @
|
|
100
|
-
* @
|
|
101
|
-
* @
|
|
102
|
-
* @returns updated story path or null
|
|
102
|
+
*
|
|
103
|
+
* @param opts handler options
|
|
104
|
+
*
|
|
105
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
106
|
+
* @req REQ-DEEP-PARSE - Recognizes @story and @req annotation lines
|
|
107
|
+
* @req REQ-DEEP-MATCH - Delegates @req validation to validateReqLine
|
|
103
108
|
*/
|
|
104
|
-
function handleAnnotationLine(
|
|
109
|
+
function handleAnnotationLine(opts) {
|
|
110
|
+
const { line, comment, context, cwd, reqCache, storyPath } = opts;
|
|
105
111
|
if (line.startsWith("@story")) {
|
|
106
112
|
const newPath = extractStoryPath(comment);
|
|
107
113
|
return newPath || storyPath;
|
|
108
114
|
}
|
|
109
115
|
else if (line.startsWith("@req")) {
|
|
110
|
-
validateReqLine(comment, context, line, storyPath, cwd, reqCache);
|
|
116
|
+
validateReqLine({ comment, context, line, storyPath, cwd, reqCache });
|
|
111
117
|
return storyPath;
|
|
112
118
|
}
|
|
113
119
|
return storyPath;
|
|
114
120
|
}
|
|
115
121
|
/**
|
|
116
122
|
* Handle JSDoc story and req annotations.
|
|
117
|
-
*
|
|
118
|
-
* @param
|
|
119
|
-
*
|
|
120
|
-
* @
|
|
121
|
-
* @
|
|
122
|
-
* @
|
|
123
|
+
*
|
|
124
|
+
* @param opts options for comment handling
|
|
125
|
+
*
|
|
126
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
127
|
+
* @req REQ-DEEP-PARSE - Parses comment blocks to extract annotation lines
|
|
128
|
+
* @req REQ-DEEP-MATCH - Uses handleAnnotationLine to validate @req entries
|
|
129
|
+
* @req REQ-DEEP-CACHE - Passes shared cache for parsed story files
|
|
123
130
|
*/
|
|
124
|
-
function handleComment(
|
|
131
|
+
function handleComment(opts) {
|
|
132
|
+
const { comment, context, cwd, reqCache, rawStoryPath } = opts;
|
|
125
133
|
let storyPath = rawStoryPath;
|
|
126
134
|
const rawLines = comment.value.split(/\r?\n/);
|
|
127
135
|
for (const rawLine of rawLines) {
|
|
128
136
|
const line = rawLine.trim().replace(/^\*+\s*/, "");
|
|
129
|
-
storyPath = handleAnnotationLine(
|
|
137
|
+
storyPath = handleAnnotationLine({
|
|
138
|
+
line,
|
|
139
|
+
comment,
|
|
140
|
+
context,
|
|
141
|
+
cwd,
|
|
142
|
+
reqCache,
|
|
143
|
+
storyPath,
|
|
144
|
+
});
|
|
130
145
|
}
|
|
131
146
|
return storyPath;
|
|
132
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Create a Program listener that iterates comments and validates annotations.
|
|
150
|
+
*
|
|
151
|
+
* @param context ESLint rule context
|
|
152
|
+
* @returns Program visitor function
|
|
153
|
+
*
|
|
154
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
155
|
+
* @req REQ-DEEP-CACHE - Maintains a cache across comment processing
|
|
156
|
+
* @req REQ-DEEP-PATH - Resolves and protects story paths against traversal
|
|
157
|
+
*/
|
|
133
158
|
function programListener(context) {
|
|
134
159
|
const sourceCode = context.getSourceCode();
|
|
135
160
|
const cwd = process.cwd();
|
|
@@ -138,7 +163,13 @@ function programListener(context) {
|
|
|
138
163
|
return function Program() {
|
|
139
164
|
const comments = sourceCode.getAllComments() || [];
|
|
140
165
|
comments.forEach((comment) => {
|
|
141
|
-
rawStoryPath = handleComment(
|
|
166
|
+
rawStoryPath = handleComment({
|
|
167
|
+
comment,
|
|
168
|
+
context,
|
|
169
|
+
cwd,
|
|
170
|
+
reqCache,
|
|
171
|
+
rawStoryPath,
|
|
172
|
+
});
|
|
142
173
|
});
|
|
143
174
|
};
|
|
144
175
|
}
|
|
@@ -155,6 +186,15 @@ exports.default = {
|
|
|
155
186
|
},
|
|
156
187
|
schema: [],
|
|
157
188
|
},
|
|
189
|
+
/**
|
|
190
|
+
* Rule create entrypoint that returns the Program visitor.
|
|
191
|
+
*
|
|
192
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
193
|
+
* @req REQ-DEEP-MATCH - Entrypoint orchestrates validation of @req annotations
|
|
194
|
+
* @req REQ-DEEP-PARSE - Uses parsing helpers to extract annotations and story paths
|
|
195
|
+
* @req REQ-DEEP-CACHE - Establishes cache used during validation
|
|
196
|
+
* @req REQ-DEEP-PATH - Ensures path validation is applied during checks
|
|
197
|
+
*/
|
|
158
198
|
create(context) {
|
|
159
199
|
return { Program: programListener(context) };
|
|
160
200
|
},
|