eslint-plugin-traceability 1.20.0 → 1.21.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 +2 -2
- package/README.md +3 -3
- package/lib/src/rules/helpers/require-story-core.d.ts +2 -0
- package/lib/src/rules/helpers/require-story-core.js +6 -4
- package/lib/src/rules/helpers/require-story-helpers.d.ts +19 -1
- package/lib/src/rules/helpers/require-story-helpers.js +48 -4
- package/lib/src/rules/helpers/require-story-visitors.js +6 -0
- package/lib/src/rules/helpers/test-callback-exclusion.d.ts +1 -0
- package/lib/src/rules/require-req-annotation.d.ts +2 -3
- package/lib/src/rules/require-req-annotation.js +11 -2
- package/lib/src/rules/require-story-annotation.js +46 -20
- package/lib/src/utils/annotation-checker.d.ts +1 -0
- package/lib/src/utils/annotation-checker.js +13 -0
- package/lib/tests/config/require-story-annotation-config.test.js +1 -0
- package/lib/tests/integration/require-traceability-aliases.integration.test.js +26 -0
- package/lib/tests/rules/require-req-annotation.test.js +20 -0
- package/lib/tests/rules/require-story-annotation.test.js +26 -0
- package/package.json +1 -1
- package/user-docs/api-reference.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# [1.
|
|
1
|
+
# [1.21.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.20.0...v1.21.0) (2025-12-19)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* support inside-brace placement for function-level rules ([064b1a4](https://github.com/voder-ai/eslint-plugin-traceability/commit/064b1a4e7627b5e35afd074752724ab5958e9d8b))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -117,15 +117,15 @@ Traceability annotations are typically placed immediately adjacent to the code t
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
The `annotationPlacement` option is also supported by the function-level rules (`traceability/require-story-annotation` and `traceability/require-req-annotation`) when you configure them directly. In `"inside"` mode, these rules treat only the first comment-only lines inside function and method bodies as satisfying the annotation requirement; JSDoc and before-function comments are ignored for block-bodied functions and methods, while TypeScript declarations and signature-only nodes continue to use before-node annotations.
|
|
121
|
+
|
|
120
122
|
- **Function-level (`traceability/require-story-annotation`, `traceability/require-req-annotation`)**
|
|
121
123
|
|
|
122
124
|
Function-level rules continue to accept annotations:
|
|
123
125
|
- As JSDoc blocks immediately preceding the function, or
|
|
124
126
|
- As line comments placed directly before the function declaration or expression.
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
- By default, annotations are still placed immediately before the function (JSDoc or line comments).
|
|
128
|
-
- When you configure `annotationPlacement: "inside"` on `traceability/require-story-annotation`, the rule prefers annotations as the first comment-only lines inside the function or method body, mirroring the branch-level inside-brace standard from Story 028.0. Declaration-only shapes such as `TSDeclareFunction` and `TSMethodSignature` remain before-function only, since they have no executable body.
|
|
128
|
+
This placement is stable and supported for all current versions. Future versions may introduce an **inside-brace** placement mode for function bodies (similar to branch blocks) to align function annotations with the branch-level `"inside"` standard, but that behaviour is not yet implemented in the current release.
|
|
129
129
|
|
|
130
130
|
For full configuration details and migration guidance between placement styles, see:
|
|
131
131
|
|
|
@@ -38,6 +38,7 @@ import type { Rule } from "eslint";
|
|
|
38
38
|
type CoreReportOptions = {
|
|
39
39
|
annotationTemplateOverride?: string;
|
|
40
40
|
autoFixToggle?: boolean;
|
|
41
|
+
annotationPlacement?: "before" | "inside";
|
|
41
42
|
};
|
|
42
43
|
type ReportDeps = {
|
|
43
44
|
hasStoryAnnotation: (_sourceCode: any, _node: any) => boolean;
|
|
@@ -53,6 +54,7 @@ type ReportDeps = {
|
|
|
53
54
|
shouldApplyAutoFix: (_autoFix: boolean | undefined) => boolean;
|
|
54
55
|
createAddStoryFix: (_target: any, _annotationTemplate: string) => any;
|
|
55
56
|
createMethodFix: (_node: any, _annotationTemplate: string) => any;
|
|
57
|
+
hasStoryAnnotationWithPlacement?: (_sourceCode: any, _node: any, _placement: "before" | "inside") => boolean;
|
|
56
58
|
};
|
|
57
59
|
/**
|
|
58
60
|
* Core helper to report a missing @story annotation for a function-like node.
|
|
@@ -158,11 +158,13 @@ function coreReportMissing(deps, context, sourceCode, config) {
|
|
|
158
158
|
const { node, target: passedTarget, options = {} } = config;
|
|
159
159
|
withSafeReporting("coreReportMissing", () => {
|
|
160
160
|
const annotationPlacement = resolveAnnotationPlacement(options);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
const hasWithPlacement = deps.hasStoryAnnotationWithPlacement;
|
|
162
|
+
if (typeof hasWithPlacement === "function") {
|
|
163
|
+
if (hasWithPlacement(sourceCode, node, annotationPlacement)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
164
166
|
}
|
|
165
|
-
if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
167
|
+
else if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
166
168
|
return;
|
|
167
169
|
}
|
|
168
170
|
const functionName = deps.getReportedFunctionName(node);
|
|
@@ -18,6 +18,7 @@ import { type CallbackExclusionOptions } from "./test-callback-exclusion";
|
|
|
18
18
|
interface ReportOptions extends CallbackExclusionOptions {
|
|
19
19
|
annotationTemplateOverride?: string;
|
|
20
20
|
autoFixToggle?: boolean;
|
|
21
|
+
annotationPlacement?: "before" | "inside";
|
|
21
22
|
}
|
|
22
23
|
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
23
24
|
declare function getAnnotationTemplate(override?: string, _options?: CallbackExclusionOptions): string;
|
|
@@ -53,7 +54,24 @@ declare function leadingCommentsHasStory(node: any): boolean;
|
|
|
53
54
|
* @req REQ-ANNOTATION-REQUIRED - Detect existing story annotations in JSDoc or comments
|
|
54
55
|
*/
|
|
55
56
|
declare function hasStoryAnnotation(sourceCode: any, node: any): boolean;
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Placement-aware story detection helper used by core reporting.
|
|
59
|
+
*
|
|
60
|
+
* When annotationPlacement is "inside" and the node supports inside-brace
|
|
61
|
+
* semantics, this helper only treats annotations found on the first
|
|
62
|
+
* comment-only lines inside the function or method body as satisfying the
|
|
63
|
+
* requirement. JSDoc and before-function comments are intentionally ignored so
|
|
64
|
+
* that misplaced annotations are reported as violations under the inside
|
|
65
|
+
* standard.
|
|
66
|
+
*
|
|
67
|
+
* For nodes that do not support inside placement (such as TS declarations,
|
|
68
|
+
* signature-only nodes, or functions without block bodies), this helper
|
|
69
|
+
* delegates to the existing hasStoryAnnotation heuristics so that they
|
|
70
|
+
* continue to rely on before-function placement.
|
|
71
|
+
*
|
|
72
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-ALL-BLOCK-TYPES REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
73
|
+
*/
|
|
74
|
+
declare function hasStoryAnnotationWithPlacement(sourceCode: any, node: any, annotationPlacement: "before" | "inside"): boolean;
|
|
57
75
|
/**
|
|
58
76
|
* Determine AST node where annotation should be inserted
|
|
59
77
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.getNodeName = exports.
|
|
3
|
+
exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.getNodeName = exports.STORY_PATH = void 0;
|
|
4
4
|
exports.getAnnotationTemplate = getAnnotationTemplate;
|
|
5
5
|
exports.shouldApplyAutoFix = shouldApplyAutoFix;
|
|
6
6
|
exports.isExportedNode = isExportedNode;
|
|
@@ -8,6 +8,7 @@ exports.jsdocHasStory = jsdocHasStory;
|
|
|
8
8
|
exports.commentsBeforeHasStory = commentsBeforeHasStory;
|
|
9
9
|
exports.leadingCommentsHasStory = leadingCommentsHasStory;
|
|
10
10
|
exports.hasStoryAnnotation = hasStoryAnnotation;
|
|
11
|
+
exports.hasStoryAnnotationWithPlacement = hasStoryAnnotationWithPlacement;
|
|
11
12
|
exports.extractName = extractName;
|
|
12
13
|
exports.resolveTargetNode = resolveTargetNode;
|
|
13
14
|
exports.shouldProcessNode = shouldProcessNode;
|
|
@@ -19,6 +20,7 @@ Object.defineProperty(exports, "parentChainHasStory", { enumerable: true, get: f
|
|
|
19
20
|
Object.defineProperty(exports, "fallbackTextBeforeHasStory", { enumerable: true, get: function () { return require_story_io_1.fallbackTextBeforeHasStory; } });
|
|
20
21
|
const require_story_utils_1 = require("./require-story-utils");
|
|
21
22
|
Object.defineProperty(exports, "getNodeName", { enumerable: true, get: function () { return require_story_utils_1.getNodeName; } });
|
|
23
|
+
const function_annotation_helpers_1 = require("../../utils/function-annotation-helpers");
|
|
22
24
|
const require_story_core_1 = require("./require-story-core");
|
|
23
25
|
Object.defineProperty(exports, "DEFAULT_SCOPE", { enumerable: true, get: function () { return require_story_core_1.DEFAULT_SCOPE; } });
|
|
24
26
|
Object.defineProperty(exports, "EXPORT_PRIORITY_VALUES", { enumerable: true, get: function () { return require_story_core_1.EXPORT_PRIORITY_VALUES; } });
|
|
@@ -220,9 +222,49 @@ function hasStoryAnnotation(sourceCode, node) {
|
|
|
220
222
|
}
|
|
221
223
|
return false;
|
|
222
224
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Placement-aware story detection helper used by core reporting.
|
|
227
|
+
*
|
|
228
|
+
* When annotationPlacement is "inside" and the node supports inside-brace
|
|
229
|
+
* semantics, this helper only treats annotations found on the first
|
|
230
|
+
* comment-only lines inside the function or method body as satisfying the
|
|
231
|
+
* requirement. JSDoc and before-function comments are intentionally ignored so
|
|
232
|
+
* that misplaced annotations are reported as violations under the inside
|
|
233
|
+
* standard.
|
|
234
|
+
*
|
|
235
|
+
* For nodes that do not support inside placement (such as TS declarations,
|
|
236
|
+
* signature-only nodes, or functions without block bodies), this helper
|
|
237
|
+
* delegates to the existing hasStoryAnnotation heuristics so that they
|
|
238
|
+
* continue to rely on before-function placement.
|
|
239
|
+
*
|
|
240
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-ALL-BLOCK-TYPES REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
|
|
241
|
+
*/
|
|
242
|
+
function hasStoryAnnotationWithPlacement(sourceCode, node, annotationPlacement) {
|
|
243
|
+
// Backward-compatible default: use existing heuristics when placement is
|
|
244
|
+
// "before" or when the function does not support inside-brace semantics.
|
|
245
|
+
if (annotationPlacement !== "inside" ||
|
|
246
|
+
!(0, function_annotation_helpers_1.supportsInsidePlacementForFunction)(node)) {
|
|
247
|
+
return hasStoryAnnotation(sourceCode, node);
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const insideText = (0, function_annotation_helpers_1.getFunctionInsideBodyCommentText)(sourceCode, node);
|
|
251
|
+
if (typeof insideText === "string" &&
|
|
252
|
+
(insideText.includes("@story") || insideText.includes("@supports"))) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
258
|
+
// Debug logging only when explicitly enabled for troubleshooting helper failures.
|
|
259
|
+
console.error("[traceability] hasStoryAnnotationWithPlacement failed for node", error?.message ?? error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// In inside-placement mode for block-bodied functions and methods we
|
|
263
|
+
// intentionally do not fall back to before-function heuristics; callers
|
|
264
|
+
// should treat this as a missing annotation so that misplaced comments are
|
|
265
|
+
// reported as violations.
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
226
268
|
/**
|
|
227
269
|
* Determine AST node where annotation should be inserted
|
|
228
270
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -375,6 +417,7 @@ function resolveAnnotationTargetNode(sourceCode, node, passedTarget) {
|
|
|
375
417
|
function reportMissing(context, sourceCode, config) {
|
|
376
418
|
(0, require_story_core_1.coreReportMissing)({
|
|
377
419
|
hasStoryAnnotation,
|
|
420
|
+
hasStoryAnnotationWithPlacement,
|
|
378
421
|
getReportedFunctionName,
|
|
379
422
|
resolveAnnotationTargetNode,
|
|
380
423
|
getNameNodeForReport,
|
|
@@ -390,6 +433,7 @@ function reportMissing(context, sourceCode, config) {
|
|
|
390
433
|
function reportMethod(context, sourceCode, config) {
|
|
391
434
|
(0, require_story_core_1.coreReportMethod)({
|
|
392
435
|
hasStoryAnnotation,
|
|
436
|
+
hasStoryAnnotationWithPlacement,
|
|
393
437
|
getReportedFunctionName,
|
|
394
438
|
resolveAnnotationTargetNode,
|
|
395
439
|
getNameNodeForReport,
|
|
@@ -33,6 +33,7 @@ function buildFunctionDeclarationVisitor(context, sourceCode, options) {
|
|
|
33
33
|
options: {
|
|
34
34
|
annotationTemplateOverride: options.annotationTemplate,
|
|
35
35
|
autoFixToggle: options.autoFix,
|
|
36
|
+
annotationPlacement: options.annotationPlacement,
|
|
36
37
|
},
|
|
37
38
|
});
|
|
38
39
|
}
|
|
@@ -68,6 +69,7 @@ function buildFunctionExpressionVisitor(context, sourceCode, options) {
|
|
|
68
69
|
options: {
|
|
69
70
|
annotationTemplateOverride: options.annotationTemplate,
|
|
70
71
|
autoFixToggle: options.autoFix,
|
|
72
|
+
annotationPlacement: options.annotationPlacement,
|
|
71
73
|
},
|
|
72
74
|
});
|
|
73
75
|
}
|
|
@@ -96,6 +98,7 @@ function buildArrowFunctionVisitor(context, sourceCode, options) {
|
|
|
96
98
|
options: {
|
|
97
99
|
annotationTemplateOverride: options.annotationTemplate,
|
|
98
100
|
autoFixToggle: options.autoFix,
|
|
101
|
+
annotationPlacement: options.annotationPlacement,
|
|
99
102
|
},
|
|
100
103
|
});
|
|
101
104
|
}
|
|
@@ -123,6 +126,7 @@ function buildTSDeclareFunctionVisitor(context, sourceCode, options) {
|
|
|
123
126
|
options: {
|
|
124
127
|
annotationTemplateOverride: options.annotationTemplate,
|
|
125
128
|
autoFixToggle: options.autoFix,
|
|
129
|
+
annotationPlacement: options.annotationPlacement,
|
|
126
130
|
},
|
|
127
131
|
});
|
|
128
132
|
}
|
|
@@ -151,6 +155,7 @@ function buildTSMethodSignatureVisitor(context, sourceCode, options) {
|
|
|
151
155
|
options: {
|
|
152
156
|
annotationTemplateOverride: options.methodAnnotationTemplate ?? options.annotationTemplate,
|
|
153
157
|
autoFixToggle: options.autoFix,
|
|
158
|
+
annotationPlacement: options.annotationPlacement,
|
|
154
159
|
},
|
|
155
160
|
});
|
|
156
161
|
}
|
|
@@ -177,6 +182,7 @@ function buildMethodDefinitionVisitor(context, sourceCode, options) {
|
|
|
177
182
|
options: {
|
|
178
183
|
annotationTemplateOverride: options.methodAnnotationTemplate ?? options.annotationTemplate,
|
|
179
184
|
autoFixToggle: options.autoFix,
|
|
185
|
+
annotationPlacement: options.annotationPlacement,
|
|
180
186
|
},
|
|
181
187
|
});
|
|
182
188
|
}
|
|
@@ -21,6 +21,7 @@ import type { TSESTree } from "@typescript-eslint/utils";
|
|
|
21
21
|
interface CallbackExclusionOptions {
|
|
22
22
|
excludeTestCallbacks?: boolean;
|
|
23
23
|
additionalTestHelperNames?: string[];
|
|
24
|
+
annotationPlacement?: "before" | "inside";
|
|
24
25
|
}
|
|
25
26
|
type TraceabilityNodeWithParent = TSESTree.Node & {
|
|
26
27
|
parent?: TraceabilityNodeWithParent | null;
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* @req REQ-CONFIGURABLE-SCOPE - Allow configuration of which exports are checked
|
|
8
8
|
* @req REQ-EXPORT-PRIORITY - Allow configuration of export priority behavior
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* require-story-annotation
|
|
12
|
-
* the function (no inside-function support yet).
|
|
10
|
+
* This rule honors the same `annotationPlacement` semantics as
|
|
11
|
+
* `require-story-annotation` for block-bodied functions and methods.
|
|
13
12
|
*/
|
|
14
13
|
import type { Rule } from "eslint";
|
|
15
14
|
declare const rule: Rule.RuleModule;
|
|
@@ -59,7 +59,13 @@ const rule = {
|
|
|
59
59
|
const rawScope = options?.scope;
|
|
60
60
|
const scope = Array.isArray(rawScope) && rawScope.length > 0 ? rawScope : require_story_helpers_1.DEFAULT_SCOPE;
|
|
61
61
|
const exportPriority = options?.exportPriority ?? "all";
|
|
62
|
-
const
|
|
62
|
+
const rawAnnotationPlacement = options?.annotationPlacement;
|
|
63
|
+
const annotationPlacement = rawAnnotationPlacement === "inside" || rawAnnotationPlacement === "before"
|
|
64
|
+
? rawAnnotationPlacement
|
|
65
|
+
: "before";
|
|
66
|
+
const shouldCheck = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, {
|
|
67
|
+
annotationPlacement,
|
|
68
|
+
});
|
|
63
69
|
/**
|
|
64
70
|
* Helper to conditionally run the annotation check only when the node
|
|
65
71
|
* should be processed according to scope/exportPriority.
|
|
@@ -67,7 +73,10 @@ const rule = {
|
|
|
67
73
|
const runCheck = (node) => {
|
|
68
74
|
if (!shouldCheck(node))
|
|
69
75
|
return;
|
|
70
|
-
(0, annotation_checker_1.checkReqAnnotation)(context, node, {
|
|
76
|
+
(0, annotation_checker_1.checkReqAnnotation)(context, node, {
|
|
77
|
+
enableFix: false,
|
|
78
|
+
annotationPlacement,
|
|
79
|
+
});
|
|
71
80
|
};
|
|
72
81
|
return {
|
|
73
82
|
/**
|
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const require_story_visitors_1 = require("./helpers/require-story-visitors");
|
|
4
4
|
const require_story_helpers_1 = require("./helpers/require-story-helpers");
|
|
5
|
+
function getNormalizedOptions(context) {
|
|
6
|
+
const sourceCode = context.getSourceCode();
|
|
7
|
+
const opts = (context.options && context.options[0]) || {};
|
|
8
|
+
const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
|
|
9
|
+
const exportPriority = opts.exportPriority || "all";
|
|
10
|
+
const annotationTemplate = typeof opts.annotationTemplate === "string" &&
|
|
11
|
+
opts.annotationTemplate.trim().length > 0
|
|
12
|
+
? opts.annotationTemplate.trim()
|
|
13
|
+
: undefined;
|
|
14
|
+
const methodAnnotationTemplate = typeof opts.methodAnnotationTemplate === "string" &&
|
|
15
|
+
opts.methodAnnotationTemplate.trim().length > 0
|
|
16
|
+
? opts.methodAnnotationTemplate.trim()
|
|
17
|
+
: undefined;
|
|
18
|
+
const autoFix = typeof opts.autoFix === "boolean" ? opts.autoFix : true;
|
|
19
|
+
const excludeTestCallbacks = typeof opts.excludeTestCallbacks === "boolean"
|
|
20
|
+
? opts.excludeTestCallbacks
|
|
21
|
+
: true;
|
|
22
|
+
const additionalTestHelperNames = Array.isArray(opts.additionalTestHelperNames) &&
|
|
23
|
+
opts.additionalTestHelperNames.every((name) => typeof name === "string")
|
|
24
|
+
? opts.additionalTestHelperNames
|
|
25
|
+
: undefined;
|
|
26
|
+
const rawAnnotationPlacement = opts.annotationPlacement;
|
|
27
|
+
const annotationPlacement = rawAnnotationPlacement === "inside" || rawAnnotationPlacement === "before"
|
|
28
|
+
? rawAnnotationPlacement
|
|
29
|
+
: "before";
|
|
30
|
+
return {
|
|
31
|
+
sourceCode,
|
|
32
|
+
scope,
|
|
33
|
+
exportPriority,
|
|
34
|
+
annotationTemplate,
|
|
35
|
+
methodAnnotationTemplate,
|
|
36
|
+
autoFix,
|
|
37
|
+
excludeTestCallbacks,
|
|
38
|
+
additionalTestHelperNames,
|
|
39
|
+
annotationPlacement,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
5
42
|
/**
|
|
6
43
|
* ESLint rule to require @story annotations on functions/methods.
|
|
7
44
|
*
|
|
@@ -58,6 +95,12 @@ const rule = {
|
|
|
58
95
|
items: { type: "string" },
|
|
59
96
|
uniqueItems: true,
|
|
60
97
|
},
|
|
98
|
+
/**
|
|
99
|
+
* @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT REQ-ALL-BLOCK-TYPES
|
|
100
|
+
*/
|
|
101
|
+
annotationPlacement: {
|
|
102
|
+
enum: ["before", "inside"],
|
|
103
|
+
},
|
|
61
104
|
},
|
|
62
105
|
additionalProperties: false,
|
|
63
106
|
},
|
|
@@ -72,26 +115,7 @@ const rule = {
|
|
|
72
115
|
* @req REQ-AUTOFIX-MISSING - The create hook wires in visitors that are capable of providing auto-fix suggestions for missing @story annotations.
|
|
73
116
|
*/
|
|
74
117
|
create(context) {
|
|
75
|
-
const sourceCode = context
|
|
76
|
-
const opts = (context.options && context.options[0]) || {};
|
|
77
|
-
const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
|
|
78
|
-
const exportPriority = opts.exportPriority || "all";
|
|
79
|
-
const annotationTemplate = typeof opts.annotationTemplate === "string" &&
|
|
80
|
-
opts.annotationTemplate.trim().length > 0
|
|
81
|
-
? opts.annotationTemplate.trim()
|
|
82
|
-
: undefined;
|
|
83
|
-
const methodAnnotationTemplate = typeof opts.methodAnnotationTemplate === "string" &&
|
|
84
|
-
opts.methodAnnotationTemplate.trim().length > 0
|
|
85
|
-
? opts.methodAnnotationTemplate.trim()
|
|
86
|
-
: undefined;
|
|
87
|
-
const autoFix = typeof opts.autoFix === "boolean" ? opts.autoFix : true;
|
|
88
|
-
const excludeTestCallbacks = typeof opts.excludeTestCallbacks === "boolean"
|
|
89
|
-
? opts.excludeTestCallbacks
|
|
90
|
-
: true;
|
|
91
|
-
const additionalTestHelperNames = Array.isArray(opts.additionalTestHelperNames) &&
|
|
92
|
-
opts.additionalTestHelperNames.every((name) => typeof name === "string")
|
|
93
|
-
? opts.additionalTestHelperNames
|
|
94
|
-
: undefined;
|
|
118
|
+
const { sourceCode, scope, exportPriority, annotationTemplate, methodAnnotationTemplate, autoFix, excludeTestCallbacks, additionalTestHelperNames, annotationPlacement, } = getNormalizedOptions(context);
|
|
95
119
|
/**
|
|
96
120
|
* Optional debug logging for troubleshooting this rule.
|
|
97
121
|
* Developers can temporarily uncomment the block below to log when the rule
|
|
@@ -110,6 +134,7 @@ const rule = {
|
|
|
110
134
|
const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, {
|
|
111
135
|
excludeTestCallbacks,
|
|
112
136
|
additionalTestHelperNames,
|
|
137
|
+
annotationPlacement,
|
|
113
138
|
});
|
|
114
139
|
// Delegate visitor construction to helper to keep this file concise.
|
|
115
140
|
return (0, require_story_visitors_1.buildVisitors)(context, sourceCode, {
|
|
@@ -121,6 +146,7 @@ const rule = {
|
|
|
121
146
|
autoFix,
|
|
122
147
|
excludeTestCallbacks,
|
|
123
148
|
additionalTestHelperNames,
|
|
149
|
+
annotationPlacement,
|
|
124
150
|
});
|
|
125
151
|
},
|
|
126
152
|
};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.checkReqAnnotation = checkReqAnnotation;
|
|
4
4
|
const require_story_utils_1 = require("../rules/helpers/require-story-utils");
|
|
5
5
|
const reqAnnotationDetection_1 = require("./reqAnnotationDetection");
|
|
6
|
+
const function_annotation_helpers_1 = require("./function-annotation-helpers");
|
|
6
7
|
/**
|
|
7
8
|
* Helper to retrieve the JSDoc comment for a node.
|
|
8
9
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -180,7 +181,19 @@ function reportMissing(context, node, enableFix = true) {
|
|
|
180
181
|
*/
|
|
181
182
|
function checkReqAnnotation(context, node, options) {
|
|
182
183
|
const { enableFix = true } = options ?? {};
|
|
184
|
+
const annotationPlacement = options?.annotationPlacement === "inside" ||
|
|
185
|
+
options?.annotationPlacement === "before"
|
|
186
|
+
? options.annotationPlacement
|
|
187
|
+
: "before";
|
|
183
188
|
const sourceCode = context.getSourceCode();
|
|
189
|
+
if (annotationPlacement === "inside" &&
|
|
190
|
+
(0, function_annotation_helpers_1.supportsInsidePlacementForFunction)(node)) {
|
|
191
|
+
const insideText = (0, function_annotation_helpers_1.getFunctionInsideBodyCommentText)(sourceCode, node);
|
|
192
|
+
if (typeof insideText === "string" &&
|
|
193
|
+
(insideText.includes("@req") || insideText.includes("@supports"))) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
184
197
|
const jsdoc = getJsdocComment(sourceCode, node);
|
|
185
198
|
const leading = getLeadingComments(node);
|
|
186
199
|
const comments = getCommentsBefore(sourceCode, node);
|
|
@@ -19,6 +19,7 @@ describe("ESLint Configuration Rule Options (Story 002.0-DEV-ESLINT-CONFIG)", ()
|
|
|
19
19
|
const schema = require_story_annotation_1.default.meta.schema[0];
|
|
20
20
|
expect(schema.properties).toHaveProperty("scope");
|
|
21
21
|
expect(schema.properties).toHaveProperty("exportPriority");
|
|
22
|
+
expect(schema.properties).toHaveProperty("annotationPlacement");
|
|
22
23
|
expect(schema.additionalProperties).toBe(false);
|
|
23
24
|
});
|
|
24
25
|
});
|
|
@@ -109,6 +109,32 @@ describe("Unified require-traceability and aliases integration (Story 010.4-DEV-
|
|
|
109
109
|
expect(messages).toHaveLength(0);
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
|
+
it("[REQ-INSIDE-BRACE-PLACEMENT][REQ-ALL-BLOCK-TYPES] unified rule and aliases accept inside-brace annotations when annotationPlacement is 'inside'", async () => {
|
|
113
|
+
const codeWithInsideAnnotations = `function foo() {\n // @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-FN-INSIDE\n return 1;\n}`;
|
|
114
|
+
const config = [
|
|
115
|
+
{
|
|
116
|
+
rules: {
|
|
117
|
+
"traceability/require-traceability": "error",
|
|
118
|
+
"traceability/require-story-annotation": [
|
|
119
|
+
"error",
|
|
120
|
+
{
|
|
121
|
+
annotationPlacement: "inside",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
"traceability/require-req-annotation": [
|
|
125
|
+
"error",
|
|
126
|
+
{
|
|
127
|
+
annotationPlacement: "inside",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
const result = await lintTextWithConfig(codeWithInsideAnnotations, "example.js", config);
|
|
134
|
+
const ruleIds = result.messages.map((m) => m.ruleId);
|
|
135
|
+
expect(ruleIds).not.toContain("traceability/require-story-annotation");
|
|
136
|
+
expect(ruleIds).not.toContain("traceability/require-req-annotation");
|
|
137
|
+
});
|
|
112
138
|
it("[REQ-PRESETS-CANONICAL-RULE] recommended preset surfaces unified and legacy diagnostics together for missing annotations", async () => {
|
|
113
139
|
const result = await lintTextWithConfig(codeMissingAll, "example.js", index_1.configs.recommended);
|
|
114
140
|
const ruleIds = result.messages.map((m) => m.ruleId).sort();
|
|
@@ -137,6 +137,26 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
|
|
|
137
137
|
code: `class C {\n /** @req REQ-EXAMPLE */\n m() {}\n}`,
|
|
138
138
|
options: [{ exportPriority: "non-exported" }],
|
|
139
139
|
},
|
|
140
|
+
{
|
|
141
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-ALL-BLOCK-TYPES] function-level @supports requirement inside body is valid when annotationPlacement is 'inside'",
|
|
142
|
+
code: `function withInsideReq() {\n // @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-FN-INSIDE\n return 1;\n}`,
|
|
143
|
+
options: [{ annotationPlacement: "inside" }],
|
|
144
|
+
},
|
|
145
|
+
(0, ts_language_options_1.withTsLanguageOptions)({
|
|
146
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-ALL-BLOCK-TYPES] method-level @req inside body is valid when annotationPlacement is 'inside'",
|
|
147
|
+
code: `class C {\n method() {\n // @req REQ-METHOD-INSIDE\n return 1;\n }\n}`,
|
|
148
|
+
options: [{ annotationPlacement: "inside" }],
|
|
149
|
+
}),
|
|
150
|
+
{
|
|
151
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT] before-function @req remains valid when annotationPlacement is 'inside' (REQ-REQ-PLACEMENT-BC)",
|
|
152
|
+
code: `/**\n * @req REQ-BEFORE-FN\n */\nfunction beforeReqOnly() {\n return 1;\n}`,
|
|
153
|
+
options: [{ annotationPlacement: "inside" }],
|
|
154
|
+
},
|
|
155
|
+
(0, ts_language_options_1.withTsLanguageOptions)({
|
|
156
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT] before-method @req remains valid when annotationPlacement is 'inside' (REQ-REQ-PLACEMENT-BC)",
|
|
157
|
+
code: `class C {\n /**\n * @req REQ-BEFORE-METHOD\n */\n method() {\n return 1;\n }\n}`,
|
|
158
|
+
options: [{ annotationPlacement: "inside" }],
|
|
159
|
+
}),
|
|
140
160
|
],
|
|
141
161
|
invalid: [
|
|
142
162
|
{
|
|
@@ -105,6 +105,21 @@ describe('Vitest suite', () => {
|
|
|
105
105
|
code: `withTestCase("does something", () => {});`,
|
|
106
106
|
options: [{ additionalTestHelperNames: ["withTestCase"] }],
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-ALL-BLOCK-TYPES] function-level @supports annotation inside body is valid when annotationPlacement is 'inside'",
|
|
110
|
+
code: `function insideAnnotated() {\n // @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-FN\n return 1;\n}`,
|
|
111
|
+
options: [{ annotationPlacement: "inside" }],
|
|
112
|
+
},
|
|
113
|
+
(0, ts_language_options_1.withTsLanguageOptions)({
|
|
114
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-ALL-BLOCK-TYPES] function-level @story annotation inside body is valid when annotationPlacement is 'inside' (TS)",
|
|
115
|
+
code: `function insideAnnotatedTs() {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n return 1;\n}`,
|
|
116
|
+
options: [{ annotationPlacement: "inside" }],
|
|
117
|
+
}),
|
|
118
|
+
(0, ts_language_options_1.withTsLanguageOptions)({
|
|
119
|
+
name: "[REQ-INSIDE-BRACE-PLACEMENT] before-method @story remains valid when annotationPlacement is 'inside' (placement BC)",
|
|
120
|
+
code: `class C {\n /** @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md */\n method() {\n return 1;\n }\n}`,
|
|
121
|
+
options: [{ annotationPlacement: "inside" }],
|
|
122
|
+
}),
|
|
108
123
|
],
|
|
109
124
|
invalid: [
|
|
110
125
|
{
|
|
@@ -237,6 +252,17 @@ describe('Vitest suite', () => {
|
|
|
237
252
|
},
|
|
238
253
|
],
|
|
239
254
|
},
|
|
255
|
+
{
|
|
256
|
+
name: "[REQ-BEFORE-BRACE-ERROR][REQ-INSIDE-BRACE-PLACEMENT] before-function annotation is ignored when annotationPlacement is 'inside'",
|
|
257
|
+
code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\nfunction beforeOnly() {\n return 1;\n}`,
|
|
258
|
+
options: [{ annotationPlacement: "inside", autoFix: false }],
|
|
259
|
+
errors: [
|
|
260
|
+
{
|
|
261
|
+
messageId: "missingStory",
|
|
262
|
+
suggestions: 1, // satisfy TypeScript's SuggestionOutput[] typing while asserting suggestion count
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
240
266
|
],
|
|
241
267
|
});
|
|
242
268
|
ruleTester.run("require-story-annotation with exportPriority option", require_story_annotation_1.default, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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",
|
|
@@ -26,7 +26,7 @@ For function-level traceability, the plugin exposes a unified rule and two legac
|
|
|
26
26
|
- `traceability/require-traceability` is the **canonical function-level rule** for new configurations. It ensures functions and methods have both story coverage and requirement coverage, and it accepts either `@supports` (preferred) or legacy `@story` / `@req` annotations.
|
|
27
27
|
- `traceability/require-story-annotation` and `traceability/require-req-annotation` are **backward-compatible aliases** that focus on the story and requirement aspects separately. They are retained for existing configurations and share the same underlying implementation model as the unified rule, but new ESLint configs should normally rely on `traceability/require-traceability` rather than enabling these legacy keys directly.
|
|
28
28
|
|
|
29
|
-
All three rule keys can still be configured individually if you need fine-grained control (for example, to tune severities separately), but the recommended and strict presets enable `traceability/require-traceability` by default and keep the legacy keys primarily for projects that adopted them before the unified rule existed.
|
|
29
|
+
All three rule keys can still be configured individually if you need fine-grained control (for example, to tune severities separately), but the recommended and strict presets enable `traceability/require-traceability` by default and keep the legacy keys primarily for projects that adopted them before the unified rule existed. Current releases support `annotationPlacement` on both the branch-level rule and the function-level rules, so you can standardize on inside-brace placement for if/else/try/catch/switch/function/loop blocks. When you leave `annotationPlacement` unspecified, all rules default to the historical before-function/before-branch behavior for backward compatibility.
|
|
30
30
|
|
|
31
31
|
### traceability/require-traceability
|
|
32
32
|
|
|
@@ -44,7 +44,7 @@ Options:
|
|
|
44
44
|
- `methodAnnotationTemplate` (string, optional) – Overrides the default placeholder JSDoc used when inserting missing `@story` annotations for class methods and TypeScript method signatures. When omitted or blank, falls back to `annotationTemplate` if provided, otherwise the built-in template.
|
|
45
45
|
- `autoFix` (boolean, optional) – When set to `false`, disables all automatic fix behavior for this rule while retaining its suggestions and diagnostics. When omitted or `true`, the rule behaves as before, inserting placeholder annotations in `--fix` mode.
|
|
46
46
|
- `excludeTestCallbacks` (boolean, optional) – When `true` (default), excludes anonymous arrow functions that are direct callbacks to common test framework functions (for example, Jest/Mocha/Vitest `describe`/`it`/`test`/`beforeEach`/`afterEach`/`beforeAll`/`afterAll`, plus focused/skipped/concurrent variants such as `fdescribe`, `xdescribe`, `fit`, `xit`, `test.concurrent`, `describe.concurrent`) from function-level annotation requirements. This assumes those test files are already covered by file-level `@supports` annotations and `traceability/require-test-traceability`. When set to `false`, these callbacks are treated like any other arrow function and must be annotated when in-scope.
|
|
47
|
-
- `annotationPlacement` ("before" | "inside", optional)
|
|
47
|
+
- `annotationPlacement` ("before" | "inside", optional) Controls whether the rule treats before-function comments as the source of annotations (`"before"`, the default and backward-compatible behavior) or instead requires annotations as the first comment-only lines inside function and method bodies (`"inside"`). In `"inside"` mode, JSDoc and before-function comments are ignored for block-bodied functions and methods, and only inside-body comments are considered; declaration-only nodes (e.g., TS signatures) continue to use before-node annotations.
|
|
48
48
|
|
|
49
49
|
Default Severity: `error`
|
|
50
50
|
Example:
|
|
@@ -71,7 +71,7 @@ Options:
|
|
|
71
71
|
|
|
72
72
|
- `scope` (string[], optional) – Controls which function-like node types are required to have @req annotations. Allowed values: "FunctionDeclaration", "FunctionExpression", "MethodDefinition", "TSDeclareFunction", "TSMethodSignature". Default: ["FunctionDeclaration", "FunctionExpression", "MethodDefinition", "TSDeclareFunction", "TSMethodSignature"].
|
|
73
73
|
- `exportPriority` ("all" | "exported" | "non-exported", optional) – Controls whether the rule checks all functions, only exported ones, or only non-exported ones. Default: "all".
|
|
74
|
-
- `annotationPlacement` ("before" | "inside", optional)
|
|
74
|
+
- `annotationPlacement` ("before" | "inside", optional) Controls whether the rule treats before-function comments as the source of annotations (`"before"`, the default and backward-compatible behavior) or instead requires annotations as the first comment-only lines inside function and method bodies (`"inside"`). In `"inside"` mode, JSDoc and before-function comments are ignored for block-bodied functions and methods, and only inside-body comments are considered; declaration-only nodes (e.g., TS signatures) continue to use before-node annotations.
|
|
75
75
|
|
|
76
76
|
Default Severity: `error`
|
|
77
77
|
Example (with both `@story` and `@req`, as typically used when both rules are enabled):
|
|
@@ -115,7 +115,7 @@ Behavior notes:
|
|
|
115
115
|
Placement modes:
|
|
116
116
|
|
|
117
117
|
- `"before"` mode preserves the existing semantics described above, including the dual-position behavior for `catch` and `else if` branches where comments immediately before the branch and the first comment-only lines inside the block are both acceptable and validated according to their existing precedence rules.
|
|
118
|
-
- `"inside"` mode standardizes on the first comment-only lines inside supported branch blocks (`if`/`else if`, loops, `catch`, and `try`) for validation and auto-fix insertion. This placement is designed to work well with Prettier and other formatters, while the current implementation still treats many before-branch annotations as needing migration and may, in corner cases where it cannot confidently rewrite placement, insert new placeholder comments above the branch rather than moving existing ones. Story `028.0-DEV-
|
|
118
|
+
- `"inside"` mode standardizes on the first comment-only lines inside supported branch blocks (`if`/`else if`, loops, `catch`, and `try`) for validation and auto-fix insertion. This placement is designed to work well with Prettier and other formatters, while the current implementation still treats many before-branch annotations as needing migration and may, in corner cases where it cannot confidently rewrite placement, insert new placeholder comments above the branch rather than moving existing ones. Story `028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION` standardizes inside-brace placement for supported branch constructs; function-level rules continue to use before-function annotations until a future story extends the same standard to functions.
|
|
119
119
|
|
|
120
120
|
For a concrete illustration of how these rules interact with Prettier, see the formatter-aware if/else/else-if example in [user-docs/examples.md](examples.md) (section **6. Branch annotations with if/else/else-if and Prettier**), which shows both the hand-written and formatted code that the rule considers valid.
|
|
121
121
|
|