eslint-plugin-traceability 1.6.4 → 1.7.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.
Files changed (42) hide show
  1. package/README.md +38 -1
  2. package/lib/src/index.d.ts +28 -25
  3. package/lib/src/index.js +49 -31
  4. package/lib/src/maintenance/cli.d.ts +12 -0
  5. package/lib/src/maintenance/cli.js +279 -0
  6. package/lib/src/maintenance/detect.js +30 -15
  7. package/lib/src/maintenance/update.js +42 -34
  8. package/lib/src/maintenance/utils.js +30 -30
  9. package/lib/src/rules/helpers/require-story-io.js +51 -15
  10. package/lib/src/rules/helpers/require-story-visitors.js +5 -16
  11. package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
  12. package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
  13. package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
  14. package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
  15. package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
  16. package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
  17. package/lib/src/rules/require-story-annotation.js +9 -14
  18. package/lib/src/rules/valid-annotation-format.js +168 -180
  19. package/lib/src/rules/valid-req-reference.js +139 -29
  20. package/lib/src/rules/valid-story-reference.d.ts +7 -0
  21. package/lib/src/rules/valid-story-reference.js +38 -80
  22. package/lib/src/utils/annotation-checker.js +2 -145
  23. package/lib/src/utils/branch-annotation-helpers.js +12 -3
  24. package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
  25. package/lib/src/utils/reqAnnotationDetection.js +152 -0
  26. package/lib/tests/maintenance/cli.test.d.ts +1 -0
  27. package/lib/tests/maintenance/cli.test.js +172 -0
  28. package/lib/tests/maintenance/detect-isolated.test.js +68 -1
  29. package/lib/tests/maintenance/report.test.js +2 -2
  30. package/lib/tests/rules/require-branch-annotation.test.js +3 -2
  31. package/lib/tests/rules/require-req-annotation.test.js +57 -68
  32. package/lib/tests/rules/require-story-annotation.test.js +13 -28
  33. package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
  34. package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
  35. package/lib/tests/rules/valid-annotation-format.test.js +328 -51
  36. package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
  37. package/lib/tests/utils/annotation-checker.test.js +24 -17
  38. package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
  39. package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
  40. package/lib/tests/utils/ts-language-options.d.ts +22 -0
  41. package/lib/tests/utils/ts-language-options.js +27 -0
  42. package/package.json +12 -3
@@ -31,28 +31,20 @@ function extractStoryPath(comment) {
31
31
  return null;
32
32
  }
33
33
  /**
34
- * Validate a @req annotation line against the extracted story content.
35
- * Performs path validation, file reading, caching, and requirement existence checks.
34
+ * Validate and resolve the referenced story path.
35
+ * Performs traversal/absolute checks and resolves to a disk path.
36
36
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
37
37
  * @req REQ-DEEP-PATH - Validate and resolve referenced story file paths
38
- * @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
39
- * @req REQ-DEEP-MATCH - Verify that a referenced requirement ID exists in the story
40
- * @req REQ-DEEP-PARSE - Parse story file contents to extract requirement identifiers
41
38
  */
