eslint-plugin-traceability 1.6.0 → 1.6.2

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.
@@ -2,6 +2,7 @@
2
2
  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
+ const require_story_io_1 = require("../rules/helpers/require-story-io");
5
6
  /**
6
7
  * Helper to retrieve the JSDoc comment for a node.
7
8
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -42,17 +43,129 @@ function combineComments(leading, before) {
42
43
  function commentContainsReq(c) {
43
44
  return c && typeof c.value === "string" && c.value.includes("@req");
44
45
  }
46
+ /**
47
+ * Line-based helper adapted from linesBeforeHasStory to detect @req.
48
+ */
49
+ function linesBeforeHasReq(sourceCode, node) {
50
+ const lines = sourceCode && sourceCode.lines;
51
+ const startLine = node && node.loc && typeof node.loc.start?.line === "number"
52
+ ? node.loc.start.line
53
+ : null;
54
+ if (!Array.isArray(lines) || typeof startLine !== "number") {
55
+ return false;
56
+ }
57
+ const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
58
+ const to = Math.max(0, startLine - 1);
59
+ for (let i = from; i < to; i++) {
60
+ const text = lines[i];
61
+ if (typeof text === "string" && text.includes("@req")) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ /**
68
+ * Parent-chain helper adapted from parentChainHasStory to detect @req.
69
+ */
70
+ function parentChainHasReq(sourceCode, node) {
71
+ let p = node && node.parent;
72
+ while (p) {
73
+ const pComments = typeof sourceCode?.getCommentsBefore === "function"
74
+ ? sourceCode.getCommentsBefore(p) || []
75
+ : [];
76
+ if (Array.isArray(pComments) &&
77
+ pComments.some((c) => typeof c.value === "string" && c.value.includes("@req"))) {
78
+ return true;
79
+ }
80
+ const pLeading = p.leadingComments || [];
81
+ if (Array.isArray(pLeading) &&
82
+ pLeading.some((c) => typeof c.value === "string" && c.value.includes("@req"))) {
83
+ return true;
84
+ }
85
+ p = p.parent;
86
+ }
87
+ return false;
88
+ }
89
+ /**
90
+ * Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
91
+ */
92
+ function fallbackTextBeforeHasReq(sourceCode, node) {
93
+ if (typeof sourceCode?.getText !== "function" ||
94
+ !Array.isArray((node && node.range) || [])) {
95
+ return false;
96
+ }
97
+ const range = node.range;
98
+ if (!Array.isArray(range) || typeof range[0] !== "number") {
99
+ return false;
100
+ }
101
+ try {
102
+ const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
103
+ const textBefore = sourceCode.getText().slice(start, range[0]);
104
+ if (typeof textBefore === "string" && textBefore.includes("@req")) {
105
+ return true;
106
+ }
107
+ }
108
+ catch {
109
+ /* noop */
110
+ }
111
+ return false;
112
+ }
45
113
  /**
46
114
  * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
47
115
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
48
116
  * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
49
117
  */
50
- function hasReqAnnotation(jsdoc, comments) {
118
+ function hasReqAnnotation(jsdoc, comments, context, node) {
119
+ try {
120
+ const sourceCode = context && typeof context.getSourceCode === "function"
121
+ ? context.getSourceCode()
122
+ : undefined;
123
+ if (sourceCode && node) {
124
+ if (linesBeforeHasReq(sourceCode, node) ||
125
+ parentChainHasReq(sourceCode, node) ||
126
+ fallbackTextBeforeHasReq(sourceCode, node)) {
127
+ return true;
128
+ }
129
+ }
130
+ }
131
+ catch {
132
+ // Swallow detection errors and fall through to simple checks.
133
+ }
134
+ // BRANCH @req detection on JSDoc or comments
135
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
136
+ // @req REQ-ANNOTATION-REQ-DETECTION
51
137
  return ((jsdoc &&
52
138
  typeof jsdoc.value === "string" &&
53
139
  jsdoc.value.includes("@req")) ||
54
140
  comments.some(commentContainsReq));
55
141
  }
142
+ /**
143
+ * Determine the most appropriate node to attach an inserted JSDoc to.
144
+ * Prefers outer function-like constructs such as methods, variable declarators,
145
+ * or wrapping expression statements for function expressions.
146
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
147
+ * @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
148
+ */
149
+ function getFixTargetNode(node) {
150
+ const parent = node && node.parent;
151
+ if (!parent) {
152
+ return node;
153
+ }
154
+ // If the node is part of a class/obj method definition, attach to the MethodDefinition
155
+ if (parent.type === "MethodDefinition") {
156
+ return parent;
157
+ }
158
+ // If the node is the init of a variable declarator, attach to the VariableDeclarator
159
+ if (parent.type === "VariableDeclarator" && parent.init === node) {
160
+ return parent;
161
+ }
162
+ // If the parent is an expression statement (e.g. IIFE or assigned via expression),
163
+ // attach to the outer ExpressionStatement.
164
+ if (parent.type === "ExpressionStatement") {
165
+ return parent;
166
+ }
167
+ return node;
168
+ }
56
169
  /**
57
170
  * Creates a fix function that inserts a missing @req JSDoc before the node.
58
171
  * Returned function is a proper named function so no inline arrow is used.
@@ -60,8 +173,9 @@ function hasReqAnnotation(jsdoc, comments) {
60
173
  * @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
61
174
  */
62
175
  function createMissingReqFix(node) {
176
+ const target = getFixTargetNode(node);
63
177
  return function missingReqFix(fixer) {
64
- return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
178
+ return fixer.insertTextBefore(target, "/** @req <REQ-ID> */\n");
65
179
  };
66
180
  }
67
181
  /**
@@ -72,30 +186,49 @@ function createMissingReqFix(node) {
72
186
  * @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
73
187
  * @req REQ-ERROR-SPECIFIC - Provide specific error details including node name
74
188
  * @req REQ-ERROR-LOCATION - Include contextual location information in errors
189
+ * @req REQ-ERROR-SUGGESTION - Provide actionable suggestions or fixes where possible
190
+ * @req REQ-ERROR-CONTEXT - Include contextual hints to help understand the error
75
191
  */
76
- function reportMissing(context, node) {
77
- const rawName = (0, require_story_utils_1.getNodeName)(node);
192
+ function reportMissing(context, node, enableFix = true) {
193
+ const rawName = (0, require_story_utils_1.getNodeName)(node) ?? (node && (0, require_story_utils_1.getNodeName)(node.parent));
78
194
  const name = rawName ?? "(anonymous)";
79
- context.report({
80
- node,
195
+ const nameNode = (node && node.id && node.id.type === "Identifier"
196
+ ? node.id
197
+ : node && node.key && node.key.type === "Identifier"
198
+ ? node.key
199
+ : node) ?? node;
200
+ const reportOptions = {
201
+ node: nameNode,
81
202
  messageId: "missingReq",
82
- data: { name },
83
- fix: createMissingReqFix(node),
84
- });
203
+ data: { name, functionName: name },
204
+ };
205
+ if (enableFix) {
206
+ reportOptions.fix = createMissingReqFix(node);
207
+ }
208
+ context.report(reportOptions);
85
209
  }
86
210
  /**
87
211
  * Helper to check @req annotation presence on TS declare functions and method signatures.
212
+ * This helper is intentionally scope/exportPriority agnostic and focuses solely
213
+ * on detection and reporting of @req annotations for the given node.
88
214
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
89
215
  * @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
216
+ * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
217
+ * @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
90
218
  */
91
- function checkReqAnnotation(context, node) {
219
+ function checkReqAnnotation(context, node, options) {
220
+ const { enableFix = true } = options ?? {};
92
221
  const sourceCode = context.getSourceCode();
93
222
  const jsdoc = getJsdocComment(sourceCode, node);
94
223
  const leading = getLeadingComments(node);
95
224
  const comments = getCommentsBefore(sourceCode, node);
96
225
  const all = combineComments(leading, comments);
97
- const hasReq = hasReqAnnotation(jsdoc, all);
226
+ const hasReq = hasReqAnnotation(jsdoc, all, context, node);
227
+ // BRANCH when a @req annotation is missing and must be reported
228
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
229
+ // @req REQ-ANNOTATION-REQ-DETECTION
230
+ // @req REQ-ANNOTATION-REPORTING
98
231
  if (!hasReq) {
99
- reportMissing(context, node);
232
+ reportMissing(context, node, enableFix);
100
233
  }
101
234
  }
@@ -39,6 +39,6 @@ describe("CLI Error Handling for Traceability Plugin (Story 001.0-DEV-PLUGIN-SET
39
39
  });
40
40
  // Expect non-zero exit and missing annotation message on stdout
41
41
  expect(result.status).not.toBe(0);
42
- expect(result.stdout).toContain("Missing @story annotation");
42
+ expect(result.stdout).toContain("Function 'foo' must have an explicit @story annotation. Add a JSDoc or line comment with @story that points to the implementing story file (for example, docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md)");
43
43
  });
44
44
  });
