eslint-plugin-traceability 1.2.0 → 1.4.0

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.
@@ -1,184 +1,164 @@
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 exported via export declaration.
15
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
16
- * @req REQ-EXPORT-PRIORITY - Determine if function node has export declaration ancestor
14
+ * Determine if a node is in an export declaration
17
15
  */
18
16
  function isExportedNode(node) {
19
- let current = node;
20
- while (current) {
21
- if (current.type === "ExportNamedDeclaration" ||
22
- current.type === "ExportDefaultDeclaration") {
17
+ let p = node.parent;
18
+ while (p) {
19
+ if (p.type === "ExportNamedDeclaration" ||
20
+ p.type === "ExportDefaultDeclaration") {
23
21
  return true;
24
22
  }
25
- current = current.parent;
23
+ p = p.parent;
26
24
  }
27
25
  return false;
28
26
  }
27
+ // Path to the story file for annotations
28
+ const STORY_PATH = "docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md";
29
+ const ANNOTATION = `/** @story ${STORY_PATH} */`;
29
30
  /**
30
- * Find nearest ancestor node of specified types.
31
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
32
- * @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
31
+ * Check if @story annotation already present in JSDoc or preceding comments
33
32
  */
34
- function findAncestorNode(node, types) {
35
- let current = node.parent;
36
- while (current) {
37
- if (types.includes(current.type)) {
38
- return current;
39
- }
40
- current = current.parent;
33
+ function hasStoryAnnotation(sourceCode, node) {
34
+ const jsdoc = sourceCode.getJSDocComment(node);
35
+ if (jsdoc?.value.includes("@story")) {
36
+ return true;
41
37
  }
42
- return null;
38
+ const comments = sourceCode.getCommentsBefore(node) || [];
39
+ return comments.some((c) => c.value.includes("@story"));
43
40
  }
44
41
  /**
45
- * Determine if node should be checked based on scope and exportPriority.
46
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
47
- * @req REQ-OPTIONS-SCOPE
48
- * @req REQ-EXPORT-PRIORITY
49
- * @req REQ-UNIFIED-CHECK
42
+ * Get the name of the function-like node
50
43
  */
51
- function shouldCheckNode(node, scope, exportPriority) {
52
- if (node.type === "FunctionExpression" &&
53
- node.parent?.type === "MethodDefinition") {
54
- return false;
55
- }
56
- if (!scope.includes(node.type)) {
57
- return false;
58
- }
59
- const exported = isExportedNode(node);
60
- if ((exportPriority === "exported" && !exported) ||
61
- (exportPriority === "non-exported" && exported)) {
62
- return false;
44
+ function getNodeName(node) {
45
+ let current = node;
46
+ while (current) {
47
+ if (current.type === "VariableDeclarator" &&
48
+ current.id &&
49
+ typeof current.id.name === "string") {
50
+ return current.id.name;
51
+ }
52
+ if ((current.type === "FunctionDeclaration" ||
53
+ current.type === "TSDeclareFunction") &&
54
+ current.id &&
55
+ typeof current.id.name === "string") {
56
+ return current.id.name;
57
+ }
58
+ if ((current.type === "MethodDefinition" ||
59
+ current.type === "TSMethodSignature") &&
60
+ current.key &&
61
+ typeof current.key.name === "string") {
62
+ return current.key.name;
63
+ }
64
+ current = current.parent;
63
65
  }
64
- return true;
66
+ return "<unknown>";
65
67
  }
66
68
  /**
67
- * Resolve the AST node to annotate or check.
68
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
69
- * @req REQ-UNIFIED-CHECK
69
+ * Determine AST node where annotation should be inserted
70
70
  */
71
71
  function resolveTargetNode(sourceCode, node) {
72
- let target = node;
73
- if (node.type === "FunctionDeclaration") {
74
- const exp = findAncestorNode(node, [
75
- "ExportNamedDeclaration",
76
- "ExportDefaultDeclaration",
77
- ]);
78
- if (exp) {
79
- target = exp;
80
- }
72
+ if (node.type === "TSMethodSignature") {
73
+ // Interface method signature -> insert on interface
74
+ return node.parent.parent;
81
75
  }
82
- else if (node.type === "FunctionExpression" ||
76
+ if (node.type === "FunctionExpression" ||
83
77
  node.type === "ArrowFunctionExpression") {
84
- const exp = findAncestorNode(node, [
85
- "ExportNamedDeclaration",
86
- "ExportDefaultDeclaration",
87
- ]);
88
- if (exp) {
89
- target = exp;
90
- }
91
- else {
92
- const anc = findAncestorNode(node, [
93
- "VariableDeclaration",
94
- "ExpressionStatement",
95
- ]);
96
- if (anc) {
97
- target = anc;
78
+ const parent = node.parent;
79
+ if (parent.type === "VariableDeclarator") {
80
+ const varDecl = parent.parent;
81
+ if (varDecl.parent && varDecl.parent.type === "ExportNamedDeclaration") {
82
+ return varDecl.parent;
98
83
  }
84
+ return varDecl;
99
85
  }
100
- }
101
- else if (node.type === "TSMethodSignature") {
102
- const exp = findAncestorNode(node, ["TSInterfaceDeclaration"]);
103
- if (exp) {
104
- target = exp;
86
+ if (parent.type === "ExportNamedDeclaration") {
87
+ return parent;
88
+ }
89
+ if (parent.type === "ExpressionStatement") {
90
+ return parent;
105
91
  }
106
92
  }
107
- else if (node.type === "MethodDefinition") {
108
- target = node;
109
- }
110
- return target;
93
+ return node;
111
94
  }
112
95
  /**
113
- * Check if the target node has @story annotation.
114
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
115
- * @req REQ-ANNOTATION-REQUIRED
96
+ * Report missing @story annotation on function or method
116
97
  */
117
- function hasStoryAnnotation(sourceCode, target) {
118
- const jsdoc = sourceCode.getJSDocComment(target);
119
- if (jsdoc?.value.includes("@story")) {
120
- return true;
98
+ function reportMissing(context, sourceCode, node, target) {
99
+ if (hasStoryAnnotation(sourceCode, node) ||
100
+ hasStoryAnnotation(sourceCode, target)) {
101
+ return;
121
102
  }
122
- const comments = sourceCode.getCommentsBefore(target) || [];
123
- return comments.some((c) => c.value.includes("@story"));
103
+ let name = getNodeName(node);
104
+ if (node.type === "TSDeclareFunction" && node.id && node.id.name) {
105
+ name = node.id.name;
106
+ }
107
+ context.report({
108
+ node,
109
+ messageId: "missingStory",
110
+ data: { name },
111
+ suggest: [
112
+ {
113
+ desc: `Add JSDoc @story annotation for function '${name}', e.g., ${ANNOTATION}`,
114
+ fix: (fixer) => fixer.insertTextBefore(target, `${ANNOTATION}\n`),
115
+ },
116
+ ],
117
+ });
124
118
  }
125
119
  /**
126
- * Check for @story annotation on function-like nodes.
127
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
128
- * @req REQ-UNIFIED-CHECK
129
- * @req REQ-ANNOTATION-REQUIRED
120
+ * Report missing @story annotation on class methods
130
121
  */
131
- function checkStoryAnnotation(sourceCode, context, node, scope, exportPriority) {
132
- if (!shouldCheckNode(node, scope, exportPriority)) {
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)) {
122
+ function reportMethod(context, sourceCode, node) {
123
+ if (hasStoryAnnotation(sourceCode, node)) {
161
124
  return;
162
125
  }
163
126
  context.report({
164
127
  node,
165
128
  messageId: "missingStory",
166
- fix(fixer) {
167
- const indentLevel = target.loc.start.column;
168
- const indent = " ".repeat(indentLevel);
169
- const insertPos = target.range[0] - indentLevel;
170
- return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}/** @story <story-file>.story.md */\n`);
171
- },
129
+ data: { name: getNodeName(node) },
130
+ suggest: [
131
+ {
132
+ desc: `Add JSDoc @story annotation for function '${getNodeName(node)}', e.g., ${ANNOTATION}`,
133
+ fix: (fixer) => fixer.insertTextBefore(node, `${ANNOTATION}\n `),
134
+ },
135
+ ],
172
136
  });
173
137
  }
174
- exports.default = {
138
+ /**
139
+ * Check if this node is within scope and matches exportPriority
140
+ */
141
+ function shouldProcessNode(node, scope, exportPriority) {
142
+ if (!scope.includes(node.type)) {
143
+ return false;
144
+ }
145
+ const exported = isExportedNode(node);
146
+ if (exportPriority === "exported" && !exported) {
147
+ return false;
148
+ }
149
+ if (exportPriority === "non-exported" && exported) {
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+ const rule = {
175
155
  meta: {
176
156
  type: "problem",
177
157
  docs: {
178
- description: "Require @story annotations on selected functions",
158
+ description: "Require @story annotations on functions",
179
159
  recommended: "error",
180
160
  },
181
- fixable: "code",
161
+ hasSuggestions: true,
182
162
  messages: {
183
163
  missingStory: "Missing @story annotation (REQ-ANNOTATION-REQUIRED)",
184
164
  },
@@ -188,21 +168,10 @@ exports.default = {
188
168
  properties: {
189
169
  scope: {
190
170
  type: "array",
191
- items: {
192
- enum: [
193
- "FunctionDeclaration",
194
- "FunctionExpression",
195
- "ArrowFunctionExpression",
196
- "MethodDefinition",
197
- "TSDeclareFunction",
198
- "TSMethodSignature",
199
- ],
200
- },
171
+ items: { type: "string", enum: DEFAULT_SCOPE },
201
172
  uniqueItems: true,
202
173
  },
203
- exportPriority: {
204
- enum: ["all", "exported", "non-exported"],
205
- },
174
+ exportPriority: { type: "string", enum: EXPORT_PRIORITY_VALUES },
206
175
  },
207
176
  additionalProperties: false,
208
177
  },
@@ -210,39 +179,53 @@ exports.default = {
210
179
  },
211
180
  create(context) {
212
181
  const sourceCode = context.getSourceCode();
213
- const options = context.options[0] || {};
214
- const scope = options.scope || [
215
- "FunctionDeclaration",
216
- "FunctionExpression",
217
- "ArrowFunctionExpression",
218
- "MethodDefinition",
219
- "TSDeclareFunction",
220
- "TSMethodSignature",
221
- ];
222
- const exportPriority = options.exportPriority || "all";
182
+ const opts = context.options[0] ||
183
+ {};
184
+ const scope = opts.scope || DEFAULT_SCOPE;
185
+ const exportPriority = opts.exportPriority || "all";
223
186
  return {
224
187
  FunctionDeclaration(node) {
225
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
188
+ if (!shouldProcessNode(node, scope, exportPriority))
189
+ return;
190
+ let target = node;
191
+ if (node.parent &&
192
+ (node.parent.type === "ExportNamedDeclaration" ||
193
+ node.parent.type === "ExportDefaultDeclaration")) {
194
+ target = node.parent;
195
+ }
196
+ reportMissing(context, sourceCode, node, target);
226
197
  },
227
198
  FunctionExpression(node) {
228
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
199
+ if (!shouldProcessNode(node, scope, exportPriority))
200
+ return;
201
+ if (node.parent && node.parent.type === "MethodDefinition")
202
+ return;
203
+ const target = resolveTargetNode(sourceCode, node);
204
+ reportMissing(context, sourceCode, node, target);
229
205
  },
230
206
  ArrowFunctionExpression(node) {
231
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
207
+ if (!shouldProcessNode(node, scope, exportPriority))
208
+ return;
209
+ const target = resolveTargetNode(sourceCode, node);
210
+ reportMissing(context, sourceCode, node, target);
232
211
  },
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
212
  TSDeclareFunction(node) {
239
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
213
+ if (!shouldProcessNode(node, scope, exportPriority))
214
+ return;
215
+ reportMissing(context, sourceCode, node, node);
240
216
  },
241
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
242
- // @req REQ-FUNCTION-DETECTION - Detect TS-specific function syntax
243
217
  TSMethodSignature(node) {
244
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
218
+ if (!shouldProcessNode(node, scope, exportPriority))
219
+ return;
220
+ const target = resolveTargetNode(sourceCode, node);
221
+ reportMissing(context, sourceCode, node, target);
222
+ },
223
+ MethodDefinition(node) {
224
+ if (!shouldProcessNode(node, scope, exportPriority))
225
+ return;
226
+ reportMethod(context, sourceCode, node);
245
227
  },
246
228
  };
247
229
  },
248
230
  };
231
+ exports.default = rule;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Helper to check @req annotation presence on TS declare functions and method signatures.
3
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
4
+ * @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
5
+ */
6
+ export declare function checkReqAnnotation(context: any, node: any): void;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkReqAnnotation = checkReqAnnotation;
4
+ /**
5
+ * Helper to check @req annotation presence on TS declare functions and method signatures.
6
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
7
+ * @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
8
+ */
9
+ function checkReqAnnotation(context, node) {
10
+ const sourceCode = context.getSourceCode();
11
+ const jsdoc = sourceCode.getJSDocComment(node);
12
+ const leading = node.leadingComments || [];
13
+ const comments = sourceCode.getCommentsBefore(node) || [];
14
+ const all = [...leading, ...comments];
15
+ const hasReq = (jsdoc && jsdoc.value.includes("@req")) ||
16
+ all.some((c) => c.value.includes("@req"));
17
+ if (!hasReq) {
18
+ context.report({
19
+ node,
20
+ messageId: "missingReq",
21
+ fix(fixer) {
22
+ return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
23
+ },
24
+ });
25
+ }
26
+ }
@@ -0,0 +1,46 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Valid branch types for require-branch-annotation rule.
4
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
5
+ * @req REQ-SIGNIFICANCE-CRITERIA - Define criteria for which branches require annotations
6
+ */
7
+ export declare const DEFAULT_BRANCH_TYPES: readonly ["IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"];
8
+ /**
9
+ * Type for branch nodes supported by require-branch-annotation rule.
10
+ */
11
+ export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
12
+ /**
13
+ * Validate branchTypes configuration option and return branch types to enforce,
14
+ * or return an ESLint listener if configuration is invalid.
15
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
16
+ * @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
17
+ */
18
+ export declare function validateBranchTypes(context: Rule.RuleContext): BranchType[] | Rule.RuleListener;
19
+ /**
20
+ * Gather leading comment text for a branch node.
21
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
22
+ * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
23
+ */
24
+ export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
25
+ /**
26
+ * Report missing @story annotation on a branch node.
27
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
28
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
29
+ */
30
+ export declare function reportMissingStory(context: Rule.RuleContext, node: any, indent: string, insertPos: number, storyFixCountRef: {
31
+ count: number;
32
+ }): void;
33
+ /**
34
+ * Report missing @req annotation on a branch node.
35
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
36
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
37
+ */
38
+ export declare function reportMissingReq(context: Rule.RuleContext, node: any, indent: string, insertPos: number, missingStory: boolean): void;
39
+ /**
40
+ * Report missing annotations on a branch node.
41
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
42
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
43
+ */
44
+ export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
45
+ count: number;
46
+ }): void;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_BRANCH_TYPES = void 0;
4
+ exports.validateBranchTypes = validateBranchTypes;
5
+ exports.gatherBranchCommentText = gatherBranchCommentText;
6
+ exports.reportMissingStory = reportMissingStory;
7
+ exports.reportMissingReq = reportMissingReq;
8
+ exports.reportMissingAnnotations = reportMissingAnnotations;
9
+ /**
10
+ * Valid branch types for require-branch-annotation rule.
11
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
12
+ * @req REQ-SIGNIFICANCE-CRITERIA - Define criteria for which branches require annotations
13
+ */
14
+ exports.DEFAULT_BRANCH_TYPES = [
15
+ "IfStatement",
16
+ "SwitchCase",
17
+ "TryStatement",
18
+ "CatchClause",
19
+ "ForStatement",
20
+ "ForOfStatement",
21
+ "ForInStatement",
22
+ "WhileStatement",
23
+ "DoWhileStatement",
24
+ ];
25
+ /**
26
+ * Validate branchTypes configuration option and return branch types to enforce,
27
+ * or return an ESLint listener if configuration is invalid.
28
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
29
+ * @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
30
+ */
31
+ function validateBranchTypes(context) {
32
+ const options = context.options[0] || {};
33
+ if (Array.isArray(options.branchTypes)) {
34
+ const invalidTypes = options.branchTypes.filter((t) => !exports.DEFAULT_BRANCH_TYPES.includes(t));
35
+ if (invalidTypes.length > 0) {
36
+ return {
37
+ Program(node) {
38
+ invalidTypes.forEach((t) => {
39
+ context.report({
40
+ node,
41
+ message: `Value "${t}" should be equal to one of the allowed values: ${exports.DEFAULT_BRANCH_TYPES.join(", ")}`,
42
+ });
43
+ });
44
+ },
45
+ };
46
+ }
47
+ }
48
+ return Array.isArray(options.branchTypes)
49
+ ? options.branchTypes
50
+ : Array.from(exports.DEFAULT_BRANCH_TYPES);
51
+ }
52
+ /**
53
+ * Gather leading comment text for a branch node.
54
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
55
+ * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
56
+ */
57
+ function gatherBranchCommentText(sourceCode, node) {
58
+ if (node.type === "SwitchCase") {
59
+ const lines = sourceCode.lines;
60
+ const startLine = node.loc.start.line;
61
+ let i = startLine - 2;
62
+ const comments = [];
63
+ while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
64
+ comments.unshift(lines[i].trim());
65
+ i--;
66
+ }
67
+ return comments.join(" ");
68
+ }
69
+ const comments = sourceCode.getCommentsBefore(node) || [];
70
+ return comments.map((c) => c.value).join(" ");
71
+ }
72
+ /**
73
+ * Report missing @story annotation on a branch node.
74
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
75
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
76
+ */
77
+ function reportMissingStory(context, node, indent, insertPos, storyFixCountRef) {
78
+ if (storyFixCountRef.count === 0) {
79
+ context.report({
80
+ node,
81
+ messageId: "missingAnnotation",
82
+ data: { missing: "@story" },
83
+ fix: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`),
84
+ });
85
+ storyFixCountRef.count++;
86
+ }
87
+ else {
88
+ context.report({
89
+ node,
90
+ messageId: "missingAnnotation",
91
+ data: { missing: "@story" },
92
+ });
93
+ }
94
+ }
95
+ /**
96
+ * Report missing @req annotation on a branch node.
97
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
98
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
99
+ */
100
+ function reportMissingReq(context, node, indent, insertPos, missingStory) {
101
+ if (!missingStory) {
102
+ context.report({
103
+ node,
104
+ messageId: "missingAnnotation",
105
+ data: { missing: "@req" },
106
+ fix: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`),
107
+ });
108
+ }
109
+ else {
110
+ context.report({
111
+ node,
112
+ messageId: "missingAnnotation",
113
+ data: { missing: "@req" },
114
+ });
115
+ }
116
+ }
117
+ /**
118
+ * Report missing annotations on a branch node.
119
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
120
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
121
+ */
122
+ function reportMissingAnnotations(context, node, storyFixCountRef) {
123
+ const sourceCode = context.getSourceCode();
124
+ const text = gatherBranchCommentText(sourceCode, node);
125
+ const missingStory = !/@story\b/.test(text);
126
+ const missingReq = !/@req\b/.test(text);
127
+ const indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
128
+ const insertPos = sourceCode.getIndexFromLoc({
129
+ line: node.loc.start.line,
130
+ column: 0,
131
+ });
132
+ const actions = [
133
+ {
134
+ missing: missingStory,
135
+ fn: reportMissingStory,
136
+ args: [context, node, indent, insertPos, storyFixCountRef],
137
+ },
138
+ {
139
+ missing: missingReq,
140
+ fn: reportMissingReq,
141
+ args: [context, node, indent, insertPos, missingStory],
142
+ },
143
+ ];
144
+ actions.forEach(({ missing, fn, args }) => missing && fn(...args));
145
+ }
@@ -76,10 +76,8 @@ function baz() {}`,
76
76
  });
77
77
  return result;
78
78
  }
79
- tests.forEach((testCase) => {
80
- it(`[REQ-PLUGIN-STRUCTURE] ${testCase.name}`, () => {
81
- const result = runEslint(testCase.code, testCase.rule);
82
- expect(result.status).toBe(testCase.expectedStatus);
83
- });
79
+ it.each(tests)("[REQ-PLUGIN-STRUCTURE] $name", ({ code, rule, expectedStatus }) => {
80
+ const result = runEslint(code, rule);
81
+ expect(result.status).toBe(expectedStatus);
84
82
  });
85
83
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
5
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
6
+ * @req REQ-MAINT-SAFE - Ensure all maintenance tools are exported correctly
7
+ */
8
+ const maintenance_1 = require("../../src/maintenance");
9
+ describe("Maintenance Tools Index Exports (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
10
+ it("[REQ-MAINT-DETECT] should export detectStaleAnnotations as a function", () => {
11
+ expect(typeof maintenance_1.detectStaleAnnotations).toBe("function");
12
+ });
13
+ it("[REQ-MAINT-UPDATE] should export updateAnnotationReferences as a function", () => {
14
+ expect(typeof maintenance_1.updateAnnotationReferences).toBe("function");
15
+ });
16
+ it("[REQ-MAINT-BATCH] should export batchUpdateAnnotations as a function", () => {
17
+ expect(typeof maintenance_1.batchUpdateAnnotations).toBe("function");
18
+ });
19
+ it("[REQ-MAINT-VERIFY] should export verifyAnnotations as a function", () => {
20
+ expect(typeof maintenance_1.verifyAnnotations).toBe("function");
21
+ });
22
+ it("[REQ-MAINT-REPORT] should export generateMaintenanceReport as a function", () => {
23
+ expect(typeof maintenance_1.generateMaintenanceReport).toBe("function");
24
+ });
25
+ });