eslint-plugin-traceability 1.14.0 → 1.15.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 (33) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/lib/src/index.js +2 -0
  3. package/lib/src/rules/helpers/require-story-core.js +1 -0
  4. package/lib/src/rules/helpers/require-story-helpers.d.ts +4 -0
  5. package/lib/src/rules/helpers/require-story-helpers.js +92 -3
  6. package/lib/src/rules/helpers/require-test-traceability-helpers.js +24 -0
  7. package/lib/src/rules/helpers/valid-annotation-options.d.ts +32 -1
  8. package/lib/src/rules/helpers/valid-annotation-options.js +144 -1
  9. package/lib/src/rules/helpers/valid-implements-utils.js +13 -5
  10. package/lib/src/rules/no-redundant-annotation.js +12 -0
  11. package/lib/src/rules/prefer-implements-annotation.js +39 -0
  12. package/lib/src/rules/require-branch-annotation.js +73 -1
  13. package/lib/src/rules/require-test-traceability.js +8 -0
  14. package/lib/src/rules/require-traceability.d.ts +19 -0
  15. package/lib/src/rules/require-traceability.js +73 -0
  16. package/lib/src/rules/valid-req-reference.js +4 -0
  17. package/lib/src/rules/valid-story-reference.d.ts +9 -0
  18. package/lib/src/rules/valid-story-reference.js +9 -0
  19. package/lib/src/utils/branch-annotation-helpers.d.ts +12 -10
  20. package/lib/src/utils/branch-annotation-helpers.js +31 -140
  21. package/lib/src/utils/branch-annotation-loop-helpers.d.ts +9 -0
  22. package/lib/src/utils/branch-annotation-loop-helpers.js +36 -0
  23. package/lib/src/utils/branch-annotation-report-helpers.d.ts +11 -0
  24. package/lib/src/utils/branch-annotation-report-helpers.js +111 -0
  25. package/lib/tests/config/flat-config-presets-integration.test.js +2 -0
  26. package/lib/tests/integration/dogfooding-validation.test.js +5 -2
  27. package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
  28. package/lib/tests/rules/require-branch-annotation.test.js +88 -19
  29. package/lib/tests/rules/require-story-annotation.test.js +56 -8
  30. package/lib/tests/utils/temp-dir-helpers.d.ts +6 -1
  31. package/lib/tests/utils/temp-dir-helpers.js +2 -1
  32. package/package.json +1 -1
  33. package/user-docs/api-reference.md +9 -6
@@ -175,6 +175,13 @@ function analyzeComment(comment) {
175
175
  });
176
176
  return { hasStory, hasReq, hasImplements, storyPaths };
177
177
  }
178
+ /**
179
+ * Check whether a given set of story paths represents multiple story/req
180
+ * blocks within the same comment, which cannot be safely auto-migrated.
181
+ *
182
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
183
+ * @req REQ-MIGRATE-INLINE
184
+ */
178
185
  function hasMultipleStories(storyPaths) {
179
186
  // @req REQ-MULTI-STORY-DETECT - Use named threshold constant instead of a magic number
180
187
  return storyPaths.size > MULTI_STORY_THRESHOLD;
@@ -215,10 +222,26 @@ function processBlockComment(comment, context) {
215
222
  fix: fix ?? undefined,
216
223
  });
217
224
  }
225
+ /**
226
+ * Extract the leading whitespace and `//` prefix from a line comment's full
227
+ * source text so that new inline annotations can be inserted with matching
228
+ * indentation and formatting.
229
+ *
230
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
231
+ * @req REQ-MIGRATE-INLINE
232
+ */
218
233
  function getLinePrefixFromText(fullText) {
219
234
  const match = fullText.match(/^(\s*\/\/\s*)/);
220
235
  return match ? match[1] : "";
221
236
  }
237
+ /**
238
+ * Attempt to construct an inline auto-fix that replaces a contiguous
239
+ * sequence of `@story` and `@req` line comments with a single `@supports`
240
+ * annotation while preserving the original comment prefix.
241
+ *
242
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
243
+ * @req REQ-MIGRATE-INLINE
244
+ */
222
245
  function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