@@ -36,7 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  /**
37
37
  * Tests for: docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
38
38
  * @story docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
39
+ * @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
39
40
  * @req REQ-PLUGIN-STRUCTURE - Validate plugin default export and configs in src/index.ts
41
+ * @req REQ-ERROR-SEVERITY - Validate error severity configuration in plugin configs
40
42
  */
41
43
  const index_1 = __importStar(require("../src/index"));
42
44
  describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", () => {
@@ -69,4 +71,18 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
69
71
  const strictRules = index_1.configs.strict[0].rules;
70
72
  expect(strictRules).toEqual(index_1.configs.recommended[0].rules);
71
73
  });
74
+ it("[REQ-ERROR-SEVERITY] configs.recommended maps valid-annotation-format to warn and others to error", () => {
75
+ const recommendedRules = index_1.configs.recommended[0].rules;
76
+ expect(recommendedRules).toHaveProperty("traceability/valid-annotation-format", "warn");
77
+ expect(recommendedRules).toHaveProperty("traceability/require-story-annotation", "error");
78
+ expect(recommendedRules).toHaveProperty("traceability/require-req-annotation", "error");
79
+ expect(recommendedRules).toHaveProperty("traceability/require-branch-annotation", "error");
80
+ expect(recommendedRules).toHaveProperty("traceability/valid-story-reference", "error");
81
+ expect(recommendedRules).toHaveProperty("traceability/valid-req-reference", "error");
82
+ });
83
+ it("[REQ-ERROR-SEVERITY] configs.strict uses same severity mapping as recommended", () => {
84
+ const strictRules = index_1.configs.strict[0].rules;
85
+ const recommendedRules = index_1.configs.recommended[0].rules;
86
+ expect(strictRules).toEqual(recommendedRules);
87
+ });
72
88
  });
