eslint-plugin-traceability 1.13.1 → 1.14.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.
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# [1.14.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.13.1...v1.14.0) (2025-12-08)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* support inline [@supports](https://github.com/supports) migration in prefer-supports-annotation rule ([32b064d](https://github.com/voder-ai/eslint-plugin-traceability/commit/32b064daee35d867d74c3f4f9041c4b780918559))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
@@ -186,7 +186,7 @@ function hasMultipleStories(storyPaths) {
|
|
|
186
186
|
*
|
|
187
187
|
* @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
188
|
*/
|
|
189
|
-
function
|
|
189
|
+
function processBlockComment(comment, context) {
|
|
190
190
|
const { hasStory, hasReq, hasImplements, storyPaths } = analyzeComment(comment);
|
|
191
191
|
if (!hasStory || !hasReq) {
|
|
192
192
|
return;
|
|
@@ -215,6 +215,137 @@ function processComment(comment, context) {
|
|
|
215
215
|
fix: fix ?? undefined,
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
|
+
function getLinePrefixFromText(fullText) {
|
|
219
|
+
const match = fullText.match(/^(\s*\/\/\s*)/);
|
|
220
|
+
return match ? match[1] : "";
|
|
221
|
+
}
|
|
222
|
+
function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
|
|
223
|
+
const sourceCode = context.getSourceCode();
|
|
224
|
+
const storyComment = comments[storyIndex];
|
|
225
|
+
const storyNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(storyComment.value || "");
|
|
226
|
+
if (!storyNormalized || !/^@story\b/.test(storyNormalized)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const storyParts = storyNormalized.split(/\s+/);
|
|
230
|
+
if (storyParts.length !== MIN_STORY_TOKENS) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const storyPath = storyParts[1];
|
|
234
|
+
const reqIds = [];
|
|
235
|
+
for (const idx of reqIndices) {
|
|
236
|
+
const reqComment = comments[idx];
|
|
237
|
+
const reqNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(reqComment.value || "");
|
|
238
|
+
if (!reqNormalized || !/^@req\b/.test(reqNormalized)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const reqParts = reqNormalized.split(/\s+/);
|
|
242
|
+
if (reqParts.length !== MIN_REQ_TOKENS) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
reqIds.push(reqParts[1]);
|
|
246
|
+
}
|
|
247
|
+
if (!reqIds.length) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const fullText = sourceCode.text.slice(storyComment.range[0], storyComment.range[1]);
|
|
251
|
+
const linePrefix = getLinePrefixFromText(fullText);
|
|
252
|
+
const implAnnotation = `@supports ${storyPath} ${reqIds.join(" ")}`;
|
|
253
|
+
const implLine = `${linePrefix}${implAnnotation}`;
|
|
254
|
+
const start = storyComment.range[0];
|
|
255
|
+
const end = comments[reqIndices[reqIndices.length - 1]].range[1];
|
|
256
|
+
return (fixer) => fixer.replaceTextRange([start, end], implLine);
|
|
257
|
+
}
|
|
258
|
+
function handleInlineStorySequence(context, group, startIndex) {
|
|
259
|
+
const n = group.length;
|
|
260
|
+
const current = group[startIndex];
|
|
261
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
262
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
263
|
+
return startIndex + 1;
|
|
264
|
+
}
|
|
265
|
+
if (/^@supports\b/.test(normalized)) {
|
|
266
|
+
return startIndex + 1;
|
|
267
|
+
}
|
|
268
|
+
const storyIndex = startIndex;
|
|
269
|
+
const reqIndices = [];
|
|
270
|
+
let j = startIndex + 1;
|
|
271
|
+
while (j < n) {
|
|
272
|
+
const next = group[j];
|
|
273
|
+
const nextNormalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(next.value || "");
|
|
274
|
+
if (!nextNormalized || /^@supports\b/.test(nextNormalized)) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (/^@req\b/.test(nextNormalized)) {
|
|
278
|
+
reqIndices.push(j);
|
|
279
|
+
j += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
if (reqIndices.length === 0) {
|
|
285
|
+
context.report({
|
|
286
|
+
node: current,
|
|
287
|
+
messageId: "preferImplements",
|
|
288
|
+
});
|
|
289
|
+
return startIndex + 1;
|
|
290
|
+
}
|
|
291
|
+
const fix = tryBuildInlineAutoFix(context, group, storyIndex, reqIndices);
|
|
292
|
+
if (fix) {
|
|
293
|
+
context.report({
|
|
294
|
+
node: current,
|
|
295
|
+
messageId: "preferImplements",
|
|
296
|
+
fix,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
context.report({
|
|
301
|
+
node: current,
|
|
302
|
+
messageId: "preferImplements",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return reqIndices[reqIndices.length - 1] + 1;
|
|
306
|
+
}
|
|
307
|
+
function processInlineGroup(context, group) {
|
|
308
|
+
if (group.length === 0)
|
|
309
|
+
return;
|
|
310
|
+
const n = group.length;
|
|
311
|
+
let i = 0;
|
|
312
|
+
while (i < n) {
|
|
313
|
+
const current = group[i];
|
|
314
|
+
const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
|
|
315
|
+
if (!normalized || !/^@story\b/.test(normalized)) {
|
|
316
|
+
i += 1;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
i = handleInlineStorySequence(context, group, i);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Scan sequences of Line comments for inline legacy @story/@req patterns and
|
|
324
|
+
* report diagnostics and optional auto-fixes.
|
|
325
|
+
*/
|
|
326
|
+
function processInlineComments(context, lineComments) {
|
|
327
|
+
if (!lineComments.length)
|
|
328
|
+
return;
|
|
329
|
+
// Group by contiguous line numbers
|
|
330
|
+
let group = [lineComments[0]];
|
|
331
|
+
const flushGroup = () => {
|
|
332
|
+
processInlineGroup(context, group);
|
|
333
|
+
group = [];
|
|
334
|
+
};
|
|
335
|
+
for (let idx = 1; idx < lineComments.length; idx++) {
|
|
336
|
+
const prev = lineComments[idx - 1];
|
|
337
|
+
const curr = lineComments[idx];
|
|
338
|
+
if (curr.loc.start.line === prev.loc.start.line + 1 &&
|
|
339
|
+
curr.loc.start.column === prev.loc.start.column) {
|
|
340
|
+
group.push(curr);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
flushGroup();
|
|
344
|
+
group.push(curr);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
flushGroup();
|
|
348
|
+
}
|
|
218
349
|
/**
|
|
219
350
|
* ESLint rule: prefer-implements-annotation
|
|
220
351
|
*
|
|
@@ -292,11 +423,12 @@ const preferImplementsAnnotationRule = {
|
|
|
292
423
|
*/
|
|
293
424
|
Program() {
|
|
294
425
|
const comments = sourceCode.getAllComments() || [];
|
|
295
|
-
comments
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
processComment(comment, context);
|
|
426
|
+
const blockComments = comments.filter((comment) => comment.type === "Block");
|
|
427
|
+
blockComments.forEach((comment) => {
|
|
428
|
+
processBlockComment(comment, context);
|
|
299
429
|
});
|
|
430
|
+
const lineComments = comments.filter((comment) => comment.type === "Line");
|
|
431
|
+
processInlineComments(context, lineComments);
|
|
300
432
|
},
|
|
301
433
|
};
|
|
302
434
|
},
|
|
@@ -87,6 +87,29 @@ describe("prefer-supports-annotation / prefer-implements-annotation aliasing (St
|
|
|
87
87
|
code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
|
|
88
88
|
errors: [{ messageId: "preferImplements" }],
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] single inline // @story + // @req auto-fixes to single // @supports line above function",
|
|
92
|
+
code: `// @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n// @req REQ-INLINE-COMMENT-SUPPORT\nfunction inlineLegacy() {}`,
|
|
93
|
+
output: `// @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-INLINE-COMMENT-SUPPORT\nfunction inlineLegacy() {}`,
|
|
94
|
+
errors: [{ messageId: "preferImplements" }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] single inline // @story with multiple // @req lines auto-fixes to single // @supports containing all REQ IDs",
|
|
98
|
+
code: `// @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n// @req REQ-INLINE-COMMENT-SUPPORT\n// @req REQ-BRANCH-POSITION-PRESERVE\nfunction inlineMultiReq() {}`,
|
|
99
|
+
output: `// @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-INLINE-COMMENT-SUPPORT REQ-BRANCH-POSITION-PRESERVE\nfunction inlineMultiReq() {}`,
|
|
100
|
+
errors: [{ messageId: "preferImplements" }],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] inline // @story + // @req above statement is auto-fixed preserving branch position (REQ-BRANCH-POSITION-PRESERVE)",
|
|
104
|
+
code: `if (flag) {\n // @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n // @req REQ-BRANCH-POSITION-PRESERVE\n doSomething();\n}`,
|
|
105
|
+
output: `if (flag) {\n // @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-BRANCH-POSITION-PRESERVE\n doSomething();\n}`,
|
|
106
|
+
errors: [{ messageId: "preferImplements" }],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "[REQ-INLINE-COMMENT-SUPPORT] complex inline // @req content is not safely auto-fixable but still reports preferImplements",
|
|
110
|
+
code: `// @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md\n// @req REQ-INLINE-COMMENT-SUPPORT extra description inline\nfunction inlineComplexReqNoAutoFix() {}`,
|
|
111
|
+
errors: [{ messageId: "preferImplements" }],
|
|
112
|
+
},
|
|
90
113
|
];
|
|
91
114
|
ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
|
|
92
115
|
valid,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|
|
@@ -349,7 +349,7 @@ Main behaviors:
|
|
|
349
349
|
Deliberate non‑targets and ignored comments:
|
|
350
350
|
|
|
351
351
|
- JSDoc blocks that contain **only** `@story`, **only** `@req`, or **only** `@supports` are **not** modified by this rule. They remain valid and continue to be governed solely by the core rules such as `require-story-annotation`, `require-req-annotation`, and `valid-annotation-format`.
|
|
352
|
-
- Inline or line comments like `// @story ...`, `// @req ...`, or `// @supports ...` are
|
|
352
|
+
- Inline or line comments like `// @story ...`, `// @req ...`, or `// @supports ...` are also supported in a limited, migration‑oriented way: when the rule detects a simple, consecutive pair or small run of `// @story ...` and `// @req ...` lines that are directly attached to a function or branch, it can, in `--fix` mode, consolidate them into a single `// @supports ...` line while preserving indentation and the comment’s relative position next to the code. More complex inline patterns (such as mixed traceability and non‑traceability content, multiple distinct stories, or interleaved unrelated comments) are still reported without auto‑fix for safety. As with JSDoc migration, this behavior is opt‑in: the rule remains disabled by default and must be explicitly enabled with your desired severity when you are ready to start migrating inline annotations.
|
|
353
353
|
- Any block that does not match the “single story + simple requirements, no supports” shape is treated conservatively: the rule may report a diagnostic to flag the legacy/mixed pattern, but it will not rewrite comments unless it is clearly safe.
|
|
354
354
|
|
|
355
355
|
Interaction with other rules:
|