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.
@@ -1,198 +1,13 @@
1
1
  "use strict";
2
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"];
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
- * Determine if a node is in an export declaration
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 for require-story-annotation.
38
+ * Create the rule visitor functions.
39
+ *
224
40
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
225
- * @req REQ-CREATE-HOOK - Provide create(context) hook for rule behavior
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
- return {
234
- FunctionDeclaration(node) {
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);
244
- },
245
- FunctionExpression(node) {
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);
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
- const invalidTypes = options.branchTypes.filter((t) => !exports.DEFAULT_BRANCH_TYPES.includes(t));
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
- return {
38
- Program(node) {
39
- invalidTypes.forEach((t) => {
40
- context.report({
41
- node,
42
- message: `Value "${t}" should be equal to one of the allowed values: ${exports.DEFAULT_BRANCH_TYPES.join(", ")}`,
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
- return comments.map((c) => c.value).join(" ");
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: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`),
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: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`),
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
- actions.forEach(({ missing, fn, args }) => missing && fn(...args));
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
- fs.chmodSync(dir, 0o000);
91
- expect(() => (0, detect_1.detectStaleAnnotations)(tmpDir2)).toThrow();
92
- // Restore permissions
93
- fs.chmodSync(dir, 0o700);
94
- // Cleanup temporary directory
95
- fs.rmSync(tmpDir2, { recursive: true, force: true });
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 {};