@@ -28,6 +28,10 @@ describe("Auto-fix behavior (Story 008.0-DEV-AUTO-FIX)", () => {
28
28
  name: "[REQ-AUTOFIX-MISSING] already annotated function is unchanged",
29
29
  code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction alreadyAnnotated() {}`,
30
30
  },
31
+ {
32
+ name: "[REQ-AUTOFIX-MISSING] already annotated class method is unchanged",
33
+ code: `class A {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
34
+ },
31
35
  ],
32
36
  invalid: [
33
37
  {
@@ -46,6 +50,78 @@ describe("Auto-fix behavior (Story 008.0-DEV-AUTO-FIX)", () => {
46
50
  },
47
51
  ],
48
52
  },
53
+ {
54
+ name: "[REQ-AUTOFIX-MISSING] adds @story before function expression when missing",
55
+ code: `const fnExpr = function() {};`,
56
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nconst fnExpr = function() {};`,
57
+ errors: [
58
+ {
59
+ messageId: "missingStory",
60
+ suggestions: [
61
+ {
62
+ desc: "Add JSDoc @story annotation for function 'fnExpr', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
63
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nconst fnExpr = function() {};`,
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ name: "[REQ-AUTOFIX-MISSING] adds @story before class method when missing",
71
+ code: `class C {\n method() {}\n}`,
72
+ output: `class C {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
73
+ errors: [
74
+ {
75
+ messageId: "missingStory",
76
+ suggestions: [
77
+ {
78
+ desc: "Add JSDoc @story annotation for function 'method', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
79
+ output: `class C {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ },
85
+ {
86
+ name: "[REQ-AUTOFIX-MISSING] adds @story before TS declare function when missing",
87
+ code: `declare function tsDecl(): void;`,
88
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\ndeclare function tsDecl(): void;`,
89
+ languageOptions: {
90
+ parser: require("@typescript-eslint/parser"),
91
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
92
+ },
93
+ errors: [
94
+ {
95
+ messageId: "missingStory",
96
+ suggestions: [
97
+ {
98
+ desc: "Add JSDoc @story annotation for function 'tsDecl', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
99
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\ndeclare function tsDecl(): void;`,
100
+ },
101
+ ],
102
+ },
103
+ ],
104
+ },
105
+ {
106
+ name: "[REQ-AUTOFIX-MISSING] adds @story before TS method signature when missing",
107
+ code: `interface D {\n method(): void;\n}`,
108
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\ninterface D {\n method(): void;\n}`,
109
+ languageOptions: {
110
+ parser: require("@typescript-eslint/parser"),
111
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
112
+ },
113
+ errors: [
114
+ {
115
+ messageId: "missingStory",
116
+ suggestions: [
117
+ {
118
+ desc: "Add JSDoc @story annotation for function 'method', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
119
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\ninterface D {\n method(): void;\n}`,
120
+ },
121
+ ],
122
+ },
123
+ ],
124
+ },
49
125
  ],
50
126
  });
51
127
  });
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  * @req REQ-ERROR-SPECIFIC - Specific details about what annotation is missing or invalid
10
10
  * @req REQ-ERROR-SUGGESTION - Suggest concrete steps to fix the issue
11
11
  * @req REQ-ERROR-CONTEXT - Include relevant context in error messages
12
+ * @req REQ-ERROR-LOCATION - Include precise location information in error messages
12
13
  */
13
14
  const eslint_1 = require("eslint");
14
15
  const require_story_annotation_1 = __importDefault(require("../../src/rules/require-story-annotation"));
