eslint-plugin-traceability 1.1.7 → 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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Prerequisites: Node.js v12+ and ESLint v9+.
15
15
  2. Using Yarn
16
16
  yarn add --dev eslint-plugin-traceability
17
17
 
18
- For detailed setup with ESLint v9, see docs/eslint-9-setup-guide.md.
18
+ For detailed setup with ESLint v9, see user-docs/eslint-9-setup-guide.md.
19
19
 
20
20
  ## Usage
21
21
 
@@ -145,7 +145,7 @@ The CLI integration tests are also executed automatically in CI under the `integ
145
145
 
146
146
  ## Documentation Links
147
147
 
148
- - ESLint v9 Setup Guide: docs/eslint-9-setup-guide.md
148
+ - ESLint v9 Setup Guide: user-docs/eslint-9-setup-guide.md
149
149
  - Plugin Development Guide: docs/eslint-plugin-development-guide.md
150
150
  - API Reference: user-docs/api-reference.md
151
151
  - Examples: user-docs/examples.md
@@ -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,10 +1,83 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- /**
2
+ /****
4
3
  * Rule to enforce @story and @req annotations on significant code branches
5
4
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
6
5
  * @req REQ-BRANCH-DETECTION - Detect significant code branches for traceability annotations
7
6
  */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ /**
9
+ * Gather leading comments for a node, with fallback for SwitchCase.
10
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
11
+ * @req REQ-BRANCH-DETECTION - Gather comments including fallback scanning
12
+ */
13
+ function gatherCommentText(sourceCode, node) {
14
+ if (node.type === "SwitchCase") {
15
+ const lines = sourceCode.lines;
16
+ const startLine = node.loc.start.line;
17
+ let i = startLine - 1;
18
+ const fallbackComments = [];
19
+ while (i > 0) {
20
+ const lineText = lines[i - 1];
21
+ if (/^\s*(\/\/|\/\*)/.test(lineText)) {
22
+ fallbackComments.unshift(lineText.trim());
23
+ i--;
24
+ }
25
+ else if (/^\s*$/.test(lineText)) {
26
+ break;
27
+ }
28
+ else {
29
+ break;
30
+ }
31
+ }
32
+ return fallbackComments.join(" ");
33
+ }
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);
44
+ const missingStory = !/@story\b/.test(text);
45
+ const missingReq = !/@req\b/.test(text);
46
+ if (missingStory) {
47
+ const reportObj = {
48
+ node,
49
+ messageId: "missingAnnotation",
50
+ data: { missing: "@story" },
51
+ };
52
+ if (node.type !== "CatchClause") {
53
+ if (node.type === "SwitchCase") {
54
+ const indent = " ".repeat(node.loc.start.column);
55
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @story <story-file>.story.md\n${indent}`);
56
+ }
57
+ else {
58
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @story <story-file>.story.md\n`);
59
+ }
60
+ }
61
+ context.report(reportObj);
62
+ }
63
+ if (missingReq) {
64
+ const reportObj = {
65
+ node,
66
+ messageId: "missingAnnotation",
67
+ data: { missing: "@req" },
68
+ };
69
+ if (!missingStory && node.type !== "CatchClause") {
70
+ if (node.type === "SwitchCase") {
71
+ const indent = " ".repeat(node.loc.start.column);
72
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @req <REQ-ID>\n${indent}`);
73
+ }
74
+ else {
75
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @req <REQ-ID>\n`);
76
+ }
77
+ }
78
+ context.report(reportObj);
79
+ }
80
+ }
8
81
  exports.default = {
9
82
  meta: {
10
83
  type: "problem",
@@ -20,92 +93,21 @@ exports.default = {
20
93
  },
21
94
  create(context) {
22
95
  const sourceCode = context.getSourceCode();
23
- /**
24
- * Helper to check a branch AST node for traceability annotations.
25
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
26
- * @req REQ-BRANCH-DETECTION - Detect significant code branches for traceability annotations
27
- */
28
- function checkBranch(node) {
29
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
30
- // @req REQ-BRANCH-DETECTION - Skip default switch cases during annotation checks
31
- // skip default cases in switch
32
- if (node.type === "SwitchCase" && node.test == null) {
33
- return;
34
- }
35
- // collect comments before node
36
- let comments = sourceCode.getCommentsBefore(node) || [];
37
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
38
- // @req REQ-BRANCH-DETECTION - Fallback scanning for SwitchCase when leading comments are absent
39
- // fallback scanning for SwitchCase if no leading comment nodes
40
- /* istanbul ignore if */
41
- if (node.type === "SwitchCase" && comments.length === 0) {
42
- const lines = sourceCode.lines;
43
- const startLine = node.loc.start.line;
44
- let i = startLine - 1;
45
- const fallbackComments = [];
46
- while (i > 0) {
47
- const lineText = lines[i - 1];
48
- if (/^\s*(\/\/|\/\*)/.test(lineText)) {
49
- fallbackComments.unshift(lineText.trim());
50
- i--;
51
- }
52
- else if (/^\s*$/.test(lineText)) {
53
- break;
54
- }
55
- else {
56
- break;
57
- }
58
- }
59
- comments = fallbackComments.map((text) => ({ value: text }));
60
- }
61
- const text = comments.map((c) => c.value).join(" ");
62
- const missingStory = !/@story\b/.test(text);
63
- const missingReq = !/@req\b/.test(text);
64
- if (missingStory) {
65
- const reportObj = {
66
- node,
67
- messageId: "missingAnnotation",
68
- data: { missing: "@story" },
69
- };
70
- if (node.type !== "CatchClause") {
71
- if (node.type === "SwitchCase") {
72
- const indent = " ".repeat(node.loc.start.column);
73
- reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @story <story-file>.story.md\n${indent}`);
74
- }
75
- else {
76
- reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @story <story-file>.story.md\n`);
77
- }
78
- }
79
- context.report(reportObj);
80
- }
81
- if (missingReq) {
82
- const reportObj = {
83
- node,
84
- messageId: "missingAnnotation",
85
- data: { missing: "@req" },
86
- };
87
- if (!missingStory && node.type !== "CatchClause") {
88
- if (node.type === "SwitchCase") {
89
- const indent = " ".repeat(node.loc.start.column);
90
- reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @req <REQ-ID>\n${indent}`);
91
- }
92
- else {
93
- reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @req <REQ-ID>\n`);
94
- }
95
- }
96
- context.report(reportObj);
97
- }
98
- }
99
96
  return {
100
- IfStatement: checkBranch,
101
- SwitchCase: checkBranch,
102
- TryStatement: checkBranch,
103
- CatchClause: checkBranch,
104
- ForStatement: checkBranch,
105
- ForOfStatement: checkBranch,
106
- ForInStatement: checkBranch,
107
- WhileStatement: checkBranch,
108
- DoWhileStatement: checkBranch,
97
+ IfStatement: (node) => checkBranchNode(sourceCode, context, node),
98
+ SwitchCase: (node) => {
99
+ if (node.test === null) {
100
+ return;
101
+ }
102
+ return checkBranchNode(sourceCode, context, node);
103
+ },
104
+ TryStatement: (node) => checkBranchNode(sourceCode, context, node),
105
+ CatchClause: (node) => checkBranchNode(sourceCode, context, node),
106
+ ForStatement: (node) => checkBranchNode(sourceCode, context, node),
107
+ ForOfStatement: (node) => checkBranchNode(sourceCode, context, node),
108
+ ForInStatement: (node) => checkBranchNode(sourceCode, context, node),
109
+ WhileStatement: (node) => checkBranchNode(sourceCode, context, node),
110
+ DoWhileStatement: (node) => checkBranchNode(sourceCode, context, node),
109
111
  };
110
112
  },
111
113
  };
@@ -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
  },
@@ -14,6 +14,134 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  */
15
15
  const fs_1 = __importDefault(require("fs"));
16
16
  const path_1 = __importDefault(require("path"));
17
+ /**
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
103
+ */
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) {
134
+ const sourceCode = context.getSourceCode();
135
+ const cwd = process.cwd();
136
+ const reqCache = new Map();
137
+ let rawStoryPath = null;
138
+ return function Program() {
139
+ const comments = sourceCode.getAllComments() || [];
140
+ comments.forEach((comment) => {
141
+ rawStoryPath = handleComment(comment, context, cwd, reqCache, rawStoryPath);
142
+ });
143
+ };
144
+ }
17
145
  exports.default = {
18
146
  meta: {
19
147
  type: "problem",
@@ -28,77 +156,6 @@ exports.default = {
28
156
  schema: [],
29
157
  },
30
158
  create(context) {
31
- const sourceCode = context.getSourceCode();
32
- const cwd = process.cwd();
33
- // Cache for resolved story file paths to parsed set of requirement IDs
34
- const reqCache = new Map();
35
- let rawStoryPath = null;
36
- return {
37
- Program() {
38
- const comments = sourceCode.getAllComments() || [];
39
- comments.forEach((comment) => {
40
- const rawLines = comment.value.split(/\r?\n/);
41
- const lines = rawLines.map((rawLine) => rawLine.trim().replace(/^\*+\s*/, ""));
42
- lines.forEach((line) => {
43
- if (line.startsWith("@story")) {
44
- const parts = line.split(/\s+/);
45
- rawStoryPath = parts[1] || null;
46
- }
47
- if (line.startsWith("@req")) {
48
- const parts = line.split(/\s+/);
49
- const reqId = parts[1];
50
- if (!reqId || !rawStoryPath) {
51
- return;
52
- }
53
- // Protect against path traversal and absolute paths
54
- if (rawStoryPath.includes("..") ||
55
- path_1.default.isAbsolute(rawStoryPath)) {
56
- context.report({
57
- node: comment,
58
- messageId: "invalidPath",
59
- data: { storyPath: rawStoryPath },
60
- });
61
- return;
62
- }
63
- const resolvedStoryPath = path_1.default.resolve(cwd, rawStoryPath);
64
- if (!resolvedStoryPath.startsWith(cwd + path_1.default.sep) &&
65
- resolvedStoryPath !== cwd) {
66
- context.report({
67
- node: comment,
68
- messageId: "invalidPath",
69
- data: { storyPath: rawStoryPath },
70
- });
71
- return;
72
- }
73
- // Load and parse story file if not cached
74
- if (!reqCache.has(resolvedStoryPath)) {
75
- try {
76
- const content = fs_1.default.readFileSync(resolvedStoryPath, "utf8");
77
- const found = new Set();
78
- const regex = /REQ-[A-Z0-9-]+/g;
79
- let match;
80
- while ((match = regex.exec(content)) !== null) {
81
- found.add(match[0]);
82
- }
83
- reqCache.set(resolvedStoryPath, found);
84
- }
85
- catch {
86
- // Unable to read file, treat as no requirements
87
- reqCache.set(resolvedStoryPath, new Set());
88
- }
89
- }
90
- const reqSet = reqCache.get(resolvedStoryPath);
91
- if (!reqSet.has(reqId)) {
92
- context.report({
93
- node: comment,
94
- messageId: "reqMissing",
95
- data: { reqId, storyPath: rawStoryPath },
96
- });
97
- }
98
- }
99
- });
100
- });
101
- },
102
- };
159
+ return { Program: programListener(context) };
103
160
  },
104
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
+ });
@@ -48,30 +48,32 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
48
48
  expect(result).toEqual([]);
49
49
  });
50
50
  it("[REQ-MAINT-DETECT] detects stale annotations in nested directories", () => {
51
- // Arrange
52
51
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-nested-"));
53
- const nestedDir = path.join(tmpDir, "nested");
54
- fs.mkdirSync(nestedDir);
55
- const filePath1 = path.join(tmpDir, "file1.ts");
56
- const filePath2 = path.join(nestedDir, "file2.ts");
57
- const content1 = `
52
+ try {
53
+ const nestedDir = path.join(tmpDir, "nested");
54
+ fs.mkdirSync(nestedDir);
55
+ const filePath1 = path.join(tmpDir, "file1.ts");
56
+ const filePath2 = path.join(nestedDir, "file2.ts");
57
+ const content1 = `
58
58
  /**
59
59
  * @story stale1.story.md
60
60
  */
61
61
  `;
62
- fs.writeFileSync(filePath1, content1, "utf8");
63
- const content2 = `
62
+ fs.writeFileSync(filePath1, content1, "utf8");
63
+ const content2 = `
64
64
  /**
65
65
  * @story stale2.story.md
66
66
  */
67
67
  `;
68
- fs.writeFileSync(filePath2, content2, "utf8");
69
- // Act
70
- const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
71
- // Assert
72
- expect(result).toHaveLength(2);
73
- expect(result).toContain("stale1.story.md");
74
- expect(result).toContain("stale2.story.md");
68
+ fs.writeFileSync(filePath2, content2, "utf8");
69
+ const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
70
+ expect(result).toHaveLength(2);
71
+ expect(result).toContain("stale1.story.md");
72
+ expect(result).toContain("stale2.story.md");
73
+ }
74
+ finally {
75
+ fs.rmSync(tmpDir, { recursive: true, force: true });
76
+ }
75
77
  });
76
78
  it("[REQ-MAINT-DETECT] throws error on permission denied", () => {
77
79
  const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
@@ -15,9 +15,13 @@ const detect_1 = require("../../src/maintenance/detect");
15
15
  describe("detectStaleAnnotations (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
16
16
  it("[REQ-MAINT-DETECT] should return empty array when no stale annotations", () => {
17
17
  const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "detect-test-"));
18
- // No annotation files are created in tmpDir to simulate no stale annotations
19
- const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
20
- expect(result).toEqual([]);
21
- fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
18
+ try {
19
+ // No annotation files are created in tmpDir to simulate no stale annotations
20
+ const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
21
+ expect(result).toEqual([]);
22
+ }
23
+ finally {
24
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
25
+ }
22
26
  });
23
27
  });
@@ -44,23 +44,23 @@ const os = __importStar(require("os"));
44
44
  const update_1 = require("../../src/maintenance/update");
45
45
  describe("updateAnnotationReferences isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
46
46
  it("[REQ-MAINT-UPDATE] updates @story annotations in files", () => {
47
- // Create a temporary directory for testing
48
47
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-"));
49
- const filePath = path.join(tmpDir, "file.ts");
50
- const originalContent = `
48
+ try {
49
+ const filePath = path.join(tmpDir, "file.ts");
50
+ const originalContent = `
51
51
  /**
52
52
  * @story old.path.md
53
53
  */
54
54
  function foo() {}
55
55
  `;
56
- fs.writeFileSync(filePath, originalContent, "utf8");
57
- // Run the function under test
58
- const count = (0, update_1.updateAnnotationReferences)(tmpDir, "old.path.md", "new.path.md");
59
- expect(count).toBe(1);
60
- // Verify the file content was updated
61
- const updatedContent = fs.readFileSync(filePath, "utf8");
62
- expect(updatedContent).toContain("@story new.path.md");
63
- // Cleanup temporary directory
64
- fs.rmSync(tmpDir, { recursive: true, force: true });
56
+ fs.writeFileSync(filePath, originalContent, "utf8");
57
+ const count = (0, update_1.updateAnnotationReferences)(tmpDir, "old.path.md", "new.path.md");
58
+ expect(count).toBe(1);
59
+ const updatedContent = fs.readFileSync(filePath, "utf8");
60
+ expect(updatedContent).toContain("@story new.path.md");
61
+ }
62
+ finally {
63
+ fs.rmSync(tmpDir, { recursive: true, force: true });
64
+ }
65
65
  });
66
66
  });
@@ -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.7",
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,6 +75,6 @@
73
75
  "node": ">=14"
74
76
  },
75
77
  "overrides": {
76
- "js-yaml": ">=4.1.1"
78
+ "glob": ">=11.0.4"
77
79
  }
78
80
  }