eslint-plugin-traceability 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/.github/workflows/ci-cd.yml +56 -0
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/.prettierignore +23 -0
- package/.prettierrc +4 -0
- package/.voder/history.md +164 -0
- package/.voder/implementation-progress.md +145 -0
- package/.voder/last-action.md +106 -0
- package/.voder/plan.md +15 -0
- package/.voder/progress-chart.png +0 -0
- package/.voder/progress-log-areas.csv +34 -0
- package/.voder/progress-log.csv +33 -0
- package/.voder/traceability/docs-stories-001.0-DEV-PLUGIN-SETUP.story.xml +17 -0
- package/.voder/traceability/docs-stories-002.0-DEV-ESLINT-CONFIG.story.xml +13 -0
- package/.voder/traceability/docs-stories-003.0-DEV-FUNCTION-ANNOTATIONS.story.xml +9 -0
- package/.voder/traceability/docs-stories-004.0-DEV-BRANCH-ANNOTATIONS.story.xml +9 -0
- package/.voder/traceability/docs-stories-005.0-DEV-ANNOTATION-VALIDATION.story.xml +9 -0
- package/.voder/traceability/docs-stories-006.0-DEV-FILE-VALIDATION.story.xml +9 -0
- package/.voder/traceability/docs-stories-007.0-DEV-ERROR-REPORTING.story.xml +9 -0
- package/.voder/traceability/docs-stories-008.0-DEV-AUTO-FIX.story.xml +9 -0
- package/.voder/traceability/docs-stories-009.0-DEV-MAINTENANCE-TOOLS.story.xml +16 -0
- package/.voder/traceability/docs-stories-010.0-DEV-DEEP-VALIDATION.story.xml +11 -0
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +97 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/cli-integration.js +157 -0
- package/docs/cli-integration.md +103 -0
- package/docs/config-presets.md +38 -0
- package/docs/decisions/001-typescript-for-eslint-plugin.accepted.md +111 -0
- package/docs/decisions/002-jest-for-eslint-testing.accepted.md +137 -0
- package/docs/decisions/003-code-quality-ratcheting-plan.md +48 -0
- package/docs/eslint-9-setup-guide.md +517 -0
- package/docs/eslint-plugin-development-guide.md +483 -0
- package/docs/jest-testing-guide.md +100 -0
- package/docs/rules/require-branch-annotation.md +34 -0
- package/docs/rules/require-req-annotation.md +39 -0
- package/docs/rules/require-story-annotation.md +36 -0
- package/docs/rules/valid-annotation-format.md +52 -0
- package/docs/rules/valid-req-reference.md +58 -0
- package/docs/rules/valid-story-reference.md +47 -0
- package/docs/security-incidents/unresolved-vulnerabilities.md +11 -0
- package/docs/stories/001.0-DEV-PLUGIN-SETUP.story.md +82 -0
- package/docs/stories/002.0-DEV-ESLINT-CONFIG.story.md +82 -0
- package/docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md +85 -0
- package/docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md +107 -0
- package/docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md +119 -0
- package/docs/stories/006.0-DEV-FILE-VALIDATION.story.md +127 -0
- package/docs/stories/007.0-DEV-ERROR-REPORTING.story.md +89 -0
- package/docs/stories/008.0-DEV-AUTO-FIX.story.md +104 -0
- package/docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md +104 -0
- package/docs/stories/010.0-DEV-DEEP-VALIDATION.story.md +110 -0
- package/docs/stories/developer-story.map.md +118 -0
- package/eslint.config.js +146 -0
- package/jest.config.js +21 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +11 -0
- package/lib/src/index.d.ts +80 -0
- package/lib/src/index.js +58 -0
- package/lib/src/maintenance/batch.d.ts +16 -0
- package/lib/src/maintenance/batch.js +28 -0
- package/lib/src/maintenance/detect.d.ts +6 -0
- package/lib/src/maintenance/detect.js +69 -0
- package/lib/src/maintenance/index.d.ts +14 -0
- package/lib/src/maintenance/index.js +22 -0
- package/lib/src/maintenance/report.d.ts +7 -0
- package/lib/src/maintenance/report.js +17 -0
- package/lib/src/maintenance/update.d.ts +6 -0
- package/lib/src/maintenance/update.js +67 -0
- package/lib/src/maintenance/utils.d.ts +6 -0
- package/lib/src/maintenance/utils.js +64 -0
- package/lib/src/rules/require-branch-annotation.d.ts +7 -0
- package/lib/src/rules/require-branch-annotation.js +111 -0
- package/lib/src/rules/require-req-annotation.d.ts +7 -0
- package/lib/src/rules/require-req-annotation.js +38 -0
- package/lib/src/rules/require-story-annotation.d.ts +7 -0
- package/lib/src/rules/require-story-annotation.js +50 -0
- package/lib/src/rules/valid-annotation-format.d.ts +10 -0
- package/lib/src/rules/valid-annotation-format.js +60 -0
- package/lib/src/rules/valid-req-reference.d.ts +3 -0
- package/lib/src/rules/valid-req-reference.js +104 -0
- package/lib/src/rules/valid-story-reference.d.ts +3 -0
- package/lib/src/rules/valid-story-reference.js +168 -0
- package/lib/tests/basic.test.d.ts +1 -0
- package/lib/tests/basic.test.js +51 -0
- package/lib/tests/fixtures/stale/example.d.ts +0 -0
- package/lib/tests/fixtures/stale/example.js +3 -0
- package/lib/tests/fixtures/update/example.d.ts +0 -0
- package/lib/tests/fixtures/update/example.js +3 -0
- package/lib/tests/fixtures/valid-annotations/example.d.ts +0 -0
- package/lib/tests/fixtures/valid-annotations/example.js +3 -0
- package/lib/tests/index.test.d.ts +1 -0
- package/lib/tests/index.test.js +68 -0
- package/lib/tests/integration/file-validation.test.d.ts +1 -0
- package/lib/tests/integration/file-validation.test.js +60 -0
- package/lib/tests/integration/plugin-validation.test.d.ts +1 -0
- package/lib/tests/integration/plugin-validation.test.js +77 -0
- package/lib/tests/maintenance/batch.test.d.ts +1 -0
- package/lib/tests/maintenance/batch.test.js +79 -0
- package/lib/tests/maintenance/detect-isolated.test.d.ts +1 -0
- package/lib/tests/maintenance/detect-isolated.test.js +90 -0
- package/lib/tests/maintenance/detect.test.d.ts +1 -0
- package/lib/tests/maintenance/detect.test.js +23 -0
- package/lib/tests/maintenance/report.test.d.ts +1 -0
- package/lib/tests/maintenance/report.test.js +67 -0
- package/lib/tests/maintenance/update-isolated.test.d.ts +1 -0
- package/lib/tests/maintenance/update-isolated.test.js +66 -0
- package/lib/tests/maintenance/update.test.d.ts +1 -0
- package/lib/tests/maintenance/update.test.js +26 -0
- package/lib/tests/rules/require-branch-annotation.test.d.ts +1 -0
- package/lib/tests/rules/require-branch-annotation.test.js +251 -0
- package/lib/tests/rules/require-req-annotation.test.d.ts +1 -0
- package/lib/tests/rules/require-req-annotation.test.js +41 -0
- package/lib/tests/rules/require-story-annotation.test.d.ts +1 -0
- package/lib/tests/rules/require-story-annotation.test.js +35 -0
- package/lib/tests/rules/valid-annotation-format.test.d.ts +1 -0
- package/lib/tests/rules/valid-annotation-format.test.js +58 -0
- package/lib/tests/rules/valid-req-reference.test.d.ts +1 -0
- package/lib/tests/rules/valid-req-reference.test.js +87 -0
- package/lib/tests/rules/valid-story-reference.test.d.ts +1 -0
- package/lib/tests/rules/valid-story-reference.test.js +69 -0
- package/package.json +67 -0
- package/src/index.ts +56 -0
- package/src/maintenance/batch.ts +29 -0
- package/src/maintenance/detect.ts +42 -0
- package/src/maintenance/index.ts +14 -0
- package/src/maintenance/report.ts +15 -0
- package/src/maintenance/update.ts +40 -0
- package/src/maintenance/utils.ts +28 -0
- package/src/rules/require-branch-annotation.ts +114 -0
- package/src/rules/require-req-annotation.ts +36 -0
- package/src/rules/require-story-annotation.ts +52 -0
- package/src/rules/valid-annotation-format.ts +62 -0
- package/src/rules/valid-req-reference.ts +114 -0
- package/src/rules/valid-story-reference.ts +213 -0
- package/tests/basic.test.ts +17 -0
- package/tests/fixtures/stale/example.ts +2 -0
- package/tests/fixtures/story_bullet.md +6 -0
- package/tests/fixtures/update/example.ts +2 -0
- package/tests/fixtures/valid-annotations/example.ts +2 -0
- package/tests/index.test.ts +46 -0
- package/tests/integration/file-validation.test.ts +67 -0
- package/tests/integration/plugin-validation.test.ts +79 -0
- package/tests/maintenance/batch.test.ts +55 -0
- package/tests/maintenance/detect-isolated.test.ts +61 -0
- package/tests/maintenance/detect.test.ts +19 -0
- package/tests/maintenance/report.test.ts +37 -0
- package/tests/maintenance/update-isolated.test.ts +39 -0
- package/tests/maintenance/update.test.ts +21 -0
- package/tests/rules/require-branch-annotation.test.ts +248 -0
- package/tests/rules/require-req-annotation.test.ts +38 -0
- package/tests/rules/require-story-annotation.test.ts +32 -0
- package/tests/rules/valid-annotation-format.test.ts +55 -0
- package/tests/rules/valid-req-reference.test.ts +85 -0
- package/tests/rules/valid-story-reference.test.ts +66 -0
- package/tsconfig.json +15 -0
- package/user-docs/api-reference.md +135 -0
- package/user-docs/examples.md +73 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getAllFiles = getAllFiles;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Recursively retrieve all files in a directory.
|
|
41
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
42
|
+
* @req REQ-MAINT-UTILS - Extract common file traversal logic for maintenance tools
|
|
43
|
+
*/
|
|
44
|
+
function getAllFiles(dir) {
|
|
45
|
+
const fileList = [];
|
|
46
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
47
|
+
return fileList;
|
|
48
|
+
}
|
|
49
|
+
function traverse(currentDir) {
|
|
50
|
+
const entries = fs.readdirSync(currentDir);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const fullPath = path.join(currentDir, entry);
|
|
53
|
+
const stat = fs.statSync(fullPath);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
traverse(fullPath);
|
|
56
|
+
}
|
|
57
|
+
else if (stat.isFile()) {
|
|
58
|
+
fileList.push(fullPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
traverse(dir);
|
|
63
|
+
return fileList;
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule to enforce @story and @req annotations on significant code branches
|
|
3
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
4
|
+
* @req REQ-BRANCH-DETECTION - Detect significant code branches for traceability annotations
|
|
5
|
+
*/
|
|
6
|
+
declare const _default: any;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Rule to enforce @story and @req annotations on significant code branches
|
|
5
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
6
|
+
* @req REQ-BRANCH-DETECTION - Detect significant code branches for traceability annotations
|
|
7
|
+
*/
|
|
8
|
+
exports.default = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Require @story and @req annotations on code branches",
|
|
13
|
+
recommended: "error",
|
|
14
|
+
},
|
|
15
|
+
fixable: "code",
|
|
16
|
+
messages: {
|
|
17
|
+
missingAnnotation: "Missing {{missing}} annotation on code branch",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
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
|
+
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,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Rule to enforce @req annotation on functions
|
|
5
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
6
|
+
* @req REQ-ANNOTATION-REQUIRED - Require @req annotation on functions
|
|
7
|
+
*/
|
|
8
|
+
exports.default = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
fixable: "code",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Require @req annotations on functions",
|
|
14
|
+
recommended: "error",
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
missingReq: "Missing @req annotation",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const sourceCode = context.getSourceCode();
|
|
23
|
+
return {
|
|
24
|
+
FunctionDeclaration(node) {
|
|
25
|
+
const jsdoc = sourceCode.getJSDocComment(node);
|
|
26
|
+
if (!jsdoc || !jsdoc.value.includes("@req")) {
|
|
27
|
+
context.report({
|
|
28
|
+
node,
|
|
29
|
+
messageId: "missingReq",
|
|
30
|
+
fix(fixer) {
|
|
31
|
+
return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Rule to enforce @story annotation on functions
|
|
5
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
6
|
+
* @req REQ-ANNOTATION-REQUIRED - Require @story annotation on functions
|
|
7
|
+
*/
|
|
8
|
+
exports.default = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Require @story annotations on functions",
|
|
13
|
+
recommended: "error",
|
|
14
|
+
},
|
|
15
|
+
fixable: "code",
|
|
16
|
+
messages: {
|
|
17
|
+
missingStory: "Missing @story annotation",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const sourceCode = context.getSourceCode();
|
|
23
|
+
return {
|
|
24
|
+
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
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule to validate @story and @req annotation format and syntax
|
|
3
|
+
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
4
|
+
* @req REQ-FORMAT-SPECIFICATION - Define clear format rules for @story and @req annotations
|
|
5
|
+
* @req REQ-SYNTAX-VALIDATION - Validate annotation syntax matches specification
|
|
6
|
+
* @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
|
|
7
|
+
* @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: any;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Rule to validate @story and @req annotation format and syntax
|
|
5
|
+
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
6
|
+
* @req REQ-FORMAT-SPECIFICATION - Define clear format rules for @story and @req annotations
|
|
7
|
+
* @req REQ-SYNTAX-VALIDATION - Validate annotation syntax matches specification
|
|
8
|
+
* @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
|
|
9
|
+
* @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
|
|
10
|
+
*/
|
|
11
|
+
exports.default = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Validate format and syntax of @story and @req annotations",
|
|
16
|
+
recommended: "error",
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
invalidStoryFormat: "Invalid @story annotation format",
|
|
20
|
+
invalidReqFormat: "Invalid @req annotation format",
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const sourceCode = context.getSourceCode();
|
|
26
|
+
return {
|
|
27
|
+
Program() {
|
|
28
|
+
const comments = sourceCode.getAllComments() || [];
|
|
29
|
+
comments.forEach((comment) => {
|
|
30
|
+
const lines = comment.value
|
|
31
|
+
.split(/\r?\n/)
|
|
32
|
+
.map((l) => l.replace(/^[^@]*/, "").trim());
|
|
33
|
+
lines.forEach((line) => {
|
|
34
|
+
if (line.startsWith("@story")) {
|
|
35
|
+
const parts = line.split(/\s+/);
|
|
36
|
+
const storyPath = parts[1];
|
|
37
|
+
if (!storyPath ||
|
|
38
|
+
!/^docs\/stories\/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$/.test(storyPath)) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: comment,
|
|
41
|
+
messageId: "invalidStoryFormat",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith("@req")) {
|
|
46
|
+
const parts = line.split(/\s+/);
|
|
47
|
+
const reqId = parts[1];
|
|
48
|
+
if (!reqId || !/^REQ-[A-Z0-9-]+$/.test(reqId)) {
|
|
49
|
+
context.report({
|
|
50
|
+
node: comment,
|
|
51
|
+
messageId: "invalidReqFormat",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
/* eslint-env node */
|
|
7
|
+
/**
|
|
8
|
+
* Rule to validate @req annotation references refer to existing requirements in story files
|
|
9
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
10
|
+
* @req REQ-DEEP-PARSE - Parse story files to extract requirement identifiers
|
|
11
|
+
* @req REQ-DEEP-MATCH - Validate @req references against story file content
|
|
12
|
+
* @req REQ-DEEP-CACHE - Cache parsed story content for performance
|
|
13
|
+
* @req REQ-DEEP-PATH - Protect against path traversal in story paths
|
|
14
|
+
*/
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
exports.default = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Validate that @req annotations reference existing requirements in referenced story files",
|
|
22
|
+
recommended: "error",
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
reqMissing: "Requirement '{{reqId}}' not found in '{{storyPath}}'",
|
|
26
|
+
invalidPath: "Invalid story path '{{storyPath}}'",
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
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
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
/* eslint-env node */
|
|
7
|
+
/**
|
|
8
|
+
* Rule to validate @story annotation references refer to existing story files
|
|
9
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
10
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
11
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
12
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
13
|
+
*/
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const defaultStoryDirs = ["docs/stories", "stories"];
|
|
17
|
+
const fileExistCache = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Build possible file paths for a given storyPath.
|
|
20
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
21
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
22
|
+
*/
|
|
23
|
+
function buildCandidates(storyPath, cwd, storyDirs) {
|
|
24
|
+
const candidates = [];
|
|
25
|
+
if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
|
|
26
|
+
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
30
|
+
for (const dir of storyDirs) {
|
|
31
|
+
candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return candidates;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if any of the candidate files exist.
|
|
38
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
39
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
40
|
+
*/
|
|
41
|
+
function existsAny(paths) {
|
|
42
|
+
for (const candidate of paths) {
|
|
43
|
+
let ok = fileExistCache.get(candidate);
|
|
44
|
+
if (ok === undefined) {
|
|
45
|
+
ok = fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile();
|
|
46
|
+
fileExistCache.set(candidate, ok);
|
|
47
|
+
}
|
|
48
|
+
if (ok) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate a single @story annotation line.
|
|
56
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
57
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
58
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
59
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
60
|
+
*/
|
|
61
|
+
function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt) {
|
|
62
|
+
const parts = line.split(/\s+/);
|
|
63
|
+
const storyPath = parts[1];
|
|
64
|
+
if (!storyPath) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Absolute path check
|
|
68
|
+
if (path_1.default.isAbsolute(storyPath)) {
|
|
69
|
+
if (!allowAbsolute) {
|
|
70
|
+
context.report({
|
|
71
|
+
node: commentNode,
|
|
72
|
+
messageId: "invalidPath",
|
|
73
|
+
data: { path: storyPath },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Path traversal prevention
|
|
79
|
+
if (storyPath.includes("..")) {
|
|
80
|
+
const normalized = path_1.default.normalize(storyPath);
|
|
81
|
+
const full = path_1.default.resolve(cwd, normalized);
|
|
82
|
+
if (!full.startsWith(cwd + path_1.default.sep)) {
|
|
83
|
+
context.report({
|
|
84
|
+
node: commentNode,
|
|
85
|
+
messageId: "invalidPath",
|
|
86
|
+
data: { path: storyPath },
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Extension check
|
|
92
|
+
if (requireExt && !storyPath.endsWith(".story.md")) {
|
|
93
|
+
context.report({
|
|
94
|
+
node: commentNode,
|
|
95
|
+
messageId: "invalidExtension",
|
|
96
|
+
data: { path: storyPath },
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Build candidate paths and check existence
|
|
101
|
+
const candidates = buildCandidates(storyPath, cwd, storyDirs);
|
|
102
|
+
if (!existsAny(candidates)) {
|
|
103
|
+
context.report({
|
|
104
|
+
node: commentNode,
|
|
105
|
+
messageId: "fileMissing",
|
|
106
|
+
data: { path: storyPath },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Handle a single comment node by processing its lines.
|
|
112
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
113
|
+
* @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
|
|
114
|
+
*/
|
|
115
|
+
function handleComment(commentNode, context, sourceCode, cwd, storyDirs, allowAbsolute, requireExt) {
|
|
116
|
+
const lines = commentNode.value
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.map((l) => l.replace(/^[^@]*/, "").trim());
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (line.startsWith("@story")) {
|
|
121
|
+
validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.default = {
|
|
126
|
+
meta: {
|
|
127
|
+
type: "problem",
|
|
128
|
+
docs: {
|
|
129
|
+
description: "Validate that @story annotations reference existing .story.md files",
|
|
130
|
+
recommended: "error",
|
|
131
|
+
},
|
|
132
|
+
messages: {
|
|
133
|
+
fileMissing: "Story file '{{path}}' not found",
|
|
134
|
+
invalidExtension: "Invalid story file extension for '{{path}}', expected '.story.md'",
|
|
135
|
+
invalidPath: "Invalid story path '{{path}}'",
|
|
136
|
+
},
|
|
137
|
+
schema: [
|
|
138
|
+
{
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
storyDirectories: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: { type: "string" },
|
|
144
|
+
},
|
|
145
|
+
allowAbsolutePaths: { type: "boolean" },
|
|
146
|
+
requireStoryExtension: { type: "boolean" },
|
|
147
|
+
},
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
create(context) {
|
|
153
|
+
const sourceCode = context.getSourceCode();
|
|
154
|
+
const cwd = process.cwd();
|
|
155
|
+
const opts = context.options[0];
|
|
156
|
+
const storyDirs = opts?.storyDirectories || defaultStoryDirs;
|
|
157
|
+
const allowAbsolute = opts?.allowAbsolutePaths || false;
|
|
158
|
+
const requireExt = opts?.requireStoryExtension !== false;
|
|
159
|
+
return {
|
|
160
|
+
Program() {
|
|
161
|
+
const comments = sourceCode.getAllComments() || [];
|
|
162
|
+
for (const comment of comments) {
|
|
163
|
+
handleComment(comment, context, sourceCode, cwd, storyDirs, allowAbsolute, requireExt);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|