@@ -18,31 +19,79 @@ const ruleTester = new eslint_1.RuleTester({
18
19
  },
19
20
  });
20
21
  describe("Error Reporting Enhancements for require-story-annotation (Story 007.0-DEV-ERROR-REPORTING)", () => {
21
- ruleTester.run("require-story-annotation", require_story_annotation_1.default, {
22
- valid: [
23
- {
24
- name: "[007.0-DEV-ERROR-REPORTING] valid with existing annotation",
25
- code: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */ function foo() {}`,
26
- },
27
- ],
28
- invalid: [
29
- {
30
- name: "[REQ-ERROR-SPECIFIC] missing @story annotation should report specific details and suggestion",
31
- code: `function bar() {}`,
32
- output: "/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction bar() {}",
33
- errors: [
22
+ describe("valid cases", () => {
23
+ ruleTester.run("require-story-annotation", require_story_annotation_1.default, {
24
+ valid: [
25
+ {
26
+ name: "[007.0-DEV-ERROR-REPORTING] valid with existing annotation",
27
+ code: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */ function foo() {}`,
28
+ },
29
+ ],
30
+ invalid: [],
31
+ });
32
+ });
33
+ describe("REQ-ERROR-SPECIFIC - missing @story annotation should report specific details and suggestion", () => {
34
+ it("reports specific message, data, and suggestions for function 'bar'", () => {
35
+ const code = "function bar() {}";
36
+ const reported = [];
37
+ const context = {
38
+ id: "require-story-annotation",
39
+ options: [],
40
+ report: (descriptor) => {
41
+ reported.push(descriptor);
42
+ },
43
+ getFilename: () => "test.js",
44
+ getSourceCode: () => ({
45
+ text: code,
46
+ getText: () => code,
47
+ ast: {
48
+ type: "Program",
49
+ body: [],
50
+ sourceType: "module",
51
+ },
52
+ }),
53
+ };
54
+ const listeners = require_story_annotation_1.default.create(context);
55
+ // Minimal synthetic AST nodes for the visitors
56
+ const programNode = {
57
+ type: "Program",
58
+ body: [
34
59
  {
35
- messageId: "missingStory",
36
- data: { name: "bar" },
37
- suggestions: [
38
- {
39
- desc: "Add JSDoc @story annotation for function 'bar', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
40
- output: "/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction bar() {}",
41
- },
42
- ],
60
+ type: "FunctionDeclaration",
61
+ id: { type: "Identifier", name: "bar" },
62
+ params: [],
63
+ body: {
64
+ type: "BlockStatement",
65
+ body: [],
66
+ },
43
67
  },
44
68
  ],
45
- },
46
- ],
69
+ sourceType: "module",
70
+ };
71
+ const functionNode = programNode.body[0];
72
+ // Invoke visitors if they exist
73
+ if (typeof listeners.Program === "function") {
74
+ listeners.Program(programNode);
75
+ }
76
+ if (typeof listeners.FunctionDeclaration === "function") {
77
+ listeners.FunctionDeclaration(functionNode);
78
+ }
79
+ expect(reported.length).toBe(1);
80
+ const error = reported[0];
81
+ // Message template should be defined and contain the {{name}} placeholder
82
+ const template = require_story_annotation_1.default.meta?.messages?.missingStory;
83
+ expect(typeof template).toBe("string");
84
+ expect(template.length).toBeGreaterThan(0);
85
+ expect(template.includes("{{name}}")).toBe(true);
86
+ // Ensure messageId and data wiring is correct
87
+ expect(error.messageId).toBe("missingStory");
88
+ expect(error.data).toEqual({ name: "bar", functionName: "bar" });
89
+ // Suggestions
90
+ expect(Array.isArray(error.suggest)).toBe(true);
91
+ expect(error.suggest.length).toBeGreaterThanOrEqual(1);
92
+ const suggestion = error.suggest[0];
93
+ expect(suggestion.desc).toBe("Add JSDoc @story annotation for function 'bar', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */");
94
+ expect(suggestion.fix).toBeDefined();
95
+ });
47
96
  });
48
97
  });
@@ -4,9 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  /**
7
- * Tests for: docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
7
+ * Tests for: docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md, docs/stories/007.0-DEV-ERROR-REPORTING.story.md
8
8
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
9
+ * @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
9
10
  * @req REQ-BRANCH-DETECTION - Verify require-branch-annotation rule enforces branch annotations
11
+ * @req REQ-ERROR-SPECIFIC - Branch-level missing-annotation error messages are specific and informative
12
+ * @req REQ-ERROR-CONSISTENCY - Branch-level missing-annotation error messages follow shared conventions
13
+ * @req REQ-ERROR-SUGGESTION - Branch-level missing-annotation errors include suggestions when applicable
10
14
  */
11
15
  const eslint_1 = require("eslint");
12
16
  const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));