eslint-plugin-traceability 1.13.1 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/lib/src/rules/helpers/require-story-core.js +1 -0
- package/lib/src/rules/helpers/require-story-helpers.d.ts +4 -0
- package/lib/src/rules/helpers/require-story-helpers.js +92 -3
- package/lib/src/rules/helpers/require-test-traceability-helpers.js +24 -0
- package/lib/src/rules/helpers/valid-annotation-options.d.ts +32 -1
- package/lib/src/rules/helpers/valid-annotation-options.js +144 -1
- package/lib/src/rules/helpers/valid-implements-utils.js +13 -5
- package/lib/src/rules/no-redundant-annotation.js +12 -0
- package/lib/src/rules/prefer-implements-annotation.js +176 -5
- package/lib/src/rules/require-branch-annotation.js +73 -1
- package/lib/src/rules/require-test-traceability.js +8 -0
- package/lib/src/rules/valid-req-reference.js +4 -0
- package/lib/src/rules/valid-story-reference.d.ts +9 -0
- package/lib/src/rules/valid-story-reference.js +9 -0
- package/lib/src/utils/branch-annotation-helpers.d.ts +12 -10
- package/lib/src/utils/branch-annotation-helpers.js +31 -140
- package/lib/src/utils/branch-annotation-loop-helpers.d.ts +9 -0
- package/lib/src/utils/branch-annotation-loop-helpers.js +36 -0
- package/lib/src/utils/branch-annotation-report-helpers.d.ts +11 -0
- package/lib/src/utils/branch-annotation-report-helpers.js +111 -0
- package/lib/tests/integration/dogfooding-validation.test.js +5 -2
- package/lib/tests/rules/prefer-implements-annotation.test.js +23 -0
- package/lib/tests/rules/require-branch-annotation.test.js +88 -19
- package/lib/tests/rules/require-story-annotation.test.js +56 -8
- package/lib/tests/utils/temp-dir-helpers.d.ts +6 -1
- package/lib/tests/utils/temp-dir-helpers.js +2 -1
- package/package.json +1 -1
- package/user-docs/api-reference.md +1 -1
|
@@ -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;
|
|
@@ -186,7 +193,7 @@ function hasMultipleStories(storyPaths) {
|
|
|
186
193
|
*
|
|
187
194
|
* @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-OPTIONAL-WARNING REQ-MULTI-STORY-DETECT REQ-AUTO-FIX REQ-VALID-OUTPUT
|
|
188
195
|
*/
|
|
189
|
-
function
|
|
196
|
+
function processBlockComment(comment, context) {
|
|
190
197
|
const { hasStory, hasReq, hasImplements, storyPaths } = analyzeComment(comment);
|
|
191
198
|
if (!hasStory || !hasReq) {
|
|
192
199
|
return;
|
|
@@ -215,6 +222,169 @@ function processComment(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
|
+
*/
|
|
233
|
+
function getLinePrefixFromText(fullText) {
|
|
234
|
+
const match = fullText.match(/^(\s*\/\/\s*)/);
|
|
235
|
+
return match ? match[1] : "";
|
|
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
|
+
*/
|
|
245
|
+
function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
|
|
246
|
+
const sourceCode = context.getSourceCode();
|
|
247
|
+
const storyComment = comments[storyIndex];
|
|
248
|
+
const storyNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(storyComment.value || "");
|
|
249
|
+
if (!storyNormalized || !/^@story\b/.test(storyNormalized)) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const storyParts = storyNormalized.split(/\s+/);
|
|
253
|
+
if (storyParts.length !== MIN_STORY_TOKENS) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const storyPath = storyParts[1];
|
|
257
|
+
const reqIds = [];
|
|
258
|
+
for (const idx of reqIndices) {
|
|
259
|
+
const reqComment = comments[idx];
|
|
260
|
+
const reqNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(reqComment.value || "");
|
|
261
|
+
if (!reqNormalized || !/^@req\b/.test(reqNormalized)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const reqParts = reqNormalized.split(/\s+/);
|
|
265
|
+
if (reqParts.length !== MIN_REQ_TOKENS) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
reqIds.push(reqParts[1]);
|
|
269
|
+
}
|
|
270
|
+
if (!reqIds.length) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const fullText = sourceCode.text.slice(storyComment.range[0], storyComment.range[1]);
|
|
274
|
+
const linePrefix = getLinePrefixFromText(fullText);
|
|
275
|
+
const implAnnotation = `@supports ${storyPath} ${reqIds.join(" ")}`;
|
|
276
|
+
const implLine = `${linePrefix}${implAnnotation}`;
|
|
277
|
+
const start = storyComment.range[0];
|
|
278
|
+
const end = comments[reqIndices[reqIndices.length - 1]].range[1];
|
|
279
|
+
return (fixer) => fixer.replaceTextRange([start, end], implLine);
|
|
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
|
+
*/
|
|
289
|
+
function handleInlineStorySequence(context, group, startIndex) {
|
|
290
|
+
const n = group.length;
|
|
291
|
+
const current = group[startIndex];
|
|
292
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
293
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
294
|
+
return startIndex + 1;
|
|
295
|
+
}
|
|
296
|
+
if (/^@supports\b/.test(normalized)) {
|
|
297
|
+
return startIndex + 1;
|
|
298
|
+
}
|
|
299
|
+
const storyIndex = startIndex;
|
|
300
|
+
const reqIndices = [];
|
|
301
|
+
let j = startIndex + 1;
|
|
302
|
+
while (j < n) {
|
|
303
|
+
const next = group[j];
|
|
304
|
+
const nextNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(next.value || "");
|
|
305
|
+
if (!nextNormalized || /^@supports\b/.test(nextNormalized)) {
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (/^@req\b/.test(nextNormalized)) {
|
|
309
|
+
reqIndices.push(j);
|
|
310
|
+
j += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (reqIndices.length === 0) {
|
|
316
|
+
context.report({
|
|
317
|
+
node: current,
|
|
318
|
+
messageId: "preferImplements",
|
|
319
|
+
});
|
|
320
|
+
return startIndex + 1;
|
|
321
|
+
}
|
|
322
|
+
const fix = tryBuildInlineAutoFix(context, group, storyIndex, reqIndices);
|
|
323
|
+
if (fix) {
|
|
324
|
+
context.report({
|
|
325
|
+
node: current,
|
|
326
|
+
messageId: "preferImplements",
|
|
327
|
+
fix,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
context.report({
|
|
332
|
+
node: current,
|
|
333
|
+
messageId: "preferImplements",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return reqIndices[reqIndices.length - 1] + 1;
|
|
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
|
+
*/
|
|
346
|
+
function processInlineGroup(context, group) {
|
|
347
|
+
if (group.length === 0)
|
|
348
|
+
return;
|
|
349
|
+
const n = group.length;
|
|
350
|
+
let i = 0;
|
|
351
|
+
while (i < n) {
|
|
352
|
+
const current = group[i];
|
|
353
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
354
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
355
|
+
i += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
i = handleInlineStorySequence(context, group, i);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Scan sequences of Line comments for inline legacy @story/@req patterns and
|
|
363
|
+
* report diagnostics and optional auto-fixes.
|
|
364
|
+
*/
|
|
365
|
+
function processInlineComments(context, lineComments) {
|
|
366
|
+
if (!lineComments.length)
|
|
367
|
+
return;
|
|
368
|
+
// Group by contiguous line numbers
|
|
369
|
+
let group = [lineComments[0]];
|
|
370
|
+
const flushGroup = () => {
|
|
371
|
+
processInlineGroup(context, group);
|
|
372
|
+
group = [];
|
|
373
|
+
};
|
|
374
|
+
for (let idx = 1; idx < lineComments.length; idx++) {
|
|
375
|
+
const prev = lineComments[idx - 1];
|
|
376
|
+
const curr = lineComments[idx];
|
|
377
|
+
if (curr.loc.start.line === prev.loc.start.line + 1 &&
|
|
378
|
+
curr.loc.start.column === prev.loc.start.column) {
|
|
379
|
+
group.push(curr);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
flushGroup();
|
|
383
|
+
group.push(curr);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
flushGroup();
|
|
387
|
+
}
|
|
218
388
|
/**
|
|
219
389
|
* ESLint rule: prefer-implements-annotation
|
|
220
390
|
*
|
|
@@ -292,11 +462,12 @@ const preferImplementsAnnotationRule = {
|
|
|
292
462
|
*/
|
|
293
463
|
Program() {
|
|
294
464
|
const comments = sourceCode.getAllComments() || [];
|
|
295
|
-
comments
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
processComment(comment, context);
|
|
465
|
+
const blockComments = comments.filter((comment) => comment.type === "Block");
|
|
466
|
+
blockComments.forEach((comment) => {
|
|
467
|
+
processBlockComment(comment, context);
|
|
299
468
|
});
|
|
469
|
+
const lineComments = comments.filter((comment) => comment.type === "Line");
|
|
470
|
+
processInlineComments(context, lineComments);
|
|
300
471
|
},
|
|
301
472
|
};
|
|
302
473
|
},
|
|
@@ -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" &&
|
|
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]) || {};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|