42
- function validateReqLine(opts) {
43
- const { comment, context, line, storyPath, cwd, reqCache } = opts;
44
- const parts = line.split(/\s+/);
45
- const reqId = parts[1];
46
- if (!reqId || !storyPath) {
47
- return;
48
- }
39
+ function validateAndResolveStoryPath(opts) {
40
+ const { comment, context, storyPath, cwd } = opts;
49
41
  if (storyPath.includes("..") || path_1.default.isAbsolute(storyPath)) {
50
42
  context.report({
51
43
  node: comment,
52
44
  messageId: "invalidPath",
53
45
  data: { storyPath },
54
46
  });
55
- return;
47
+ return null;
56
48
  }
57
49
  const resolvedStoryPath = path_1.default.resolve(cwd, storyPath);
58
50
  if (!resolvedStoryPath.startsWith(cwd + path_1.default.sep) &&
@@ -62,8 +54,19 @@ function validateReqLine(opts) {
62
54
  messageId: "invalidPath",
63
55
  data: { storyPath },
64
56
  });
65
- return;
57
+ return null;
66
58
  }
59
+ return resolvedStoryPath;
60
+ }
61
+ /**
62
+ * Load and cache requirement IDs from a story file.
63
+ * Reads the story file, extracts requirement IDs, and updates the cache.
64
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
65
+ * @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
66
+ * @req REQ-DEEP-PARSE - Parse story file contents to extract requirement identifiers
67
+ */
68
+ function loadAndCacheRequirements(opts) {
69
+ const { resolvedStoryPath, reqCache } = opts;
67
70
  if (!reqCache.has(resolvedStoryPath)) {
68
71
  try {
69
72
  const content = fs_1.default.readFileSync(resolvedStoryPath, "utf8");
@@ -79,7 +82,15 @@ function validateReqLine(opts) {
79
82
  reqCache.set(resolvedStoryPath, new Set());
80
83
  }
81
84
  }
82
- const reqSet = reqCache.get(resolvedStoryPath);
85
+ return reqCache.get(resolvedStoryPath);
86
+ }
87
+ /**
88
+ * Perform the final requirement existence check and report if missing.
89
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
90
+ * @req REQ-DEEP-MATCH - Verify that a referenced requirement ID exists in the story
91
+ */
92
+ function checkRequirementExists(opts) {
93
+ const { comment, context, reqId, storyPath, reqSet } = opts;
83
94
  if (!reqSet.has(reqId)) {
84
95
  context.report({
85
96
  node: comment,
@@ -88,6 +99,71 @@ function validateReqLine(opts) {
88
99
  });
89
100
  }
90
101
  }
102
+ /**
103
+ * Extract requirement ID from a @req line.
104
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
105
+ * @req REQ-DEEP-PARSE - Parse annotation lines to extract requirement IDs
106
+ */
107
+ function extractReqIdFromLine(line) {
108
+ const parts = line.split(/\s+/);
109
+ return parts[1];
110
+ }
111
+ /**
112
+ * Resolve story path and load requirements set for validation.
113
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
114
+ * @req REQ-DEEP-PATH - Validate and resolve referenced story file paths
115
+ * @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
116
+ */
117
+ function resolveStoryAndRequirements(opts) {
118
+ const { comment, context, storyPath, cwd, reqCache } = opts;
119
+ const resolvedStoryPath = validateAndResolveStoryPath({
120
+ comment,
121
+ context,
122
+ storyPath,
123
+ cwd,
124
+ });
125
+ if (!resolvedStoryPath) {
126
+ return { resolvedStoryPath: null, reqSet: null };
127
+ }
128
+ const reqSet = loadAndCacheRequirements({
129
+ resolvedStoryPath,
130
+ reqCache,
131
+ });
132
+ return { resolvedStoryPath, reqSet };
133
+ }
134
+ /**
135
+ * Validate a @req annotation line against the extracted story content.
136
+ * Performs path validation, file reading, caching, and requirement existence checks.
137
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
138
+ * @req REQ-DEEP-PATH - Validate and resolve referenced story file paths
139
+ * @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
140
+ * @req REQ-DEEP-MATCH - Verify that a referenced requirement ID exists in the story
141
+ * @req REQ-DEEP-PARSE - Parse story file contents to extract requirement identifiers
142
+ */
143
+ function validateReqLine(opts) {
144
+ const { comment, context, line, storyPath, cwd, reqCache } = opts;
145
+ const reqId = extractReqIdFromLine(line);
146
+ if (!reqId || !storyPath) {
147
+ return;
148
+ }
149
+ const { reqSet } = resolveStoryAndRequirements({
150
+ comment,
151
+ context,
152
+ storyPath,
153
+ cwd,
154
+ reqCache,
155
+ });
156
+ if (!reqSet) {
157
+ return;
158
+ }
159
+ checkRequirementExists({
160
+ comment,
161
+ context,
162
+ reqId,
163
+ storyPath,
164
+ reqSet,
165
+ });
166
+ }
91
167
  /**
92
168
  * Handle a single annotation line for story or requirement metadata.
93
169
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
@@ -107,15 +183,14 @@ function handleAnnotationLine(opts) {
107
183
  return storyPath;
108
184
  }
109
185
  /**
110
- * Handle JSDoc story and req annotations for a single comment block.
186
+ * Iterate over all raw lines in a comment and update storyPath as needed.
111
187
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
112
188
  * @req REQ-DEEP-PARSE - Iterate comment lines to process @story/@req annotations
113
189
  * @req REQ-DEEP-MATCH - Coordinate annotation handling across a comment block
114
- * @req REQ-DEEP-CACHE - Maintain and reuse discovered story path across comments
115
190
  */
116
- function handleComment(opts) {
117
- const { comment, context, cwd, reqCache, rawStoryPath } = opts;
118
- let storyPath = rawStoryPath;
191
+ function processCommentLines(opts) {
192
+ const { comment, context, cwd, reqCache, initialStoryPath } = opts;
193
+ let storyPath = initialStoryPath;
119
194
  const rawLines = comment.value.split(/\r?\n/);
120
195
  for (const rawLine of rawLines) {
121
196
  const line = rawLine.trim().replace(/^\*+\s*/, "");
@@ -130,6 +205,44 @@ function handleComment(opts) {
130
205
  }
131
206
  return storyPath;
132
207
  }
208
+ /**
209
+ * Handle JSDoc story and req annotations for a single comment block.
210
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
211
+ * @req REQ-DEEP-PARSE - Iterate comment lines to process @story/@req annotations
212
+ * @req REQ-DEEP-MATCH - Coordinate annotation handling across a comment block
213
+ * @req REQ-DEEP-CACHE - Maintain and reuse discovered story path across comments
214
+ */
215
+ function handleComment(opts) {
216
+ const { comment, context, cwd, reqCache, rawStoryPath } = opts;
217
+ return processCommentLines({
218
+ comment,
219
+ context,
220
+ cwd,
221
+ reqCache,
222
+ initialStoryPath: rawStoryPath,
223
+ });
224
+ }
225
+ /**
226
+ * Get all comments from source and drive comment-level handling.
227
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
228
+ * @req REQ-DEEP-PARSE - Collect all comments from the source code
229
+ * @req REQ-DEEP-MATCH - Drive comment-level handling for traceability checks
230
+ * @req REQ-DEEP-CACHE - Reuse story path and requirement cache across comments
231
+ */
232
+ function processAllComments(opts) {
233
+ const { sourceCode, context, cwd, reqCache } = opts;
234
+ let rawStoryPath = opts.initialStoryPath;
235
+ const comments = sourceCode.getAllComments() || [];
236
+ comments.forEach((comment) => {
237
+ rawStoryPath = handleComment({
238
+ comment,
239
+ context,
240
+ cwd,
241
+ reqCache,
242
+ rawStoryPath,
243
+ });
244
+ });
245
+ }
133
246
  /**
134
247
  * Create a Program listener that iterates comments and validates annotations.
135
248
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
@@ -150,15 +263,12 @@ function programListener(context) {
150
263
  * @req REQ-DEEP-PATH - Ensure validation respects project-relative paths
151
264
  */
152
265
  return function Program() {
153
- const comments = sourceCode.getAllComments() || [];
154
- comments.forEach((comment) => {
155
- rawStoryPath = handleComment({
156
- comment,
157
- context,
158
- cwd,
159
- reqCache,
160
- rawStoryPath,
161
- });
266
+ processAllComments({
267
+ sourceCode,
268
+ context,
269
+ cwd,
270
+ reqCache,
271
+ initialStoryPath: rawStoryPath,
162
272
  });
163
273
  };
164
274
  }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Rule to validate @story annotation references refer to existing story files
3
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
4
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
5
+ * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
6
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
7
+ */
1
8
  import type { Rule } from "eslint";
2
9
  declare const _default: Rule.RuleModule;
3
10
  export default _default;
@@ -1,19 +1,25 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- /* eslint-env node */
3
+ const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
4
+ const valid_story_reference_helpers_1 = require("./helpers/valid-story-reference-helpers");
5
+ const defaultStoryDirs = ["docs/stories", "stories"];
7
6
  /**
8
- * Rule to validate @story annotation references refer to existing story files
7
+ * Shared helper to report an invalid story path. Centralizes the
8
+ * `invalidPath` diagnostic so callers don't repeat the same
9
+ * `context.report` shape.
10
+ *
9
11
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
10
- * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
11
- * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
12
- * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
12
+ * @req REQ-PROJECT-BOUNDARY - Ensure resolved candidate paths remain within the project root
13
+ * @req REQ-ERROR-CONSISTENCY - Maintain a consistent template for invalid path diagnostics across rules
13
14
  */
14
- const path_1 = __importDefault(require("path"));
15
- const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
16
- const defaultStoryDirs = ["docs/stories", "stories"];
15
+ function reportInvalidPath(opts) {
16
+ const { storyPath, commentNode, context } = opts;
17
+ context.report({
18
+ node: commentNode,
19
+ messageId: "invalidPath",
20
+ data: { path: storyPath },
21
+ });
22
+ }
17
23
  /**
18
24
  * Extract the story path from the annotation line and delegate validation.
19
25
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
@@ -35,28 +41,6 @@ function validateStoryPath(opts) {
35
41
  requireExt,
36
42
  });
37
43
  }
38
- /**
39
- * Analyze candidate paths against the project boundary, returning whether any
40
- * are within the project and whether any are outside.
41
- *
42
- * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
43
- * @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
44
- * @req REQ-CONFIGURABLE-PATHS - Respect configured storyDirectories while enforcing project boundaries
45
- */
46
- function analyzeCandidateBoundaries(candidates, cwd) {
47
- let hasInProjectCandidate = false;
48
- let hasOutOfProjectCandidate = false;
49
- for (const candidate of candidates) {
50
- const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(candidate, cwd);
51
- if (boundary.isWithinProject) {
52
- hasInProjectCandidate = true;
53
- }
54
- else {
55
- hasOutOfProjectCandidate = true;
56
- }
57
- }
58
- return { hasInProjectCandidate, hasOutOfProjectCandidate };
59
- }
60
44
  /**
61
45
  * Handle existence status and report appropriate diagnostics for missing
62
46
  * or filesystem-error conditions, assuming project-boundary checks have
@@ -116,29 +100,17 @@ function reportExistenceProblems(opts) {
116
100
  const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
117
101
  const existenceResult = result.existence;
118
102
  const candidates = result.candidates || [];
119
- if (candidates.length > 0) {
120
- const { hasInProjectCandidate, hasOutOfProjectCandidate } = analyzeCandidateBoundaries(candidates, cwd);
121
- if (hasOutOfProjectCandidate && !hasInProjectCandidate) {
122
- context.report({
123
- node: commentNode,
124
- messageId: "invalidPath",
125
- data: { path: storyPath },
126
- });
127
- return;
128
- }
129
- }
130
- if (existenceResult &&
131
- existenceResult.status === "exists" &&
132
- existenceResult.matchedPath) {
133
- const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(existenceResult.matchedPath, cwd);
134
- if (!boundary.isWithinProject) {
135
- context.report({
136
- node: commentNode,
137
- messageId: "invalidPath",
138
- data: { path: storyPath },
139
- });
140
- return;
141
- }
103
+ const invalidByBoundary = (0, valid_story_reference_helpers_1.handleProjectBoundaryForExistence)({
104
+ storyPath,
105
+ commentNode,
106
+ context,
107
+ cwd,
108
+ candidates,
109
+ existenceResult,
110
+ reportInvalidPath,
111
+ });
112
+ if (invalidByBoundary) {
113
+ return;
142
114
  }
143
115
  reportExistenceStatus(existenceResult, storyPath, commentNode, context);
144
116
  }
@@ -156,30 +128,16 @@ function reportExistenceProblems(opts) {
156
128
  */
157
129
  function processStoryPath(opts) {
158
130
  const { storyPath, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
159
- // Absolute path check
160
- if (path_1.default.isAbsolute(storyPath)) {
161
- if (!allowAbsolute) {
162
- context.report({
163
- node: commentNode,
164
- messageId: "invalidPath",
165
- data: { path: storyPath },
166
- });
167
- return;
168
- }
169
- // When absolute paths are allowed, we still enforce extension and
170
- // project-boundary checks below via the existence phase.
171
- }
172
- // Path traversal check
173
- if ((0, storyReferenceUtils_1.containsPathTraversal)(storyPath)) {
174
- const full = path_1.default.resolve(cwd, path_1.default.normalize(storyPath));
175
- if (!full.startsWith(cwd + path_1.default.sep)) {
176
- context.report({
177
- node: commentNode,
178
- messageId: "invalidPath",
179
- data: { path: storyPath },
180
- });
181
- return;
182
- }
131
+ const securityOk = (0, valid_story_reference_helpers_1.performSecurityValidations)({
132
+ storyPath,
133
+ commentNode,
134
+ context,
135
+ cwd,
136
+ allowAbsolute,
137
+ reportInvalidPath,
138
+ });
139
+ if (!securityOk) {
140
+ return;
183
141
  }
184
142
  // Extension check
185
143
  if (requireExt && !(0, storyReferenceUtils_1.hasValidExtension)(storyPath)) {
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkReqAnnotation = checkReqAnnotation;
4
4
  const require_story_utils_1 = require("../rules/helpers/require-story-utils");
5
- const require_story_io_1 = require("../rules/helpers/require-story-io");
5
+ const reqAnnotationDetection_1 = require("./reqAnnotationDetection");
6
6
  /**
7
7
  * Helper to retrieve the JSDoc comment for a node.
8
8
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -35,149 +35,6 @@ function getCommentsBefore(sourceCode, node) {
35
35
  function combineComments(leading, before) {
36
36
  return [...leading, ...before];
37
37
  }
38
- /**
39
- * Predicate helper to check whether a comment contains a @req annotation.
40
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
41
- * @req REQ-ANNOTATION-CHECK-COMMENT - Detect @req tag inside a comment
42
- */
43
- function commentContainsReq(c) {
44
- return c && typeof c.value === "string" && c.value.includes("@req");
45
- }
46
- /**
47
- * Line-based helper adapted from linesBeforeHasStory to detect @req.
48
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
49
- * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in preceding source lines
50
- */
51
- function linesBeforeHasReq(sourceCode, node) {
52
- const lines = sourceCode && sourceCode.lines;
53
- const startLine = node && node.loc && typeof node.loc.start?.line === "number"
54
- ? node.loc.start.line
55
- : null;
56
- // Guard against missing or malformed source/loc information before scanning.
57
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
58
- // @req REQ-ANNOTATION-REQ-DETECTION - Avoid false positives when sourceCode/loc is incomplete
59
- if (!Array.isArray(lines) || typeof startLine !== "number") {
60
- return false;
61
- }
62
- const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
63
- const to = Math.max(0, startLine - 1);
64
- // Scan each physical line in the configured lookback window for an @req marker.
65
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
66
- // @req REQ-ANNOTATION-REQ-DETECTION - Search preceding lines for @req text
67
- for (let i = from; i < to; i++) {
68
- const text = lines[i];
69
- // When a line contains @req we treat the function as already annotated.
70
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
71
- // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in raw source lines
72
- if (typeof text === "string" && text.includes("@req")) {
73
- return true;
74
- }
75
- }
76
- return false;
77
- }
78
- /**
79
- * Parent-chain helper adapted from parentChainHasStory to detect @req.
80
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
81
- * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in parent-chain comments
82
- */
83
- function parentChainHasReq(sourceCode, node) {
84
- let p = node && node.parent;
85
- // Walk up the parent chain and inspect comments attached to each ancestor.
86
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
87
- // @req REQ-ANNOTATION-REQ-DETECTION - Traverse parent nodes when local comments are absent
88
- while (p) {
89
- const pComments = typeof sourceCode?.getCommentsBefore === "function"
90
- ? sourceCode.getCommentsBefore(p) || []
91
- : [];
92
- // Look for @req in comments immediately preceding each parent node.
93
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
94
- // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent comments
95
- if (Array.isArray(pComments) && pComments.some(commentContainsReq)) {
96
- return true;
97
- }
98
- const pLeading = p.leadingComments || [];
99
- // Also inspect leadingComments attached directly to the parent node.
100
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
101
- // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent leadingComments
102
- if (Array.isArray(pLeading) && pLeading.some(commentContainsReq)) {
103
- return true;
104
- }
105
- p = p.parent;
106
- }
107
- return false;
108
- }
109
- /**
110
- * Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
111
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
112
- * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in fallback text window before node
113
- */
114
- function fallbackTextBeforeHasReq(sourceCode, node) {
115
- // Guard against unsupported sourceCode or nodes without a usable range.
116
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
117
- // @req REQ-ANNOTATION-REQ-DETECTION - Ensure we only inspect text when range information is available
118
- if (typeof sourceCode?.getText !== "function" ||
119
- !Array.isArray((node && node.range) || [])) {
120
- return false;
121
- }
122
- const range = node.range;
123
- // Guard when the node range cannot provide a numeric start index.
124
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
125
- // @req REQ-ANNOTATION-REQ-DETECTION - Avoid scanning when range start is not a number
126
- if (!Array.isArray(range) || typeof range[0] !== "number") {
127
- return false;
128
- }
129
- try {
130
- const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
131
- const textBefore = sourceCode.getText().slice(start, range[0]);
132
- // Detect @req in the bounded text window immediately preceding the node.
133
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
134
- // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in fallback text window
135
- if (typeof textBefore === "string" && textBefore.includes("@req")) {
136
- return true;
137
- }
138
- }
139
- catch {
140
- // Swallow detection errors to avoid breaking lint runs due to malformed source.
141
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
142
- // @req REQ-ANNOTATION-REQ-DETECTION - Treat IO/detection failures as "no annotation" instead of throwing
143
- /* noop */
144
- }
145
- return false;
146
- }
147
- /**
148
- * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
149
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
150
- * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
151
- */
152
- function hasReqAnnotation(jsdoc, comments, context, node) {
153
- try {
154
- const sourceCode = context && typeof context.getSourceCode === "function"
155
- ? context.getSourceCode()
156
- : undefined;
157
- // Prefer robust, location-based heuristics when sourceCode and node are available.
158
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
159
- // @req REQ-ANNOTATION-REQ-DETECTION - Use multiple heuristics to detect @req markers around the node
160
- if (sourceCode && node) {
161
- if (linesBeforeHasReq(sourceCode, node) ||
162
- parentChainHasReq(sourceCode, node) ||
163
- fallbackTextBeforeHasReq(sourceCode, node)) {
164
- return true;
165
- }
166
- }
167
- }
168
- catch {
169
- // Swallow detection errors and fall through to simple checks.
170
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
171
- // @req REQ-ANNOTATION-REQ-DETECTION - Fail gracefully when advanced detection heuristics throw
172
- }
173
- // BRANCH @req detection on JSDoc or comments
174
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
175
- // @req REQ-ANNOTATION-REQ-DETECTION
176
- return ((jsdoc &&
177
- typeof jsdoc.value === "string" &&
178
- jsdoc.value.includes("@req")) ||
179
- comments.some(commentContainsReq));
180
- }
181
38
  /**
182
39
  * Determine the most appropriate node to attach an inserted JSDoc to.
183
40
  * Prefers outer function-like constructs such as methods, variable declarators,
@@ -283,7 +140,7 @@ function checkReqAnnotation(context, node, options) {
283
140
  const leading = getLeadingComments(node);
284
141
  const comments = getCommentsBefore(sourceCode, node);
285
142
  const all = combineComments(leading, comments);
286
- const hasReq = hasReqAnnotation(jsdoc, all, context, node);
143
+ const hasReq = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, all, context, node);
287
144
  // BRANCH when a @req annotation is missing and must be reported
288
145
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
289
146
  // @req REQ-ANNOTATION-REQ-DETECTION
@@ -187,12 +187,11 @@ function reportMissingReq(context, node, options) {
187
187
  }
188
188
  }
189
189
  /**
190
- * Report missing annotations on a branch node.
190
+ * Compute annotation-related metadata for a branch node.
191
191
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
192
192
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
193
193
  */
194
- function reportMissingAnnotations(context, node, storyFixCountRef) {
195
- const sourceCode = context.getSourceCode();
194
+ function getBranchAnnotationInfo(sourceCode, node) {
196
195
  const text = gatherBranchCommentText(sourceCode, node);
197
196
  const missingStory = !/@story\b/.test(text);
198
197
  const missingReq = !/@req\b/.test(text);
@@ -201,6 +200,16 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
201
200
  line: node.loc.start.line,
202
201
  column: 0,
203
202
  });
203
+ return { missingStory, missingReq, indent, insertPos };
204
+ }
205
+ /**
206
+ * Report missing annotations on a branch node.
207
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
208
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
209
+ */
210
+ function reportMissingAnnotations(context, node, storyFixCountRef) {
211
+ const sourceCode = context.getSourceCode();
212
+ const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node);
204
213
  const actions = [
205
214
  {
206
215
  missing: missingStory,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
3
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
4
+ * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
5
+ */
6
+ export declare function hasReqAnnotation(jsdoc: any, comments: any[], context?: any, node?: any): boolean;