223
246
  const sourceCode = context.getSourceCode();
224
247
  const storyComment = comments[storyIndex];
@@ -255,6 +278,14 @@ function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
255
278
  const end = comments[reqIndices[reqIndices.length - 1]].range[1];
256
279
  return (fixer) => fixer.replaceTextRange([start, end], implLine);
257
280
  }
281
+ /**
282
+ * Coordinate detection and optional migration of a single inline `@story`
283
+ * comment and its following `@req` comments, reporting diagnostics and
284
+ * scheduling auto-fixes where safe.
285
+ *
286
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
287
+ * @req REQ-MIGRATE-INLINE
288
+ */
258
289
  function handleInlineStorySequence(context, group, startIndex) {
259
290
  const n = group.length;
260
291
  const current = group[startIndex];
@@ -304,6 +335,14 @@ function handleInlineStorySequence(context, group, startIndex) {
304
335
  }
305
336
  return reqIndices[reqIndices.length - 1] + 1;
306
337
  }
338
+ /**
339
+ * Process a contiguous group of inline line comments, identifying legacy
340
+ * `@story`/`@req` sequences and scheduling the corresponding diagnostics
341
+ * and potential auto-fixes for migration to `@supports`.
342
+ *
343
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
344
+ * @req REQ-MIGRATE-INLINE
345
+ */
307
346
  function processInlineGroup(context, group) {
308
347
  if (group.length === 0)
309
348
  return;
@@ -1,6 +1,74 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const branch_annotation_helpers_1 = require("../utils/branch-annotation-helpers");
4
+ /**
5
+ * @supports Switch case node detection for fall-through handling
6
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
7
+ * @req REQ-SWITCH-CASE-ANNOTATION
8
+ * @req REQ-SWITCH-DEFAULT-REQUIRED
9
+ * @req REQ-SWITCH-FALLTHROUGH
10
+ */
11
+ function isSwitchCaseNode(node) {
12
+ return (!!node && typeof node === "object" && node.type === "SwitchCase");
13
+ }
14
+ /**
15
+ * Sentinel index value used when a SwitchCase is not found in its parent's cases array.
16
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SWITCH-FALLTHROUGH
17
+ */
18
+ const INVALID_INDEX = -1;
19
+ /**
20
+ * Determine whether a SwitchCase is an intermediate fall-through label
21
+ * that should not require its own annotation.
22
+ *
23
+ * An intermediate fall-through case:
24
+ * - Has an empty consequent array
25
+ * - Has a following SwitchCase sibling in the same SwitchStatement
26
+ * - That following sibling has a non-empty consequent array
27
+ *
28
+ * @supports Switch fall-through behavior for branch annotations
29
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
30
+ * @req REQ-SWITCH-CASE-ANNOTATION
31
+ * @req REQ-SWITCH-DEFAULT-REQUIRED
32
+ * @req REQ-SWITCH-FALLTHROUGH
33
+ */
34
+ function isFallthroughIntermediateCase(node) {
35
+ if (!isSwitchCaseNode(node))
36
+ return false;
37
+ // Default cases must always be annotated when they represent a branch.
38
+ if (node.test == null) {
39
+ return false;
40
+ }
41
+ if (!Array.isArray(node.consequent) || node.consequent.length > 0) {
42
+ return false;
43
+ }
44
+ const parent = node.parent;
45
+ if (!parent ||
46
+ parent.type !== "SwitchStatement" ||
47
+ !Array.isArray(parent.cases)) {
48
+ return false;
49
+ }
50
+ const cases = parent.cases;
51
+ const index = cases.indexOf(node);
52
+ if (index === INVALID_INDEX) {
53
+ return false;
54
+ }
55
+ // Walk forward from this case until we either find a case with a non-empty
56
+ // consequent (shared body) or run out of cases. All empty cases in this
57
+ // prefix are treated as intermediate labels that participate in fall-through
58
+ // but do not themselves require annotations. The last case with the shared
59
+ // body remains subject to annotation requirements.
60
+ let j = index;
61
+ while (j < cases.length &&
62
+ (!Array.isArray(cases[j].consequent) || cases[j].consequent.length === 0)) {
63
+ j++;
64
+ }
65
+ if (j >= cases.length) {
66
+ // No later case with a body; treat this as an independent branch that
67
+ // should be annotated when appropriate.
68
+ return false;
69
+ }
70
+ return true;
71
+ }
4
72
  /**
5
73
  * ESLint rule definition for require-branch-annotation.
6
74
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -66,7 +134,11 @@ const rule = {
66
134
  * @req REQ-CONFIGURABLE-SCOPE
67
135
  */
68
136
  handlers[type] = function branchHandler(node) {
69
- if (type === "SwitchCase" && node.test == null) {
137
+ if (type === "SwitchCase" &&
138
+ isSwitchCaseNode(node) &&
139
+ isFallthroughIntermediateCase(node)) {
140
+ // Skip intermediate fall-through labels; only the last case before a shared code block
141
+ // requires its own annotation per REQ-SWITCH-FALLTHROUGH.
70
142
  return;
71
143
  }
72
144
  (0, branch_annotation_helpers_1.reportMissingAnnotations)(context, node, storyFixCountRef);
@@ -72,6 +72,14 @@ const rule = {
72
72
  missingReqPrefix: "Test name should start with requirement ID (e.g., '[REQ-MAINT-DETECT] ...').",
73
73
  },
74
74
  },
75
+ /**
76
+ * Wire up the ESLint rule visitors that enforce test traceability conventions
77
+ * for supported test frameworks.
78
+ *
79
+ * @story docs/stories/020.0-DEV-TEST-ANNOTATION-VALIDATION.story.md
80
+ * @req REQ-TEST-FILE-SUPPORTS REQ-TEST-DESCRIBE-STORY REQ-TEST-IT-REQ-PREFIX
81
+ * @supports docs/stories/021.0-DEV-TEST-ANNOTATION-AUTO-FIX.story.md REQ-TEST-FIX-TEMPLATE REQ-TEST-FIX-PREFIX-FORMAT
82
+ */
75
83
  create(context) {
76
84
  const filename = context.getFilename();
77
85
  const rawOptions = (context.options && context.options[0]) || {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Composite ESLint rule that enforces both story and requirement traceability
3
+ * annotations on functions and methods.
4
+ *
5
+ * Implements Story 003.0-DEV-FUNCTION-ANNOTATIONS with:
6
+ * - REQ-ANNOTATION-REQUIRED
7
+ * - REQ-FUNCTION-DETECTION
8
+ * - REQ-CONFIGURABLE-SCOPE
9
+ * - REQ-EXPORT-PRIORITY
10
+ * - REQ-ERROR-LOCATION
11
+ * - REQ-TYPESCRIPT-SUPPORT
12
+ *
13
+ * via composition of:
14
+ * - ./require-story-annotation
15
+ * - ./require-req-annotation
16
+ */
17
+ import type { Rule } from "eslint";
18
+ declare const rule: Rule.RuleModule;
19
+ export default rule;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * Composite ESLint rule that enforces both story and requirement traceability
4
+ * annotations on functions and methods.
5
+ *
6
+ * Implements Story 003.0-DEV-FUNCTION-ANNOTATIONS with:
7
+ * - REQ-ANNOTATION-REQUIRED
8
+ * - REQ-FUNCTION-DETECTION
9
+ * - REQ-CONFIGURABLE-SCOPE
10
+ * - REQ-EXPORT-PRIORITY
11
+ * - REQ-ERROR-LOCATION
12
+ * - REQ-TYPESCRIPT-SUPPORT
13
+ *
14
+ * via composition of:
15
+ * - ./require-story-annotation
16
+ * - ./require-req-annotation
17
+ */
18
+ var __importDefault = (this && this.__importDefault) || function (mod) {
19
+ return (mod && mod.__esModule) ? mod : { "default": mod };
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ const require_story_annotation_1 = __importDefault(require("./require-story-annotation"));
23
+ const require_req_annotation_1 = __importDefault(require("./require-req-annotation"));
24
+ const storyRule = require_story_annotation_1.default;
25
+ const reqRule = require_req_annotation_1.default;
26
+ const rule = {
27
+ meta: {
28
+ type: "problem",
29
+ docs: {
30
+ description: "Require @story and @req (or @supports) annotations on functions and methods",
31
+ recommended: "error",
32
+ },
33
+ hasSuggestions: Boolean(storyRule.meta?.hasSuggestions) ||
34
+ Boolean(reqRule.meta?.hasSuggestions),
35
+ fixable: (storyRule.meta && storyRule.meta.fixable) ||
36
+ (reqRule.meta && reqRule.meta.fixable) ||
37
+ undefined,
38
+ messages: {
39
+ ...(storyRule.meta?.messages ?? {}),
40
+ ...(reqRule.meta?.messages ?? {}),
41
+ },
42
+ schema: (storyRule.meta && storyRule.meta.schema) ??
43
+ (reqRule.meta && reqRule.meta.schema) ??
44
+ [],
45
+ },
46
+ create(context) {
47
+ const storyListeners = storyRule.create(context) || {};
48
+ const reqListeners = reqRule.create(context) || {};
49
+ const mergedListener = {};
50
+ const allEventNames = new Set([
51
+ ...Object.keys(storyListeners),
52
+ ...Object.keys(reqListeners),
53
+ ]);
54
+ for (const eventName of allEventNames) {
55
+ const storyHandler = storyListeners[eventName];
56
+ const reqHandler = reqListeners[eventName];
57
+ if (storyHandler && reqHandler) {
58
+ mergedListener[eventName] = function mergedHandler(...args) {
59
+ storyHandler.apply(this, args);
60
+ reqHandler.apply(this, args);
61
+ };
62
+ }
63
+ else if (storyHandler) {
64
+ mergedListener[eventName] = storyHandler;
65
+ }
66
+ else if (reqHandler) {
67
+ mergedListener[eventName] = reqHandler;
68
+ }
69
+ }
70
+ return mergedListener;
71
+ },
72
+ };
73
+ exports.default = rule;
@@ -31,6 +31,10 @@ exports.default = {
31
31
  /**
32
32
  * Rule create entrypoint that returns the Program visitor.
33
33
  * Delegates to createValidReqReferenceProgramVisitor helper.
34
+ *
35
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
36
+ * @req REQ-REQ-VALIDATION - Validate that requirement references in annotations resolve to known requirements
37
+ * @req REQ-REQ-FILE-EXISTS - Ensure that referenced story files exist before validating requirement references
34
38
  */
35
39
  create(context) {
36
40
  return {
@@ -6,5 +6,14 @@
6
6
  * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
7
7
  */
8
8
  import type { Rule } from "eslint";
9
+ /**
10
+ * ESLint rule factory: configures and returns visitors that validate story
11
+ * references in @story and @supports annotations across all comments.
12
+ *
13
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
14
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
15
+ * @req REQ-STORY-FILE-EXISTS - Ensure each referenced story in @story/@supports annotations points to an existing file
16
+ * @req REQ-STORY-CONTENT - Provide a foundation for deeper story content validation by guaranteeing valid references
17
+ */
9
18
  declare const _default: Rule.RuleModule;
10
19
  export default _default;
@@ -195,6 +195,15 @@ function handleComment(opts) {
195
195
  }
196
196
  }
197
197
  }
198
+ /**
199
+ * ESLint rule factory: configures and returns visitors that validate story
200
+ * references in @story and @supports annotations across all comments.
201
+ *
202
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
203
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
204
+ * @req REQ-STORY-FILE-EXISTS - Ensure each referenced story in @story/@supports annotations points to an existing file
205
+ * @req REQ-STORY-CONTENT - Provide a foundation for deeper story content validation by guaranteeing valid references
206
+ */
198
207
  exports.default = {
199
208
  meta: {
200
209
  type: "problem",
@@ -1,4 +1,5 @@
1
1
  import type { Rule } from "eslint";
2
+ import { reportMissingAnnotations } from "./branch-annotation-report-helpers";
2
3
  /**
3
4
  * Valid branch types for require-branch-annotation rule.
4
5
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -16,6 +17,16 @@ export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
16
17
  * @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
17
18
  */
18
19
  export declare function validateBranchTypes(context: Rule.RuleContext): BranchType[] | Rule.RuleListener;
20
+ /**
21
+ * Scan contiguous formatter-aware comment lines between the provided 0-based
22
+ * start and end indices (inclusive), stopping when a non-comment or blank line
23
+ * is encountered. This helper is used as a line-based fallback when
24
+ * structured comment APIs are not available for branch bodies.
25
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
26
+ * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC
27
+ * @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC-ELSE-IF
28
+ */
29
+ export declare function scanCommentLinesInRange(lines: string[], startIndex: number, endIndexInclusive: number): string;
19
30
  /**
20
31
  * Gather leading comment text for a branch node.
21
32
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -46,13 +57,4 @@ export declare function reportMissingReq(context: Rule.RuleContext, node: any, o
46
57
  insertPos: number;
47
58
  missingStory: boolean;
48
59
  }): void;
49
- /**
50
- * Report missing annotations on a branch node.
51
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
52
- * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
53
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
54
- * @supports REQ-DUAL-POSITION-DETECTION
55
- */
56
- export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
57
- count: number;
58
- }): void;
60
+ export { reportMissingAnnotations };
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_BRANCH_TYPES = void 0;
3
+ exports.reportMissingAnnotations = exports.DEFAULT_BRANCH_TYPES = void 0;
4
4
  exports.validateBranchTypes = validateBranchTypes;
5
+ exports.scanCommentLinesInRange = scanCommentLinesInRange;
5
6
  exports.gatherBranchCommentText = gatherBranchCommentText;
6
7
  exports.reportMissingStory = reportMissingStory;
7
8
  exports.reportMissingReq = reportMissingReq;
8
- exports.reportMissingAnnotations = reportMissingAnnotations;
9
+ const branch_annotation_report_helpers_1 = require("./branch-annotation-report-helpers");
10
+ Object.defineProperty(exports, "reportMissingAnnotations", { enumerable: true, get: function () { return branch_annotation_report_helpers_1.reportMissingAnnotations; } });
11
+ const branch_annotation_loop_helpers_1 = require("./branch-annotation-loop-helpers");
9
12
  const PRE_COMMENT_OFFSET = 2; // number of lines above branch to inspect for comments
10
13
  /**
11
14
  * Valid branch types for require-branch-annotation rule.
@@ -150,6 +153,7 @@ function scanCommentLinesInRange(lines, startIndex, endIndexInclusive) {
150
153
  }
151
154
  return comments.join(" ");
152
155
  }
156
+ /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
153
157
  function isElseIfBranch(node, parent) {
154
158
  return (node &&
155
159
  node.type === "IfStatement" &&
@@ -294,6 +298,18 @@ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
294
298
  }
295
299
  return beforeText;
296
300
  }
301
+ /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
302
+ function gatherSwitchCaseCommentText(sourceCode, node) {
303
+ const lines = sourceCode.lines;
304
+ const startLine = node.loc.start.line;
305
+ let i = startLine - PRE_COMMENT_OFFSET;
306
+ const comments = [];
307
+ while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
308
+ comments.unshift(lines[i].trim());
309
+ i--;
310
+ }
311
+ return comments.join(" ");
312
+ }
297
313
  /**
298
314
  * Gather leading comment text for a branch node.
299
315
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -308,17 +324,7 @@ function gatherBranchCommentText(sourceCode, node, parent) {
308
324
  * @req REQ-TRACEABILITY-SWITCHCASE-COMMENTS - Trace collection of preceding comments for SwitchCase
309
325
  */
310
326
  if (node.type === "SwitchCase") {
311
- const lines = sourceCode.lines;
312
- const startLine = node.loc.start.line;
313
- let i = startLine - PRE_COMMENT_OFFSET;
314
- const comments = [];
315
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
316
- // @req REQ-TRACEABILITY-WHILE - Trace while loop that collects preceding comments for SwitchCase
317
- while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
318
- comments.unshift(lines[i].trim());
319
- i--;
320
- }
321
- return comments.join(" ");
327
+ return gatherSwitchCaseCommentText(sourceCode, node);
322
328
  }
323
329
  const beforeComments = sourceCode.getCommentsBefore(node) || [];
324
330
  const beforeText = beforeComments.map(extractCommentValue).join(" ");
@@ -334,6 +340,18 @@ function gatherBranchCommentText(sourceCode, node, parent) {
334
340
  if (node.type === "IfStatement") {
335
341
  return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
336
342
  }
343
+ /**
344
+ * Conditional branch for loop nodes that may include annotations either on the loop
345
+ * statement itself or at the top of the loop body, allowing flexible placement.
346
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-LOOP-ANNOTATION REQ-LOOP-PLACEMENT-FLEXIBLE
347
+ */
348
+ if (node.type === "ForStatement" ||
349
+ node.type === "ForInStatement" ||
350
+ node.type === "ForOfStatement" ||
351
+ node.type === "WhileStatement" ||
352
+ node.type === "DoWhileStatement") {
353
+ return (0, branch_annotation_loop_helpers_1.gatherLoopCommentText)(sourceCode, node, beforeText);
354
+ }
337
355
  return beforeText;
338
356
  }
339
357
  /**
@@ -409,130 +427,3 @@ function reportMissingReq(context, node, options) {
409
427
  });
410
428
  }
411
429
  }
412
- /**
413
- * Compute the base indent and insert position for a branch node, including
414
- * special handling for CatchClause bodies.
415
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
416
- * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
417
- * @supports REQ-ANNOTATION-PARSING
418
- * @supports REQ-DUAL-POSITION-DETECTION
419
- */
420
- /**
421
- * Compute indentation and insert position for the start of a given 1-based line
422
- * number. This keeps indentation and fixer insert positions consistent across
423
- * branch helpers that need to align auto-inserted comments with existing
424
- * source formatting.
425
- * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-ANNOTATION-PARSING
426
- */
427
- function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
428
- const lines = sourceCode.lines;
429
- let indent = fallbackIndent;
430
- if (line >= 1 && line <= lines.length) {
431
- const rawLine = lines[line - 1];
432
- indent = rawLine.match(/^(\s*)/)?.[1] || fallbackIndent;
433
- }
434
- const insertPos = sourceCode.getIndexFromLoc({
435
- line,
436
- column: 0,
437
- });
438
- return { indent, insertPos };
439
- }
440
- function getBaseBranchIndentAndInsertPos(sourceCode, node) {
441
- let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
442
- if (node.type === "CatchClause" && node.body) {
443
- const bodyNode = node.body;
444
- const bodyStatements = Array.isArray(bodyNode.body)
445
- ? bodyNode.body
446
- : undefined;
447
- const firstStatement = bodyStatements && bodyStatements.length > 0
448
- ? bodyStatements[0]
449
- : undefined;
450
- if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
451
- const firstLine = firstStatement.loc.start.line;
452
- const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, "");
453
- indent = firstLineInfo.indent;
454
- insertPos = firstLineInfo.insertPos;
455
- }
456
- else if (bodyNode.loc && bodyNode.loc.start) {
457
- const blockLine = bodyNode.loc.start.line;
458
- const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, "");
459
- const innerIndent = `${blockLineInfo.indent} `;
460
- indent = innerIndent;
461
- insertPos = blockLineInfo.insertPos;
462
- }
463
- }
464
- return { indent, insertPos };
465
- }
466
- /**
467
- * Compute annotation-related metadata for a branch node.
468
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
469
- * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
470
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
471
- * @supports REQ-DUAL-POSITION-DETECTION
472
- * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
473
- */
474
- function getBranchAnnotationInfo(sourceCode, node, parent) {
475
- const text = gatherBranchCommentText(sourceCode, node, parent);
476
- const hasSupports = /@supports\b/.test(text);
477
- const missingStory = !/@story\b/.test(text) && !hasSupports;
478
- const missingReq = !/@req\b/.test(text) && !hasSupports;
479
- let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
480
- if (isElseIfBranch(node, parent) &&
481
- node.consequent &&
482
- node.consequent.type === "BlockStatement" &&
483
- node.consequent.loc &&
484
- node.consequent.loc.start) {
485
- // For else-if blocks, align auto-fix comments with Prettier's tendency to place comments
486
- // inside the wrapped block body; non-block consequents intentionally keep the default behavior.
487
- const commentLine = node.consequent.loc.start.line + 1;
488
- const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
489
- indent = commentLineInfo.indent;
490
- insertPos = commentLineInfo.insertPos;
491
- }
492
- return { missingStory, missingReq, indent, insertPos };
493
- }
494
- /**
495
- * Report missing annotations on a branch node.
496
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
497
- * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
498
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
499
- * @supports REQ-DUAL-POSITION-DETECTION
500
- */
501
- function reportMissingAnnotations(context, node, storyFixCountRef) {
502
- const sourceCode = context.getSourceCode();
503
- /**
504
- * Determine the direct parent of the node using the parent reference on the node.
505
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
506
- * @supports REQ-DUAL-POSITION-DETECTION
507
- */
508
- const parent = node.parent;
509
- const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
510
- const actions = [
511
- {
512
- missing: missingStory,
513
- fn: reportMissingStory,
514
- args: [context, node, { indent, insertPos, storyFixCountRef }],
515
- },
516
- {
517
- missing: missingReq,
518
- fn: reportMissingReq,
519
- args: [context, node, { indent, insertPos, missingStory }],
520
- },
521
- ];
522
- /**
523
- * Process a single action from the actions array.
524
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
525
- * @req REQ-TRACEABILITY-ACTIONS-FOREACH - Trace processing of actions array to report missing annotations
526
- */
527
- function processAction(item) {
528
- /**
529
- * Callback invoked for each action to decide and execute reporting.
530
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
531
- * @req REQ-TRACEABILITY-FOR-EACH-CALLBACK - Trace callback handling for each action item
532
- */
533
- if (item.missing) {
534
- item.fn(...item.args);
535
- }
536
- }
537
- actions.forEach(processAction);
538
- }
@@ -0,0 +1,9 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Gather annotation text for loop branches, supporting annotations either on the
4
+ * loop statement itself or on the first comment lines inside the loop body.
5
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
6
+ * @req REQ-LOOP-ANNOTATION
7
+ * @req REQ-LOOP-PLACEMENT-FLEXIBLE
8
+ */
9
+ export declare function gatherLoopCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, beforeText: string): string;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gatherLoopCommentText = gatherLoopCommentText;
4
+ const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
5
+ /**
6
+ * Gather annotation text for loop branches, supporting annotations either on the
7
+ * loop statement itself or on the first comment lines inside the loop body.
8
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
9
+ * @req REQ-LOOP-ANNOTATION
10
+ * @req REQ-LOOP-PLACEMENT-FLEXIBLE
11
+ */
12
+ function gatherLoopCommentText(sourceCode, node, beforeText) {
13
+ if (/@story\b/.test(beforeText) ||
14
+ /@req\b/.test(beforeText) ||
15
+ /@supports\b/.test(beforeText)) {
16
+ return beforeText;
17
+ }
18
+ const body = node.body;
19
+ if (body &&
20
+ body.type === "BlockStatement" &&
21
+ body.loc &&
22
+ body.loc.start &&
23
+ body.loc.end) {
24
+ const lines = sourceCode.lines;
25
+ const startIndex = body.loc.start.line; // first line inside block body (start.line is 1-based)
26
+ const endIndex = body.loc.end.line - 1;
27
+ const insideText = (0, branch_annotation_helpers_1.scanCommentLinesInRange)(lines, startIndex, endIndex);
28
+ if (insideText &&
29
+ (/@story\b/.test(insideText) ||
30
+ /@req\b/.test(insideText) ||
31
+ /@supports\b/.test(insideText))) {
32
+ return insideText;
33
+ }
34
+ }
35
+ return beforeText;
36
+ }
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Report missing annotations on a branch node.
4
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
5
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
6
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
7
+ * @supports REQ-DUAL-POSITION-DETECTION
8
+ */
9
+ export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
10
+ count: number;
11
+ }): void;