eslint-plugin-traceability 1.1.7 → 1.1.8

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,10 +1,80 @@
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
+ * Helper to check a branch AST node for traceability annotations.
10
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
11
+ * @req REQ-BRANCH-DETECTION - Helper for branch annotation detection
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) {
21
+ const lines = sourceCode.lines;
22
+ const startLine = node.loc.start.line;
23
+ let i = startLine - 1;
24
+ const fallbackComments = [];
25
+ while (i > 0) {
26
+ const lineText = lines[i - 1];
27
+ if (/^\s*(\/\/|\/\*)/.test(lineText)) {
28
+ fallbackComments.unshift(lineText.trim());
29
+ i--;
30
+ }
31
+ else if (/^\s*$/.test(lineText)) {
32
+ break;
33
+ }
34
+ else {
35
+ break;
36
+ }
37
+ }
38
+ comments = fallbackComments.map((text) => ({ value: text }));
39
+ }
40
+ const text = comments.map((c) => c.value).join(" ");
41
+ const missingStory = !/@story\b/.test(text);
42
+ const missingReq = !/@req\b/.test(text);
43
+ if (missingStory) {
44
+ const reportObj = {
45
+ node,
46
+ messageId: "missingAnnotation",
47
+ data: { missing: "@story" },
48
+ };
49
+ if (node.type !== "CatchClause") {
50
+ if (node.type === "SwitchCase") {
51
+ const indent = " ".repeat(node.loc.start.column);
52
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @story <story-file>.story.md\n${indent}`);
53
+ }
54
+ else {
55
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @story <story-file>.story.md\n`);
56
+ }
57
+ }
58
+ context.report(reportObj);
59
+ }
60
+ if (missingReq) {
61
+ const reportObj = {
62
+ node,
63
+ messageId: "missingAnnotation",
64
+ data: { missing: "@req" },
65
+ };
66
+ if (!missingStory && node.type !== "CatchClause") {
67
+ if (node.type === "SwitchCase") {
68
+ const indent = " ".repeat(node.loc.start.column);
69
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @req <REQ-ID>\n${indent}`);
70
+ }
71
+ else {
72
+ reportObj.fix = (fixer) => fixer.insertTextBefore(node, `// @req <REQ-ID>\n`);
73
+ }
74
+ }
75
+ context.report(reportObj);
76
+ }
77
+ }
8
78
  exports.default = {
9
79
  meta: {
10
80
  type: "problem",
@@ -20,92 +90,16 @@ exports.default = {
20
90
  },
21
91
  create(context) {
22
92
  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
93
  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,
94
+ IfStatement: (node) => checkBranchNode(sourceCode, context, node),
95
+ SwitchCase: (node) => checkBranchNode(sourceCode, context, node),
96
+ TryStatement: (node) => checkBranchNode(sourceCode, context, node),
97
+ CatchClause: (node) => checkBranchNode(sourceCode, context, node),
98
+ ForStatement: (node) => checkBranchNode(sourceCode, context, node),
99
+ ForOfStatement: (node) => checkBranchNode(sourceCode, context, node),
100
+ ForInStatement: (node) => checkBranchNode(sourceCode, context, node),
101
+ WhileStatement: (node) => checkBranchNode(sourceCode, context, node),
102
+ DoWhileStatement: (node) => checkBranchNode(sourceCode, context, node),
109
103
  };
110
104
  },
111
105
  };
@@ -14,6 +14,81 @@ 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
+ * 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
24
+ */
25
+ function createProgramListener(context) {
26
+ const sourceCode = context.getSourceCode();
27
+ const cwd = process.cwd();
28
+ const reqCache = new Map();
29
+ let rawStoryPath = null;
30
+ return function Program() {
31
+ const comments = sourceCode.getAllComments() || [];
32
+ 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
+ });
89
+ });
90
+ };
91
+ }
17
92
  exports.default = {
18
93
  meta: {
19
94
  type: "problem",
@@ -28,77 +103,7 @@ exports.default = {
28
103
  schema: [],
29
104
  },
30
105
  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
- };
106
+ const program = createProgramListener(context);
107
+ return { Program: program };
103
108
  },
104
109
  };
@@ -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
  });
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.8",
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",
@@ -73,6 +73,7 @@
73
73
  "node": ">=14"
74
74
  },
75
75
  "overrides": {
76
- "js-yaml": ">=4.1.1"
76
+ "js-yaml": ">=4.1.1",
77
+ "tar": ">=6.1.11"
77
78
  }
78
79
  }