eslint-plugin-traceability 1.1.8 → 1.1.9

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.
@@ -1,4 +1,4 @@
1
- /**
1
+ /****
2
2
  * Rule to enforce @story and @req annotations on significant code branches
3
3
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
4
4
  * @req REQ-BRANCH-DETECTION - Detect significant code branches for traceability annotations
@@ -1,23 +1,17 @@
1
1
  "use strict";
2
- /**
2
+ /****
3
3
  * Rule to enforce @story and @req annotations on significant code branches
4
4
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
5
5
  * @req REQ-BRANCH-DETECTION - Detect significant code branches for traceability annotations
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  /**
9
- * Helper to check a branch AST node for traceability annotations.
9
+ * Gather leading comments for a node, with fallback for SwitchCase.
10
10
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
11
- * @req REQ-BRANCH-DETECTION - Helper for branch annotation detection
11
+ * @req REQ-BRANCH-DETECTION - Gather comments including fallback scanning
12
12
  */
13
- function checkBranchNode(sourceCode, context, node) {
14
- // Skip default switch cases during annotation checks
15
- if (node.type === "SwitchCase" && node.test == null) {
16
- return;
17
- }
18
- let comments = sourceCode.getCommentsBefore(node) || [];
19
- // Fallback scanning for SwitchCase when no leading comment nodes
20
- if (node.type === "SwitchCase" && comments.length === 0) {
13
+ function gatherCommentText(sourceCode, node) {
14
+ if (node.type === "SwitchCase") {
21
15
  const lines = sourceCode.lines;
22
16
  const startLine = node.loc.start.line;
23
17
  let i = startLine - 1;
@@ -35,9 +29,18 @@ function checkBranchNode(sourceCode, context, node) {
35
29
  break;
36
30
  }
37
31
  }
38
- comments = fallbackComments.map((text) => ({ value: text }));
32
+ return fallbackComments.join(" ");
39
33
  }
40
- const text = comments.map((c) => c.value).join(" ");
34
+ const comments = sourceCode.getCommentsBefore(node) || [];
35
+ return comments.map((c) => c.value).join(" ");
36
+ }
37
+ /**
38
+ * Helper to check a branch AST node for traceability annotations.
39
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
40
+ * @req REQ-BRANCH-DETECTION - Helper for branch annotation detection
41
+ */
42
+ function checkBranchNode(sourceCode, context, node) {
43
+ const text = gatherCommentText(sourceCode, node);
41
44
  const missingStory = !/@story\b/.test(text);
42
45
  const missingReq = !/@req\b/.test(text);
43
46
  if (missingStory) {
@@ -92,7 +95,12 @@ exports.default = {
92
95
  const sourceCode = context.getSourceCode();
93
96
  return {
94
97
  IfStatement: (node) => checkBranchNode(sourceCode, context, node),
95
- SwitchCase: (node) => checkBranchNode(sourceCode, context, node),
98
+ SwitchCase: (node) => {
99
+ if (node.test === null) {
100
+ return;
101
+ }
102
+ return checkBranchNode(sourceCode, context, node);
103
+ },
96
104
  TryStatement: (node) => checkBranchNode(sourceCode, context, node),
97
105
  CatchClause: (node) => checkBranchNode(sourceCode, context, node),
98
106
  ForStatement: (node) => checkBranchNode(sourceCode, context, node),
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Rule to enforce @story annotation on functions
2
+ * Rule to enforce @story annotation on functions, function expressions, arrow functions, and methods
3
3
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
4
4
  * @req REQ-ANNOTATION-REQUIRED - Require @story annotation on functions
5
+ * @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
6
+ * @req REQ-EXPORT-PRIORITY - Add exportPriority option to target exported or non-exported
7
+ * @req REQ-UNIFIED-CHECK - Implement unified checkNode for all supported node types
5
8
  */
6
9
  declare const _default: any;
7
10
  export default _default;
@@ -1,49 +1,201 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
2
  /**
4
- * Rule to enforce @story annotation on functions
3
+ * Rule to enforce @story annotation on functions, function expressions, arrow functions, and methods
5
4
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
6
5
  * @req REQ-ANNOTATION-REQUIRED - Require @story annotation on functions
6
+ * @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
7
+ * @req REQ-EXPORT-PRIORITY - Add exportPriority option to target exported or non-exported
8
+ * @req REQ-UNIFIED-CHECK - Implement unified checkNode for all supported node types
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ /**
12
+ * Determine if a node is exported via export declaration.
13
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
14
+ * @req REQ-EXPORT-PRIORITY - Determine if function node has export declaration ancestor
15
+ */
16
+ function isExportedNode(node) {
17
+ let current = node;
18
+ while (current) {
19
+ if (current.type === "ExportNamedDeclaration" ||
20
+ current.type === "ExportDefaultDeclaration") {
21
+ return true;
22
+ }
23
+ current = current.parent;
24
+ }
25
+ return false;
26
+ }
27
+ /**
28
+ * Find nearest ancestor node of specified types.
29
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
30
+ * @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
31
+ */
32
+ function findAncestorNode(node, types) {
33
+ let current = node.parent;
34
+ while (current) {
35
+ if (types.includes(current.type)) {
36
+ return current;
37
+ }
38
+ current = current.parent;
39
+ }
40
+ return null;
41
+ }
42
+ /**
43
+ * Determine if node should be checked based on scope and exportPriority.
44
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
45
+ * @req REQ-OPTIONS-SCOPE
46
+ * @req REQ-EXPORT-PRIORITY
47
+ * @req REQ-UNIFIED-CHECK
48
+ */
49
+ function shouldCheckNode(node, scope, exportPriority) {
50
+ if (node.type === "FunctionExpression" &&
51
+ node.parent?.type === "MethodDefinition") {
52
+ return false;
53
+ }
54
+ if (!scope.includes(node.type)) {
55
+ return false;
56
+ }
57
+ const exported = isExportedNode(node);
58
+ if ((exportPriority === "exported" && !exported) ||
59
+ (exportPriority === "non-exported" && exported)) {
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ /**
65
+ * Resolve the AST node to annotate or check.
66
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
67
+ * @req REQ-UNIFIED-CHECK
7
68
  */
69
+ function resolveTargetNode(sourceCode, node) {
70
+ let target = node;
71
+ if (node.type === "FunctionDeclaration") {
72
+ const exp = findAncestorNode(node, [
73
+ "ExportNamedDeclaration",
74
+ "ExportDefaultDeclaration",
75
+ ]);
76
+ if (exp) {
77
+ target = exp;
78
+ }
79
+ }
80
+ else if (node.type === "FunctionExpression" ||
81
+ node.type === "ArrowFunctionExpression") {
82
+ const exp = findAncestorNode(node, [
83
+ "ExportNamedDeclaration",
84
+ "ExportDefaultDeclaration",
85
+ ]);
86
+ if (exp) {
87
+ target = exp;
88
+ }
89
+ else {
90
+ const anc = findAncestorNode(node, [
91
+ "VariableDeclaration",
92
+ "ExpressionStatement",
93
+ ]);
94
+ if (anc) {
95
+ target = anc;
96
+ }
97
+ }
98
+ }
99
+ else if (node.type === "MethodDefinition") {
100
+ target = node;
101
+ }
102
+ return target;
103
+ }
104
+ /**
105
+ * Check if the target node has @story annotation.
106
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
107
+ * @req REQ-ANNOTATION-REQUIRED
108
+ */
109
+ function hasStoryAnnotation(sourceCode, target) {
110
+ const jsdoc = sourceCode.getJSDocComment(target);
111
+ if (jsdoc?.value.includes("@story")) {
112
+ return true;
113
+ }
114
+ const comments = sourceCode.getCommentsBefore(target) || [];
115
+ return comments.some((c) => c.value.includes("@story"));
116
+ }
117
+ /**
118
+ * Check for @story annotation on function-like nodes.
119
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
120
+ * @req REQ-UNIFIED-CHECK
121
+ * @req REQ-ANNOTATION-REQUIRED
122
+ */
123
+ function checkStoryAnnotation(sourceCode, context, node, scope, exportPriority) {
124
+ if (!shouldCheckNode(node, scope, exportPriority)) {
125
+ return;
126
+ }
127
+ const target = resolveTargetNode(sourceCode, node);
128
+ if (hasStoryAnnotation(sourceCode, target)) {
129
+ return;
130
+ }
131
+ context.report({
132
+ node,
133
+ messageId: "missingStory",
134
+ fix(fixer) {
135
+ const indentLevel = target.loc.start.column;
136
+ const indent = " ".repeat(indentLevel);
137
+ const insertPos = target.range[0] - indentLevel;
138
+ return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}/** @story <story-file>.story.md */\n`);
139
+ },
140
+ });
141
+ }
8
142
  exports.default = {
9
143
  meta: {
10
144
  type: "problem",
11
145
  docs: {
12
- description: "Require @story annotations on functions",
146
+ description: "Require @story annotations on selected functions",
13
147
  recommended: "error",
14
148
  },
15
149
  fixable: "code",
16
150
  messages: {
17
- missingStory: "Missing @story annotation",
151
+ missingStory: "Missing @story annotation (REQ-ANNOTATION-REQUIRED)",
18
152
  },
19
- schema: [],
153
+ schema: [
154
+ {
155
+ type: "object",
156
+ properties: {
157
+ scope: {
158
+ type: "array",
159
+ items: {
160
+ enum: [
161
+ "FunctionDeclaration",
162
+ "FunctionExpression",
163
+ "ArrowFunctionExpression",
164
+ "MethodDefinition",
165
+ ],
166
+ },
167
+ uniqueItems: true,
168
+ },
169
+ exportPriority: {
170
+ enum: ["all", "exported", "non-exported"],
171
+ },
172
+ },
173
+ additionalProperties: false,
174
+ },
175
+ ],
20
176
  },
21
177
  create(context) {
22
178
  const sourceCode = context.getSourceCode();
179
+ const options = context.options[0] || {};
180
+ const scope = options.scope || [
181
+ "FunctionDeclaration",
182
+ "FunctionExpression",
183
+ "ArrowFunctionExpression",
184
+ "MethodDefinition",
185
+ ];
186
+ const exportPriority = options.exportPriority || "all";
23
187
  return {
24
188
  FunctionDeclaration(node) {
25
- const jsdoc = sourceCode.getJSDocComment(node);
26
- let hasStory = false;
27
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
28
- // @req REQ-JSDOC-PARSING - Detect JSDoc @story annotation presence
29
- if (jsdoc && jsdoc.value.includes("@story")) {
30
- hasStory = true;
31
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
32
- // @req REQ-JSDOC-PARSING - Fallback to loading comments before node for @story annotation detection
33
- }
34
- else {
35
- const commentsBefore = sourceCode.getCommentsBefore(node) || [];
36
- hasStory = commentsBefore.some((comment) => comment.value.includes("@story"));
37
- }
38
- if (!hasStory) {
39
- context.report({
40
- node,
41
- messageId: "missingStory",
42
- fix(fixer) {
43
- return fixer.insertTextBefore(node, "/** @story <story-file>.story.md */\n");
44
- },
45
- });
46
- }
189
+ checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
190
+ },
191
+ FunctionExpression(node) {
192
+ checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
193
+ },
194
+ ArrowFunctionExpression(node) {
195
+ checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
196
+ },
197
+ MethodDefinition(node) {
198
+ checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
47
199
  },
48
200
  };
49
201
  },
@@ -15,14 +15,122 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const fs_1 = __importDefault(require("fs"));
16
16
  const path_1 = __importDefault(require("path"));
17
17
  /**
18
- * Create the Program listener for validating @req annotations.
19
- * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
20
- * @req REQ-DEEP-PARSE - Parse story files to extract requirement identifiers
21
- * @req REQ-DEEP-MATCH - Validate @req references against story file content
22
- * @req REQ-DEEP-CACHE - Cache parsed story content for performance
23
- * @req REQ-DEEP-PATH - Protect against path traversal in story paths
18
+ * Extract the story path from a JSDoc comment.
19
+ * Parses comment.value lines for @story annotation.
20
+ * @param comment any JSDoc comment node
21
+ * @returns story path or null if not found
22
+ */
23
+ function extractStoryPath(comment) {
24
+ const rawLines = comment.value.split(/\r?\n/);
25
+ for (const rawLine of rawLines) {
26
+ const line = rawLine.trim().replace(/^\*+\s*/, "");
27
+ if (line.startsWith("@story")) {
28
+ const parts = line.split(/\s+/);
29
+ return parts[1] || null;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * Validate a @req annotation line against the extracted story content.
36
+ * Performs path validation, file reading, caching, and requirement existence checks.
37
+ * @param comment any JSDoc comment node
38
+ * @param context ESLint rule context
39
+ * @param line the @req annotation line
40
+ * @param storyPath current story path
41
+ * @param cwd current working directory
42
+ * @param reqCache cache mapping story paths to sets of requirement IDs
43
+ */
44
+ function validateReqLine(comment, context, line, storyPath, cwd, reqCache) {
45
+ const parts = line.split(/\s+/);
46
+ const reqId = parts[1];
47
+ if (!reqId || !storyPath) {
48
+ return;
49
+ }
50
+ if (storyPath.includes("..") || path_1.default.isAbsolute(storyPath)) {
51
+ context.report({
52
+ node: comment,
53
+ messageId: "invalidPath",
54
+ data: { storyPath },
55
+ });
56
+ return;
57
+ }
58
+ const resolvedStoryPath = path_1.default.resolve(cwd, storyPath);
59
+ if (!resolvedStoryPath.startsWith(cwd + path_1.default.sep) &&
60
+ resolvedStoryPath !== cwd) {
61
+ context.report({
62
+ node: comment,
63
+ messageId: "invalidPath",
64
+ data: { storyPath },
65
+ });
66
+ return;
67
+ }
68
+ if (!reqCache.has(resolvedStoryPath)) {
69
+ try {
70
+ const content = fs_1.default.readFileSync(resolvedStoryPath, "utf8");
71
+ const found = new Set();
72
+ const regex = /REQ-[A-Z0-9-]+/g;
73
+ let match;
74
+ while ((match = regex.exec(content)) !== null) {
75
+ found.add(match[0]);
76
+ }
77
+ reqCache.set(resolvedStoryPath, found);
78
+ }
79
+ catch {
80
+ reqCache.set(resolvedStoryPath, new Set());
81
+ }
82
+ }
83
+ const reqSet = reqCache.get(resolvedStoryPath);
84
+ if (!reqSet.has(reqId)) {
85
+ context.report({
86
+ node: comment,
87
+ messageId: "reqMissing",
88
+ data: { reqId, storyPath },
89
+ });
90
+ }
91
+ }
92
+ /**
93
+ * Handle a single annotation line.
94
+ * @story Updates the current story path when encountering an @story annotation
95
+ * @req Validates the requirement reference against the current story content
96
+ * @param line the trimmed annotation line
97
+ * @param comment JSDoc comment node
98
+ * @param context ESLint rule context
99
+ * @param cwd current working directory
100
+ * @param reqCache cache mapping story paths to sets of requirement IDs
101
+ * @param storyPath current story path or null
102
+ * @returns updated story path or null
24
103
  */
25
- function createProgramListener(context) {
104
+ function handleAnnotationLine(line, comment, context, cwd, reqCache, storyPath) {
105
+ if (line.startsWith("@story")) {
106
+ const newPath = extractStoryPath(comment);
107
+ return newPath || storyPath;
108
+ }
109
+ else if (line.startsWith("@req")) {
110
+ validateReqLine(comment, context, line, storyPath, cwd, reqCache);
111
+ return storyPath;
112
+ }
113
+ return storyPath;
114
+ }
115
+ /**
116
+ * Handle JSDoc story and req annotations.
117
+ * @param comment any JSDoc comment node
118
+ * @param context ESLint rule context
119
+ * @param cwd current working directory
120
+ * @param reqCache cache mapping story paths to sets of requirement IDs
121
+ * @param rawStoryPath the last extracted story path or null
122
+ * @returns updated story path or null
123
+ */
124
+ function handleComment(comment, context, cwd, reqCache, rawStoryPath) {
125
+ let storyPath = rawStoryPath;
126
+ const rawLines = comment.value.split(/\r?\n/);
127
+ for (const rawLine of rawLines) {
128
+ const line = rawLine.trim().replace(/^\*+\s*/, "");
129
+ storyPath = handleAnnotationLine(line, comment, context, cwd, reqCache, storyPath);
130
+ }
131
+ return storyPath;
132
+ }
133
+ function programListener(context) {
26
134
  const sourceCode = context.getSourceCode();
27
135
  const cwd = process.cwd();
28
136
  const reqCache = new Map();
@@ -30,62 +138,7 @@ function createProgramListener(context) {
30
138
  return function Program() {
31
139
  const comments = sourceCode.getAllComments() || [];
32
140
  comments.forEach((comment) => {
33
- const rawLines = comment.value.split(/\r?\n/);
34
- const lines = rawLines.map((rawLine) => rawLine.trim().replace(/^\*+\s*/, ""));
35
- lines.forEach((line) => {
36
- if (line.startsWith("@story")) {
37
- const parts = line.split(/\s+/);
38
- rawStoryPath = parts[1] || null;
39
- }
40
- if (line.startsWith("@req")) {
41
- const parts = line.split(/\s+/);
42
- const reqId = parts[1];
43
- if (!reqId || !rawStoryPath) {
44
- return;
45
- }
46
- if (rawStoryPath.includes("..") || path_1.default.isAbsolute(rawStoryPath)) {
47
- context.report({
48
- node: comment,
49
- messageId: "invalidPath",
50
- data: { storyPath: rawStoryPath },
51
- });
52
- return;
53
- }
54
- const resolvedStoryPath = path_1.default.resolve(cwd, rawStoryPath);
55
- if (!resolvedStoryPath.startsWith(cwd + path_1.default.sep) &&
56
- resolvedStoryPath !== cwd) {
57
- context.report({
58
- node: comment,
59
- messageId: "invalidPath",
60
- data: { storyPath: rawStoryPath },
61
- });
62
- return;
63
- }
64
- if (!reqCache.has(resolvedStoryPath)) {
65
- try {
66
- const content = fs_1.default.readFileSync(resolvedStoryPath, "utf8");
67
- const found = new Set();
68
- const regex = /REQ-[A-Z0-9-]+/g;
69
- let match;
70
- while ((match = regex.exec(content)) !== null) {
71
- found.add(match[0]);
72
- }
73
- reqCache.set(resolvedStoryPath, found);
74
- }
75
- catch {
76
- reqCache.set(resolvedStoryPath, new Set());
77
- }
78
- }
79
- const reqSet = reqCache.get(resolvedStoryPath);
80
- if (!reqSet.has(reqId)) {
81
- context.report({
82
- node: comment,
83
- messageId: "reqMissing",
84
- data: { reqId, storyPath: rawStoryPath },
85
- });
86
- }
87
- }
88
- });
141
+ rawStoryPath = handleComment(comment, context, cwd, reqCache, rawStoryPath);
89
142
  });
90
143
  };
91
144
  }
@@ -103,7 +156,6 @@ exports.default = {
103
156
  schema: [],
104
157
  },
105
158
  create(context) {
106
- const program = createProgramListener(context);
107
- return { Program: program };
159
+ return { Program: programListener(context) };
108
160
  },
109
161
  };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const valid_story_reference_1 = __importDefault(require("../../src/rules/valid-story-reference"));
7
+ /** @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md */
8
+ describe("ESLint Configuration Setup (Story 002.0-DEV-ESLINT-CONFIG)", () => {
9
+ it("[REQ-RULE-OPTIONS] rule meta.schema defines expected properties", () => {
10
+ const schema = valid_story_reference_1.default.meta.schema[0];
11
+ expect(schema.properties).toHaveProperty("storyDirectories");
12
+ expect(schema.properties).toHaveProperty("allowAbsolutePaths");
13
+ expect(schema.properties).toHaveProperty("requireStoryExtension");
14
+ });
15
+ it("[REQ-CONFIG-VALIDATION] schema disallows unknown options", () => {
16
+ const schema = valid_story_reference_1.default.meta.schema[0];
17
+ expect(schema.additionalProperties).toBe(false);
18
+ });
19
+ });
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const require_story_annotation_1 = __importDefault(require("../../src/rules/require-story-annotation"));
7
+ /** @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md */
8
+ describe("ESLint Configuration Rule Options (Story 002.0-DEV-ESLINT-CONFIG)", () => {
9
+ it("[REQ-RULE-OPTIONS] require-story-annotation schema defines expected properties", () => {
10
+ const schema = require_story_annotation_1.default.meta.schema[0];
11
+ expect(schema.properties).toHaveProperty("scope");
12
+ expect(schema.properties).toHaveProperty("exportPriority");
13
+ expect(schema.additionalProperties).toBe(false);
14
+ });
15
+ });
@@ -10,7 +10,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  */
11
11
  const eslint_1 = require("eslint");
12
12
  const require_story_annotation_1 = __importDefault(require("../../src/rules/require-story-annotation"));
13
- const ruleTester = new eslint_1.RuleTester();
13
+ const ruleTester = new eslint_1.RuleTester({
14
+ languageOptions: {
15
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
16
+ },
17
+ });
14
18
  describe("Require Story Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
15
19
  ruleTester.run("require-story-annotation", require_story_annotation_1.default, {
16
20
  valid: [
@@ -23,6 +27,19 @@ describe("Require Story Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)",
23
27
  code: `// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
24
28
  function foo() {}`,
25
29
  },
30
+ {
31
+ name: "[REQ-ANNOTATION-REQUIRED] valid on function expression with annotation",
32
+ code: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nconst fnExpr = function() {};`,
33
+ },
34
+ {
35
+ name: "[REQ-ANNOTATION-REQUIRED] valid on arrow function with annotation",
36
+ code: `// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
37
+ const arrowFn = () => {};`,
38
+ },
39
+ {
40
+ name: "[REQ-ANNOTATION-REQUIRED] valid on class method with annotation",
41
+ code: `class A {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
42
+ },
26
43
  ],
27
44
  invalid: [
28
45
  {
@@ -31,6 +48,72 @@ function foo() {}`,
31
48
  output: `/** @story <story-file>.story.md */\nfunction bar() {}`,
32
49
  errors: [{ messageId: "missingStory" }],
33
50
  },
51
+ {
52
+ name: "[REQ-ANNOTATION-REQUIRED] missing @story on function expression",
53
+ code: `const fnExpr = function() {};`,
54
+ output: `/** @story <story-file>.story.md */\nconst fnExpr = function() {};`,
55
+ errors: [{ messageId: "missingStory" }],
56
+ },
57
+ {
58
+ name: "[REQ-ANNOTATION-REQUIRED] missing @story on arrow function",
59
+ code: `const arrowFn = () => {};`,
60
+ output: `/** @story <story-file>.story.md */\nconst arrowFn = () => {};`,
61
+ errors: [{ messageId: "missingStory" }],
62
+ },
63
+ {
64
+ name: "[REQ-ANNOTATION-REQUIRED] missing @story on class method",
65
+ code: `class C {\n method() {}\n}`,
66
+ output: `class C {\n /** @story <story-file>.story.md */\n method() {}\n}`,
67
+ errors: [{ messageId: "missingStory" }],
68
+ },
69
+ ],
70
+ });
71
+ ruleTester.run("require-story-annotation with exportPriority option", require_story_annotation_1.default, {
72
+ valid: [
73
+ {
74
+ name: "[exportPriority] unexported function without @story should be valid",
75
+ code: `function local() {}`,
76
+ options: [{ exportPriority: "exported" }],
77
+ },
78
+ {
79
+ name: "[exportPriority] exported with annotation",
80
+ code: `// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\nexport function exportedAnnotated() {}`,
81
+ options: [{ exportPriority: "exported" }],
82
+ },
83
+ ],
84
+ invalid: [
85
+ {
86
+ name: "[exportPriority] exported function missing @story annotation",
87
+ code: `export function exportedMissing() {}`,
88
+ output: `/** @story <story-file>.story.md */\nexport function exportedMissing() {}`,
89
+ options: [{ exportPriority: "exported" }],
90
+ errors: [{ messageId: "missingStory" }],
91
+ },
92
+ {
93
+ name: "[exportPriority] exported arrow function missing @story annotation",
94
+ code: `export const arrowExported = () => {};`,
95
+ output: `/** @story <story-file>.story.md */\nexport const arrowExported = () => {};`,
96
+ options: [{ exportPriority: "exported" }],
97
+ errors: [{ messageId: "missingStory" }],
98
+ },
99
+ ],
100
+ });
101
+ ruleTester.run("require-story-annotation with scope option", require_story_annotation_1.default, {
102
+ valid: [
103
+ {
104
+ name: "[scope] arrow function ignored when scope is FunctionDeclaration",
105
+ code: `const arrow = () => {};`,
106
+ options: [{ scope: ["FunctionDeclaration"] }],
107
+ },
108
+ ],
109
+ invalid: [
110
+ {
111
+ name: "[scope] function declaration missing annotation when scope is FunctionDeclaration",
112
+ code: `function onlyDecl() {}`,
113
+ options: [{ scope: ["FunctionDeclaration"] }],
114
+ output: `/** @story <story-file>.story.md */\nfunction onlyDecl() {}`,
115
+ errors: [{ messageId: "missingStory" }],
116
+ },
34
117
  ],
35
118
  });
36
119
  });
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- /**
6
+ /****
7
7
  * Tests for: docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
8
8
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
9
9
  * @req REQ-DEEP-PARSE - Verify valid-req-reference rule enforces existing requirement content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
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",
@@ -17,10 +17,11 @@
17
17
  "build": "tsc -p tsconfig.json",
18
18
  "type-check": "tsc --noEmit -p tsconfig.json",
19
19
  "lint": "eslint \"src/**/*.{js,ts}\" \"tests/**/*.{js,ts}\" --max-warnings=0",
20
- "test": "jest --ci --bail --coverage",
20
+ "test": "jest --ci --bail",
21
21
  "format": "prettier --write .",
22
22
  "format:check": "prettier --check .",
23
23
  "duplication": "jscpd src tests --reporters console --threshold 3",
24
+ "audit:dev-high": "node scripts/generate-dev-deps-audit.js",
24
25
  "smoke-test": "./scripts/smoke-test.sh",
25
26
  "prepare": "husky install"
26
27
  },
@@ -40,7 +41,7 @@
40
41
  },
41
42
  "keywords": [],
42
43
  "author": "",
43
- "license": "ISC",
44
+ "license": "MIT",
44
45
  "bugs": {
45
46
  "url": "https://github.com/voder-ai/eslint-plugin-traceability/issues"
46
47
  },
@@ -49,7 +50,8 @@
49
50
  "@eslint/js": "^9.39.1",
50
51
  "@semantic-release/changelog": "^6.0.3",
51
52
  "@semantic-release/git": "^10.0.1",
52
- "@semantic-release/github": "^12.0.2",
53
+ "@semantic-release/github": "^10.3.5",
54
+ "@semantic-release/npm": "^10.0.6",
53
55
  "@types/eslint": "^9.6.1",
54
56
  "@types/jest": "^30.0.0",
55
57
  "@types/node": "^24.10.1",
@@ -58,11 +60,11 @@
58
60
  "actionlint": "^2.0.6",
59
61
  "eslint": "^9.39.1",
60
62
  "husky": "^9.1.7",
61
- "jest": "^30.2.0",
63
+ "jest": "^29.7.0",
62
64
  "jscpd": "^4.0.5",
63
65
  "lint-staged": "^16.2.6",
64
66
  "prettier": "^3.6.2",
65
- "semantic-release": "^25.0.2",
67
+ "semantic-release": "^21.1.2",
66
68
  "ts-jest": "^29.4.5",
67
69
  "typescript": "^5.9.3"
68
70
  },
@@ -73,7 +75,6 @@
73
75
  "node": ">=14"
74
76
  },
75
77
  "overrides": {
76
- "js-yaml": ">=4.1.1",
77
- "tar": ">=6.1.11"
78
+ "glob": ">=11.0.4"
78
79
  }
79
80
  }