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 +2 -2
- package/lib/src/rules/require-branch-annotation.d.ts +1 -1
- package/lib/src/rules/require-branch-annotation.js +89 -87
- package/lib/src/rules/require-story-annotation.d.ts +4 -1
- package/lib/src/rules/require-story-annotation.js +179 -27
- package/lib/src/rules/valid-req-reference.js +129 -72
- package/lib/tests/config/eslint-config-validation.test.d.ts +1 -0
- package/lib/tests/config/eslint-config-validation.test.js +19 -0
- package/lib/tests/config/require-story-annotation-config.test.d.ts +1 -0
- package/lib/tests/config/require-story-annotation-config.test.js +15 -0
- package/lib/tests/maintenance/detect-isolated.test.js +17 -15
- package/lib/tests/maintenance/detect.test.js +8 -4
- package/lib/tests/maintenance/update-isolated.test.js +12 -12
- package/lib/tests/rules/require-story-annotation.test.js +84 -1
- package/lib/tests/rules/valid-req-reference.test.js +1 -1
- package/package.json +9 -7
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,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
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:
|
|
101
|
-
SwitchCase:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
62
|
+
fs.writeFileSync(filePath1, content1, "utf8");
|
|
63
|
+
const content2 = `
|
|
64
64
|
/**
|
|
65
65
|
* @story stale2.story.md
|
|
66
66
|
*/
|
|
67
67
|
`;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
|
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": "
|
|
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": "^
|
|
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": "^
|
|
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": "^
|
|
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
|
-
"
|
|
78
|
+
"glob": ">=11.0.4"
|
|
77
79
|
}
|
|
78
80
|
}
|