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.13.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.13.0...v1.13.1) (2025-12-08)
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
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * refine no-redundant-annotation rule tests and behavior ([7d72670](https://github.com/voder-ai/eslint-plugin-traceability/commit/7d726702e3ad2268778c06de3f3a9673033e3a61))
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 processComment(comment, context) {
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
- .filter((comment) => comment.type === "Block")
297
- .forEach((comment) => {
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.13.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 intentionally ignored by this migration helper; they are still checked by the underlying validation rules where applicable.
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: