eslint-plugin-traceability 1.16.1 → 1.17.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.16.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.16.0...v1.16.1) (2025-12-09)
1
+ # [1.17.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.16.1...v1.17.0) (2025-12-09)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * broaden test callback exclusion coverage for function annotations ([8078745](https://github.com/voder-ai/eslint-plugin-traceability/commit/80787455dcba39724158f378fce54ae78a75b59b))
6
+ * allow configuring additional excluded test helper callbacks ([d266197](https://github.com/voder-ai/eslint-plugin-traceability/commit/d26619721b1826fe97d01a63647f07505e35c846))
7
7
 
8
8
  # Changelog
9
9
 
@@ -11,21 +11,17 @@ import type { Rule } from "eslint";
11
11
  import { linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory } from "./require-story-io";
12
12
  import { getNodeName } from "./require-story-utils";
13
13
  import { DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, STORY_PATH } from "./require-story-core";
14
+ import { type CallbackExclusionOptions } from "./test-callback-exclusion";
14
15
  /**
15
16
  * Shared configuration helpers
16
17
  */
17
- interface ReportOptions {
18
+ interface ReportOptions extends CallbackExclusionOptions {
18
19
  annotationTemplateOverride?: string;
19
20
  autoFixToggle?: boolean;
20
- excludeTestCallbacks?: boolean;
21
21
  }
22
22
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
23
- declare function getAnnotationTemplate(override?: string, _options?: {
24
- excludeTestCallbacks?: boolean;
25
- }): string;
26
- declare function shouldApplyAutoFix(autoFix: boolean | undefined, _options?: {
27
- excludeTestCallbacks?: boolean;
28
- }): boolean;
23
+ declare function getAnnotationTemplate(override?: string, _options?: CallbackExclusionOptions): string;
24
+ declare function shouldApplyAutoFix(autoFix: boolean | undefined, _options?: CallbackExclusionOptions): boolean;
29
25
  /**
30
26
  * Determine if a node is in an export declaration
31
27
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -70,9 +66,7 @@ declare function resolveTargetNode(sourceCode: any, node: any): any;
70
66
  */
71
67
  declare function extractName(node: any): string;
72
68
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
73
- declare function shouldProcessNode(node: any, scope: string[], exportPriority?: string, options?: {
74
- excludeTestCallbacks?: boolean;
75
- }): boolean;
69
+ declare function shouldProcessNode(node: any, scope: string[], exportPriority?: string, options?: CallbackExclusionOptions): boolean;
76
70
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
77
71
  declare function reportMissing(context: Rule.RuleContext, sourceCode: any, config: {
78
72
  node: any;
@@ -23,38 +23,7 @@ const require_story_core_1 = require("./require-story-core");
23
23
  Object.defineProperty(exports, "DEFAULT_SCOPE", { enumerable: true, get: function () { return require_story_core_1.DEFAULT_SCOPE; } });
24
24
  Object.defineProperty(exports, "EXPORT_PRIORITY_VALUES", { enumerable: true, get: function () { return require_story_core_1.EXPORT_PRIORITY_VALUES; } });
25
25
  Object.defineProperty(exports, "STORY_PATH", { enumerable: true, get: function () { return require_story_core_1.STORY_PATH; } });
26
- /**
27
- * Known test framework function names and variants.
28
- * Includes Jest, Mocha, Vitest and their focused/skipped/concurrent variants.
29
- * @req REQ-TEST-CALLBACK-EXCLUSION
30
- */
31
- const TEST_FUNCTION_NAMES = new Set([
32
- // Core test/describe-style functions (Jest, Mocha, Vitest share many of these)
33
- "it",
34
- "test",
35
- "describe",
36
- "suite",
37
- // Focused variants
38
- "fit",
39
- "ftest",
40
- "fdescribe",
41
- "fsuite",
42
- // Skipped variants
43
- "xit",
44
- "xtest",
45
- "xdescribe",
46
- "xsuite",
47
- // Additional common aliases
48
- "context",
49
- "specify",
50
- "before",
51
- "after",
52
- "beforeEach",
53
- "afterEach",
54
- "beforeAll",
55
- "afterAll",
56
- ]);
57
- const TEST_FUNCTION_CONCURRENT_PROP = "concurrent";
26
+ const test_callback_exclusion_1 = require("./test-callback-exclusion");
58
27
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
59
28
  function getAnnotationTemplate(override, _options) {
60
29
  if (typeof override === "string" && override.trim().length > 0) {
@@ -74,9 +43,13 @@ function shouldApplyAutoFix(autoFix, _options) {
74
43
  */
75
44
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
76
45
  function buildTemplateConfig(options) {
77
- const effectiveTemplate = getAnnotationTemplate(options?.annotationTemplateOverride, { excludeTestCallbacks: options?.excludeTestCallbacks });
46
+ const effectiveTemplate = getAnnotationTemplate(options?.annotationTemplateOverride, {
47
+ excludeTestCallbacks: options?.excludeTestCallbacks,
48
+ additionalTestHelperNames: options?.additionalTestHelperNames,
49
+ });
78
50
  const allowFix = shouldApplyAutoFix(options?.autoFixToggle, {
79
51
  excludeTestCallbacks: options?.excludeTestCallbacks,
52
+ additionalTestHelperNames: options?.additionalTestHelperNames,
80
53
  });
81
54
  return { effectiveTemplate, allowFix };
82
55
  }
@@ -123,46 +96,6 @@ function isEffectivelyAnonymousFunction(node) {
123
96
  }
124
97
  return true;
125
98
  }
126
- /**
127
- * Determine whether a node represents a callback passed to a known test
128
- * framework function (Jest, Mocha, Vitest, etc).
129
- *
130
- * Supports:
131
- * - it(), test(), describe(), suite(), context(), specify()
132
- * - lifecycle hooks: before(), after(), beforeEach(), afterEach(), beforeAll(), afterAll()
133
- * - focused variants: fit(), ftest(), fdescribe(), fsuite()
134
- * - skipped variants and helpers: xit(), xtest(), xdescribe(), xsuite()
135
- * - their .concurrent variants (e.g., it.concurrent(), test.concurrent())
136
- *
137
- * @req REQ-TEST-CALLBACK-EXCLUSION
138
- */
139
- function isTestFrameworkCallback(node, options) {
140
- if (options?.excludeTestCallbacks === false) {
141
- return false;
142
- }
143
- if (!node || node.type !== "ArrowFunctionExpression") {
144
- return false;
145
- }
146
- const parent = node.parent;
147
- if (!parent || parent.type !== "CallExpression") {
148
- return false;
149
- }
150
- const callee = parent.callee;
151
- if (callee.type === "Identifier") {
152
- return TEST_FUNCTION_NAMES.has(callee.name);
153
- }
154
- if (callee.type === "MemberExpression" &&
155
- !callee.computed &&
156
- callee.property &&
157
- callee.property.type === "Identifier" &&
158
- callee.property.name === TEST_FUNCTION_CONCURRENT_PROP) {
159
- const obj = callee.object;
160
- if (obj && obj.type === "Identifier") {
161
- return TEST_FUNCTION_NAMES.has(obj.name);
162
- }
163
- }
164
- return false;
165
- }
166
99
  /**
167
100
  * Determine whether a function node is required to carry its own annotation
168
101
  * according to Story 004.0-DEV-BRANCH-ANNOTATIONS rules.
@@ -177,7 +110,7 @@ function isTestFrameworkCallback(node, options) {
177
110
  * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-ARROW-FUNCTION-EXCLUDED REQ-NESTED-FUNCTION-INHERITANCE
178
111
  */
179
112
  function requiresOwnFunctionAnnotation(node, options) {
180
- if (isTestFrameworkCallback(node, options)) {
113
+ if ((0, test_callback_exclusion_1.isTestFrameworkCallback)(node, options)) {
181
114
  return false;
182
115
  }
183
116
  // Anonymous arrow functions used as callbacks are excluded from function-level
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared helpers for determining whether a function-like node should be
3
+ * treated as a test framework callback that may be excluded from
4
+ * function-level annotation requirements.
5
+ *
6
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
7
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
8
+ * @story docs/stories/013-exclude-test-framework-callbacks.proposed.md
9
+ * @req REQ-TEST-CALLBACK-EXCLUSION - Provide reusable test callback exclusion logic
10
+ */
11
+ /**
12
+ * Options controlling how test callbacks are treated by the helpers.
13
+ *
14
+ * - excludeTestCallbacks: when false, no callbacks are excluded and all
15
+ * function-like nodes are treated as regular functions.
16
+ * - additionalTestHelperNames: optional array of additional helper names that
17
+ * should be treated like built-in test functions when excludeTestCallbacks
18
+ * is enabled.
19
+ */
20
+ interface CallbackExclusionOptions {
21
+ excludeTestCallbacks?: boolean;
22
+ additionalTestHelperNames?: string[];
23
+ }
24
+ /**
25
+ * Determine whether a node represents a callback passed to a known test
26
+ * framework function (Jest, Mocha, Vitest, etc).
27
+ *
28
+ * Supports:
29
+ * - it(), test(), describe(), suite(), context(), specify()
30
+ * - lifecycle hooks: before(), after(), beforeEach(), afterEach(), beforeAll(), afterAll()
31
+ * - focused variants: fit(), ftest(), fdescribe(), fsuite()
32
+ * - skipped variants and helpers: xit(), xtest(), xdescribe(), xsuite()
33
+ * - their .concurrent variants (e.g., it.concurrent(), test.concurrent())
34
+ *
35
+ * @req REQ-TEST-CALLBACK-EXCLUSION
36
+ */
37
+ declare function isTestFrameworkCallback(node: any, options?: CallbackExclusionOptions): boolean;
38
+ export type { CallbackExclusionOptions };
39
+ export { isTestFrameworkCallback };
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Shared helpers for determining whether a function-like node should be
4
+ * treated as a test framework callback that may be excluded from
5
+ * function-level annotation requirements.
6
+ *
7
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
8
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
9
+ * @story docs/stories/013-exclude-test-framework-callbacks.proposed.md
10
+ * @req REQ-TEST-CALLBACK-EXCLUSION - Provide reusable test callback exclusion logic
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.isTestFrameworkCallback = isTestFrameworkCallback;
14
+ /**
15
+ * Known test framework function names and variants.
16
+ * Includes Jest, Mocha, Vitest and their focused/skipped/concurrent variants.
17
+ *
18
+ * @req REQ-TEST-CALLBACK-EXCLUSION
19
+ */
20
+ const TEST_FUNCTION_NAMES = new Set([
21
+ // Core test/describe-style functions (Jest, Mocha, Vitest share many of these)
22
+ "it",
23
+ "test",
24
+ "describe",
25
+ "suite",
26
+ // Focused variants
27
+ "fit",
28
+ "ftest",
29
+ "fdescribe",
30
+ "fsuite",
31
+ // Skipped variants
32
+ "xit",
33
+ "xtest",
34
+ "xdescribe",
35
+ "xsuite",
36
+ // Additional common aliases
37
+ "context",
38
+ "specify",
39
+ "before",
40
+ "after",
41
+ "beforeEach",
42
+ "afterEach",
43
+ "beforeAll",
44
+ "afterAll",
45
+ ]);
46
+ const TEST_FUNCTION_CONCURRENT_PROP = "concurrent";
47
+ /**
48
+ * Determine if a function name should be treated as a recognized test helper,
49
+ * including core test functions and any configured additional helper names.
50
+ *
51
+ * Vitest's `bench` is explicitly never treated as an excluded test callback,
52
+ * even if it appears in additionalTestHelperNames, to preserve the story
53
+ * requirement that bench callbacks always require annotations.
54
+ *
55
+ * @req REQ-TEST-CALLBACK-EXCLUSION
56
+ */
57
+ function isRecognizedTestHelperName(name, options) {
58
+ if (name === "bench") {
59
+ return false;
60
+ }
61
+ if (TEST_FUNCTION_NAMES.has(name)) {
62
+ return true;
63
+ }
64
+ if (options?.additionalTestHelperNames &&
65
+ Array.isArray(options.additionalTestHelperNames)) {
66
+ return options.additionalTestHelperNames.includes(name);
67
+ }
68
+ return false;
69
+ }
70
+ /**
71
+ * Determine whether a node represents a callback passed to a known test
72
+ * framework function (Jest, Mocha, Vitest, etc).
73
+ *
74
+ * Supports:
75
+ * - it(), test(), describe(), suite(), context(), specify()
76
+ * - lifecycle hooks: before(), after(), beforeEach(), afterEach(), beforeAll(), afterAll()
77
+ * - focused variants: fit(), ftest(), fdescribe(), fsuite()
78
+ * - skipped variants and helpers: xit(), xtest(), xdescribe(), xsuite()
79
+ * - their .concurrent variants (e.g., it.concurrent(), test.concurrent())
80
+ *
81
+ * @req REQ-TEST-CALLBACK-EXCLUSION
82
+ */
83
+ function isTestFrameworkCallback(node, options) {
84
+ if (options?.excludeTestCallbacks === false) {
85
+ return false;
86
+ }
87
+ if (!node || node.type !== "ArrowFunctionExpression") {
88
+ return false;
89
+ }
90
+ const parent = node.parent;
91
+ if (!parent || parent.type !== "CallExpression") {
92
+ return false;
93
+ }
94
+ const callee = parent.callee;
95
+ if (callee.type === "Identifier") {
96
+ return isRecognizedTestHelperName(callee.name, options);
97
+ }
98
+ if (callee.type === "MemberExpression" &&
99
+ !callee.computed &&
100
+ callee.property &&
101
+ callee.property.type === "Identifier" &&
102
+ callee.property.name === TEST_FUNCTION_CONCURRENT_PROP) {
103
+ const obj = callee.object;
104
+ if (obj && obj.type === "Identifier") {
105
+ return isRecognizedTestHelperName(obj.name, options);
106
+ }
107
+ }
108
+ return false;
109
+ }
@@ -49,6 +49,11 @@ const rule = {
49
49
  methodAnnotationTemplate: { type: "string" },
50
50
  autoFix: { type: "boolean" },
51
51
  excludeTestCallbacks: { type: "boolean" },
52
+ additionalTestHelperNames: {
53
+ type: "array",
54
+ items: { type: "string" },
55
+ uniqueItems: true,
56
+ },
52
57
  },
53
58
  additionalProperties: false,
54
59
  },
@@ -79,6 +84,10 @@ const rule = {
79
84
  const excludeTestCallbacks = typeof opts.excludeTestCallbacks === "boolean"
80
85
  ? opts.excludeTestCallbacks
81
86
  : true;
87
+ const additionalTestHelperNames = Array.isArray(opts.additionalTestHelperNames) &&
88
+ opts.additionalTestHelperNames.every((name) => typeof name === "string")
89
+ ? opts.additionalTestHelperNames
90
+ : undefined;
82
91
  /**
83
92
  * Optional debug logging for troubleshooting this rule.
84
93
  * Developers can temporarily uncomment the block below to log when the rule
@@ -94,7 +103,10 @@ const rule = {
94
103
  // : "<unknown>",
95
104
  // );
96
105
  // Local closure that binds configured scope and export priority to the helper.
97
- const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, { excludeTestCallbacks });
106
+ const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, {
107
+ excludeTestCallbacks,
108
+ additionalTestHelperNames,
109
+ });
98
110
  // Delegate visitor construction to helper to keep this file concise.
99
111
  return (0, require_story_visitors_1.buildVisitors)(context, sourceCode, {
100
112
  shouldProcessNode: should,
@@ -104,6 +116,7 @@ const rule = {
104
116
  methodAnnotationTemplate,
105
117
  autoFix,
106
118
  excludeTestCallbacks,
119
+ additionalTestHelperNames,
107
120
  });
108
121
  },
109
122
  };
@@ -100,6 +100,11 @@ describe('Vitest suite', () => {
100
100
  bench('Vitest bench', () => {});
101
101
  });`,
102
102
  },
103
+ {
104
+ name: "[REQ-TEST-CALLBACK-EXCLUSION][Story 003.0] additionalTestHelperNames excludes configured helper callbacks when excludeTestCallbacks=true",
105
+ code: `withTestCase("does something", () => {});`,
106
+ options: [{ additionalTestHelperNames: ["withTestCase"] }],
107
+ },
103
108
  ],
104
109
  invalid: [
105
110
  {
@@ -216,6 +221,22 @@ describe('Vitest suite', () => {
216
221
  },
217
222
  ],
218
223
  },
224
+ {
225
+ name: "[REQ-TEST-CALLBACK-EXCLUSION][Story 003.0] bench callback still reported even when included in additionalTestHelperNames",
226
+ code: `bench("bench case", () => {});`,
227
+ options: [{ additionalTestHelperNames: ["bench"], autoFix: false }],
228
+ errors: [
229
+ {
230
+ messageId: "missingStory",
231
+ suggestions: [
232
+ {
233
+ desc: `Add traceability annotation for function '(anonymous)' using @supports (preferred) or @story (legacy), for example: /** @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */`,
234
+ output: `bench("bench case", /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n() => {});`,
235
+ },
236
+ ],
237
+ },
238
+ ],
239
+ },
219
240
  ],
220
241
  });
221
242
  ruleTester.run("require-story-annotation with exportPriority option", require_story_annotation_1.default, {
@@ -401,4 +401,73 @@ describe("Require Story Helpers (Story 003.0)", () => {
401
401
  });
402
402
  expect(result).toBeTruthy();
403
403
  });
404
+ /**
405
+ * Additional coverage for nested and helper-wrapped test callbacks.
406
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
407
+ * @req REQ-TEST-CALLBACK-EXCLUSION - Document how nested and wrapper-based callbacks interact with exclusion logic
408
+ */
409
+ test("[REQ-TEST-CALLBACK-EXCLUSION] Nested anonymous arrow inside it() callback is excluded via nested-function inheritance", () => {
410
+ const outerCallback = {
411
+ type: "ArrowFunctionExpression",
412
+ parent: {
413
+ type: "CallExpression",
414
+ callee: { type: "Identifier", name: "it" },
415
+ },
416
+ };
417
+ const innerCallback = {
418
+ type: "ArrowFunctionExpression",
419
+ parent: {
420
+ type: "BlockStatement",
421
+ parent: outerCallback,
422
+ },
423
+ };
424
+ // Outer callback is treated as a test framework callback and excluded.
425
+ const outerResult = (0, require_story_helpers_1.shouldProcessNode)(outerCallback, require_story_helpers_1.DEFAULT_SCOPE);
426
+ // Inner anonymous arrow inherits from its nested parent and is also excluded.
427
+ const innerResult = (0, require_story_helpers_1.shouldProcessNode)(innerCallback, require_story_helpers_1.DEFAULT_SCOPE);
428
+ expect(outerResult).toBeFalsy();
429
+ expect(innerResult).toBeFalsy();
430
+ });
431
+ test("[REQ-TEST-CALLBACK-EXCLUSION] Arrow callback passed to local wrapper around describe() is not treated as a test callback", () => {
432
+ const node = {
433
+ type: "ArrowFunctionExpression",
434
+ parent: {
435
+ type: "CallExpression",
436
+ callee: { type: "Identifier", name: "withDescribe" },
437
+ },
438
+ };
439
+ const result = (0, require_story_helpers_1.shouldProcessNode)(node, require_story_helpers_1.DEFAULT_SCOPE);
440
+ expect(result).toBeTruthy();
441
+ });
442
+ /**
443
+ * Additional coverage for configurable test helper names.
444
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
445
+ * @req REQ-TEST-CALLBACK-EXCLUSION - Verify additionalTestHelperNames interacts correctly with exclusion logic
446
+ */
447
+ test("[REQ-TEST-CALLBACK-EXCLUSION] Arrow callback passed to configured additionalTestHelperNames helper is excluded by default", () => {
448
+ const node = {
449
+ type: "ArrowFunctionExpression",
450
+ parent: {
451
+ type: "CallExpression",
452
+ callee: { type: "Identifier", name: "withTest" },
453
+ },
454
+ };
455
+ const result = (0, require_story_helpers_1.shouldProcessNode)(node, require_story_helpers_1.DEFAULT_SCOPE, "all", {
456
+ additionalTestHelperNames: ["withTest"],
457
+ });
458
+ expect(result).toBeFalsy();
459
+ });
460
+ test("[REQ-TEST-CALLBACK-EXCLUSION] bench callback is never excluded even when included in additionalTestHelperNames", () => {
461
+ const node = {
462
+ type: "ArrowFunctionExpression",
463
+ parent: {
464
+ type: "CallExpression",
465
+ callee: { type: "Identifier", name: "bench" },
466
+ },
467
+ };
468
+ const result = (0, require_story_helpers_1.shouldProcessNode)(node, require_story_helpers_1.DEFAULT_SCOPE, "all", {
469
+ additionalTestHelperNames: ["bench"],
470
+ });
471
+ expect(result).toBeTruthy();
472
+ });
404
473
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.16.1",
3
+ "version": "1.17.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",