eslint-plugin-traceability 1.6.5 → 1.7.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/README.md +38 -1
- package/lib/src/index.d.ts +28 -25
- package/lib/src/index.js +49 -31
- package/lib/src/maintenance/cli.d.ts +12 -0
- package/lib/src/maintenance/cli.js +279 -0
- package/lib/src/maintenance/detect.js +27 -12
- package/lib/src/maintenance/update.js +42 -34
- package/lib/src/maintenance/utils.js +30 -30
- package/lib/src/rules/helpers/require-story-io.js +51 -15
- package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
- package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
- package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
- package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
- package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
- package/lib/src/rules/valid-annotation-format.js +168 -180
- package/lib/src/rules/valid-req-reference.js +139 -29
- package/lib/src/rules/valid-story-reference.d.ts +7 -0
- package/lib/src/rules/valid-story-reference.js +38 -80
- package/lib/src/utils/annotation-checker.js +2 -145
- package/lib/src/utils/branch-annotation-helpers.js +12 -3
- package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
- package/lib/src/utils/reqAnnotationDetection.js +152 -0
- package/lib/tests/maintenance/cli.test.d.ts +1 -0
- package/lib/tests/maintenance/cli.test.js +172 -0
- package/lib/tests/rules/require-branch-annotation.test.js +3 -2
- package/lib/tests/rules/require-req-annotation.test.js +57 -68
- package/lib/tests/rules/require-story-annotation.test.js +13 -28
- package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
- package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
- package/lib/tests/rules/valid-annotation-format.test.js +328 -51
- package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
- package/lib/tests/utils/annotation-checker.test.js +24 -17
- package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
- package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
- package/lib/tests/utils/ts-language-options.d.ts +22 -0
- package/lib/tests/utils/ts-language-options.js +27 -0
- package/package.json +12 -3
|
@@ -31,28 +31,20 @@ function extractStoryPath(comment) {
|
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Validate
|
|
35
|
-
* Performs
|
|
34
|
+
* Validate and resolve the referenced story path.
|
|
35
|
+
* Performs traversal/absolute checks and resolves to a disk path.
|
|
36
36
|
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
37
37
|
* @req REQ-DEEP-PATH - Validate and resolve referenced story file paths
|
|
38
|
-
* @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
|
|
39
|
-
* @req REQ-DEEP-MATCH - Verify that a referenced requirement ID exists in the story
|
|
40
|
-
* @req REQ-DEEP-PARSE - Parse story file contents to extract requirement identifiers
|
|
41
38
|
*/
|
|
42
|
-
function
|
|
43
|
-
const { comment, context,
|
|
44
|
-
const parts = line.split(/\s+/);
|
|
45
|
-
const reqId = parts[1];
|
|
46
|
-
if (!reqId || !storyPath) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
39
|
+
function validateAndResolveStoryPath(opts) {
|
|
40
|
+
const { comment, context, storyPath, cwd } = opts;
|
|
49
41
|
if (storyPath.includes("..") || path_1.default.isAbsolute(storyPath)) {
|
|
50
42
|
context.report({
|
|
51
43
|
node: comment,
|
|
52
44
|
messageId: "invalidPath",
|
|
53
45
|
data: { storyPath },
|
|
54
46
|
});
|
|
55
|
-
return;
|
|
47
|
+
return null;
|
|
56
48
|
}
|
|
57
49
|
const resolvedStoryPath = path_1.default.resolve(cwd, storyPath);
|
|
58
50
|
if (!resolvedStoryPath.startsWith(cwd + path_1.default.sep) &&
|
|
@@ -62,8 +54,19 @@ function validateReqLine(opts) {
|
|
|
62
54
|
messageId: "invalidPath",
|
|
63
55
|
data: { storyPath },
|
|
64
56
|
});
|
|
65
|
-
return;
|
|
57
|
+
return null;
|
|
66
58
|
}
|
|
59
|
+
return resolvedStoryPath;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load and cache requirement IDs from a story file.
|
|
63
|
+
* Reads the story file, extracts requirement IDs, and updates the cache.
|
|
64
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
65
|
+
* @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
|
|
66
|
+
* @req REQ-DEEP-PARSE - Parse story file contents to extract requirement identifiers
|
|
67
|
+
*/
|
|
68
|
+
function loadAndCacheRequirements(opts) {
|
|
69
|
+
const { resolvedStoryPath, reqCache } = opts;
|
|
67
70
|
if (!reqCache.has(resolvedStoryPath)) {
|
|
68
71
|
try {
|
|
69
72
|
const content = fs_1.default.readFileSync(resolvedStoryPath, "utf8");
|
|
@@ -79,7 +82,15 @@ function validateReqLine(opts) {
|
|
|
79
82
|
reqCache.set(resolvedStoryPath, new Set());
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
|
|
85
|
+
return reqCache.get(resolvedStoryPath);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Perform the final requirement existence check and report if missing.
|
|
89
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
90
|
+
* @req REQ-DEEP-MATCH - Verify that a referenced requirement ID exists in the story
|
|
91
|
+
*/
|
|
92
|
+
function checkRequirementExists(opts) {
|
|
93
|
+
const { comment, context, reqId, storyPath, reqSet } = opts;
|
|
83
94
|
if (!reqSet.has(reqId)) {
|
|
84
95
|
context.report({
|
|
85
96
|
node: comment,
|
|
@@ -88,6 +99,71 @@ function validateReqLine(opts) {
|
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract requirement ID from a @req line.
|
|
104
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
105
|
+
* @req REQ-DEEP-PARSE - Parse annotation lines to extract requirement IDs
|
|
106
|
+
*/
|
|
107
|
+
function extractReqIdFromLine(line) {
|
|
108
|
+
const parts = line.split(/\s+/);
|
|
109
|
+
return parts[1];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve story path and load requirements set for validation.
|
|
113
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
114
|
+
* @req REQ-DEEP-PATH - Validate and resolve referenced story file paths
|
|
115
|
+
* @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
|
|
116
|
+
*/
|
|
117
|
+
function resolveStoryAndRequirements(opts) {
|
|
118
|
+
const { comment, context, storyPath, cwd, reqCache } = opts;
|
|
119
|
+
const resolvedStoryPath = validateAndResolveStoryPath({
|
|
120
|
+
comment,
|
|
121
|
+
context,
|
|
122
|
+
storyPath,
|
|
123
|
+
cwd,
|
|
124
|
+
});
|
|
125
|
+
if (!resolvedStoryPath) {
|
|
126
|
+
return { resolvedStoryPath: null, reqSet: null };
|
|
127
|
+
}
|
|
128
|
+
const reqSet = loadAndCacheRequirements({
|
|
129
|
+
resolvedStoryPath,
|
|
130
|
+
reqCache,
|
|
131
|
+
});
|
|
132
|
+
return { resolvedStoryPath, reqSet };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Validate a @req annotation line against the extracted story content.
|
|
136
|
+
* Performs path validation, file reading, caching, and requirement existence checks.
|
|
137
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
138
|
+
* @req REQ-DEEP-PATH - Validate and resolve referenced story file paths
|
|
139
|
+
* @req REQ-DEEP-CACHE - Cache requirement IDs discovered in story files
|
|
140
|
+
* @req REQ-DEEP-MATCH - Verify that a referenced requirement ID exists in the story
|
|
141
|
+
* @req REQ-DEEP-PARSE - Parse story file contents to extract requirement identifiers
|
|
142
|
+
*/
|
|
143
|
+
function validateReqLine(opts) {
|
|
144
|
+
const { comment, context, line, storyPath, cwd, reqCache } = opts;
|
|
145
|
+
const reqId = extractReqIdFromLine(line);
|
|
146
|
+
if (!reqId || !storyPath) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { reqSet } = resolveStoryAndRequirements({
|
|
150
|
+
comment,
|
|
151
|
+
context,
|
|
152
|
+
storyPath,
|
|
153
|
+
cwd,
|
|
154
|
+
reqCache,
|
|
155
|
+
});
|
|
156
|
+
if (!reqSet) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
checkRequirementExists({
|
|
160
|
+
comment,
|
|
161
|
+
context,
|
|
162
|
+
reqId,
|
|
163
|
+
storyPath,
|
|
164
|
+
reqSet,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
91
167
|
/**
|
|
92
168
|
* Handle a single annotation line for story or requirement metadata.
|
|
93
169
|
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
@@ -107,15 +183,14 @@ function handleAnnotationLine(opts) {
|
|
|
107
183
|
return storyPath;
|
|
108
184
|
}
|
|
109
185
|
/**
|
|
110
|
-
*
|
|
186
|
+
* Iterate over all raw lines in a comment and update storyPath as needed.
|
|
111
187
|
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
112
188
|
* @req REQ-DEEP-PARSE - Iterate comment lines to process @story/@req annotations
|
|
113
189
|
* @req REQ-DEEP-MATCH - Coordinate annotation handling across a comment block
|
|
114
|
-
* @req REQ-DEEP-CACHE - Maintain and reuse discovered story path across comments
|
|
115
190
|
*/
|
|
116
|
-
function
|
|
117
|
-
const { comment, context, cwd, reqCache,
|
|
118
|
-
let storyPath =
|
|
191
|
+
function processCommentLines(opts) {
|
|
192
|
+
const { comment, context, cwd, reqCache, initialStoryPath } = opts;
|
|
193
|
+
let storyPath = initialStoryPath;
|
|
119
194
|
const rawLines = comment.value.split(/\r?\n/);
|
|
120
195
|
for (const rawLine of rawLines) {
|
|
121
196
|
const line = rawLine.trim().replace(/^\*+\s*/, "");
|
|
@@ -130,6 +205,44 @@ function handleComment(opts) {
|
|
|
130
205
|
}
|
|
131
206
|
return storyPath;
|
|
132
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Handle JSDoc story and req annotations for a single comment block.
|
|
210
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
211
|
+
* @req REQ-DEEP-PARSE - Iterate comment lines to process @story/@req annotations
|
|
212
|
+
* @req REQ-DEEP-MATCH - Coordinate annotation handling across a comment block
|
|
213
|
+
* @req REQ-DEEP-CACHE - Maintain and reuse discovered story path across comments
|
|
214
|
+
*/
|
|
215
|
+
function handleComment(opts) {
|
|
216
|
+
const { comment, context, cwd, reqCache, rawStoryPath } = opts;
|
|
217
|
+
return processCommentLines({
|
|
218
|
+
comment,
|
|
219
|
+
context,
|
|
220
|
+
cwd,
|
|
221
|
+
reqCache,
|
|
222
|
+
initialStoryPath: rawStoryPath,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get all comments from source and drive comment-level handling.
|
|
227
|
+
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
228
|
+
* @req REQ-DEEP-PARSE - Collect all comments from the source code
|
|
229
|
+
* @req REQ-DEEP-MATCH - Drive comment-level handling for traceability checks
|
|
230
|
+
* @req REQ-DEEP-CACHE - Reuse story path and requirement cache across comments
|
|
231
|
+
*/
|
|
232
|
+
function processAllComments(opts) {
|
|
233
|
+
const { sourceCode, context, cwd, reqCache } = opts;
|
|
234
|
+
let rawStoryPath = opts.initialStoryPath;
|
|
235
|
+
const comments = sourceCode.getAllComments() || [];
|
|
236
|
+
comments.forEach((comment) => {
|
|
237
|
+
rawStoryPath = handleComment({
|
|
238
|
+
comment,
|
|
239
|
+
context,
|
|
240
|
+
cwd,
|
|
241
|
+
reqCache,
|
|
242
|
+
rawStoryPath,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
133
246
|
/**
|
|
134
247
|
* Create a Program listener that iterates comments and validates annotations.
|
|
135
248
|
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
@@ -150,15 +263,12 @@ function programListener(context) {
|
|
|
150
263
|
* @req REQ-DEEP-PATH - Ensure validation respects project-relative paths
|
|
151
264
|
*/
|
|
152
265
|
return function Program() {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
reqCache,
|
|
160
|
-
rawStoryPath,
|
|
161
|
-
});
|
|
266
|
+
processAllComments({
|
|
267
|
+
sourceCode,
|
|
268
|
+
context,
|
|
269
|
+
cwd,
|
|
270
|
+
reqCache,
|
|
271
|
+
initialStoryPath: rawStoryPath,
|
|
162
272
|
});
|
|
163
273
|
};
|
|
164
274
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule to validate @story annotation references refer to existing story files
|
|
3
|
+
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
4
|
+
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
5
|
+
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
6
|
+
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
7
|
+
*/
|
|
1
8
|
import type { Rule } from "eslint";
|
|
2
9
|
declare const _default: Rule.RuleModule;
|
|
3
10
|
export default _default;
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
|
|
3
|
+
const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
|
|
4
|
+
const valid_story_reference_helpers_1 = require("./helpers/valid-story-reference-helpers");
|
|
5
|
+
const defaultStoryDirs = ["docs/stories", "stories"];
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
7
|
+
* Shared helper to report an invalid story path. Centralizes the
|
|
8
|
+
* `invalidPath` diagnostic so callers don't repeat the same
|
|
9
|
+
* `context.report` shape.
|
|
10
|
+
*
|
|
9
11
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
10
|
-
* @req REQ-
|
|
11
|
-
* @req REQ-
|
|
12
|
-
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
12
|
+
* @req REQ-PROJECT-BOUNDARY - Ensure resolved candidate paths remain within the project root
|
|
13
|
+
* @req REQ-ERROR-CONSISTENCY - Maintain a consistent template for invalid path diagnostics across rules
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
function reportInvalidPath(opts) {
|
|
16
|
+
const { storyPath, commentNode, context } = opts;
|
|
17
|
+
context.report({
|
|
18
|
+
node: commentNode,
|
|
19
|
+
messageId: "invalidPath",
|
|
20
|
+
data: { path: storyPath },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
17
23
|
/**
|
|
18
24
|
* Extract the story path from the annotation line and delegate validation.
|
|
19
25
|
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
@@ -35,28 +41,6 @@ function validateStoryPath(opts) {
|
|
|
35
41
|
requireExt,
|
|
36
42
|
});
|
|
37
43
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Analyze candidate paths against the project boundary, returning whether any
|
|
40
|
-
* are within the project and whether any are outside.
|
|
41
|
-
*
|
|
42
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
43
|
-
* @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
|
|
44
|
-
* @req REQ-CONFIGURABLE-PATHS - Respect configured storyDirectories while enforcing project boundaries
|
|
45
|
-
*/
|
|
46
|
-
function analyzeCandidateBoundaries(candidates, cwd) {
|
|
47
|
-
let hasInProjectCandidate = false;
|
|
48
|
-
let hasOutOfProjectCandidate = false;
|
|
49
|
-
for (const candidate of candidates) {
|
|
50
|
-
const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(candidate, cwd);
|
|
51
|
-
if (boundary.isWithinProject) {
|
|
52
|
-
hasInProjectCandidate = true;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
hasOutOfProjectCandidate = true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { hasInProjectCandidate, hasOutOfProjectCandidate };
|
|
59
|
-
}
|
|
60
44
|
/**
|
|
61
45
|
* Handle existence status and report appropriate diagnostics for missing
|
|
62
46
|
* or filesystem-error conditions, assuming project-boundary checks have
|
|
@@ -116,29 +100,17 @@ function reportExistenceProblems(opts) {
|
|
|
116
100
|
const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
|
|
117
101
|
const existenceResult = result.existence;
|
|
118
102
|
const candidates = result.candidates || [];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (existenceResult &&
|
|
131
|
-
existenceResult.status === "exists" &&
|
|
132
|
-
existenceResult.matchedPath) {
|
|
133
|
-
const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(existenceResult.matchedPath, cwd);
|
|
134
|
-
if (!boundary.isWithinProject) {
|
|
135
|
-
context.report({
|
|
136
|
-
node: commentNode,
|
|
137
|
-
messageId: "invalidPath",
|
|
138
|
-
data: { path: storyPath },
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
103
|
+
const invalidByBoundary = (0, valid_story_reference_helpers_1.handleProjectBoundaryForExistence)({
|
|
104
|
+
storyPath,
|
|
105
|
+
commentNode,
|
|
106
|
+
context,
|
|
107
|
+
cwd,
|
|
108
|
+
candidates,
|
|
109
|
+
existenceResult,
|
|
110
|
+
reportInvalidPath,
|
|
111
|
+
});
|
|
112
|
+
if (invalidByBoundary) {
|
|
113
|
+
return;
|
|
142
114
|
}
|
|
143
115
|
reportExistenceStatus(existenceResult, storyPath, commentNode, context);
|
|
144
116
|
}
|
|
@@ -156,30 +128,16 @@ function reportExistenceProblems(opts) {
|
|
|
156
128
|
*/
|
|
157
129
|
function processStoryPath(opts) {
|
|
158
130
|
const { storyPath, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// When absolute paths are allowed, we still enforce extension and
|
|
170
|
-
// project-boundary checks below via the existence phase.
|
|
171
|
-
}
|
|
172
|
-
// Path traversal check
|
|
173
|
-
if ((0, storyReferenceUtils_1.containsPathTraversal)(storyPath)) {
|
|
174
|
-
const full = path_1.default.resolve(cwd, path_1.default.normalize(storyPath));
|
|
175
|
-
if (!full.startsWith(cwd + path_1.default.sep)) {
|
|
176
|
-
context.report({
|
|
177
|
-
node: commentNode,
|
|
178
|
-
messageId: "invalidPath",
|
|
179
|
-
data: { path: storyPath },
|
|
180
|
-
});
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
131
|
+
const securityOk = (0, valid_story_reference_helpers_1.performSecurityValidations)({
|
|
132
|
+
storyPath,
|
|
133
|
+
commentNode,
|
|
134
|
+
context,
|
|
135
|
+
cwd,
|
|
136
|
+
allowAbsolute,
|
|
137
|
+
reportInvalidPath,
|
|
138
|
+
});
|
|
139
|
+
if (!securityOk) {
|
|
140
|
+
return;
|
|
183
141
|
}
|
|
184
142
|
// Extension check
|
|
185
143
|
if (requireExt && !(0, storyReferenceUtils_1.hasValidExtension)(storyPath)) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkReqAnnotation = checkReqAnnotation;
|
|
4
4
|
const require_story_utils_1 = require("../rules/helpers/require-story-utils");
|
|
5
|
-
const
|
|
5
|
+
const reqAnnotationDetection_1 = require("./reqAnnotationDetection");
|
|
6
6
|
/**
|
|
7
7
|
* Helper to retrieve the JSDoc comment for a node.
|
|
8
8
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -35,149 +35,6 @@ function getCommentsBefore(sourceCode, node) {
|
|
|
35
35
|
function combineComments(leading, before) {
|
|
36
36
|
return [...leading, ...before];
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Predicate helper to check whether a comment contains a @req annotation.
|
|
40
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
41
|
-
* @req REQ-ANNOTATION-CHECK-COMMENT - Detect @req tag inside a comment
|
|
42
|
-
*/
|
|
43
|
-
function commentContainsReq(c) {
|
|
44
|
-
return c && typeof c.value === "string" && c.value.includes("@req");
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Line-based helper adapted from linesBeforeHasStory to detect @req.
|
|
48
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
49
|
-
* @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in preceding source lines
|
|
50
|
-
*/
|
|
51
|
-
function linesBeforeHasReq(sourceCode, node) {
|
|
52
|
-
const lines = sourceCode && sourceCode.lines;
|
|
53
|
-
const startLine = node && node.loc && typeof node.loc.start?.line === "number"
|
|
54
|
-
? node.loc.start.line
|
|
55
|
-
: null;
|
|
56
|
-
// Guard against missing or malformed source/loc information before scanning.
|
|
57
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
58
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Avoid false positives when sourceCode/loc is incomplete
|
|
59
|
-
if (!Array.isArray(lines) || typeof startLine !== "number") {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
|
|
63
|
-
const to = Math.max(0, startLine - 1);
|
|
64
|
-
// Scan each physical line in the configured lookback window for an @req marker.
|
|
65
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
66
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Search preceding lines for @req text
|
|
67
|
-
for (let i = from; i < to; i++) {
|
|
68
|
-
const text = lines[i];
|
|
69
|
-
// When a line contains @req we treat the function as already annotated.
|
|
70
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
71
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in raw source lines
|
|
72
|
-
if (typeof text === "string" && text.includes("@req")) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Parent-chain helper adapted from parentChainHasStory to detect @req.
|
|
80
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
81
|
-
* @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in parent-chain comments
|
|
82
|
-
*/
|
|
83
|
-
function parentChainHasReq(sourceCode, node) {
|
|
84
|
-
let p = node && node.parent;
|
|
85
|
-
// Walk up the parent chain and inspect comments attached to each ancestor.
|
|
86
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
87
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Traverse parent nodes when local comments are absent
|
|
88
|
-
while (p) {
|
|
89
|
-
const pComments = typeof sourceCode?.getCommentsBefore === "function"
|
|
90
|
-
? sourceCode.getCommentsBefore(p) || []
|
|
91
|
-
: [];
|
|
92
|
-
// Look for @req in comments immediately preceding each parent node.
|
|
93
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
94
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent comments
|
|
95
|
-
if (Array.isArray(pComments) && pComments.some(commentContainsReq)) {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
const pLeading = p.leadingComments || [];
|
|
99
|
-
// Also inspect leadingComments attached directly to the parent node.
|
|
100
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
101
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent leadingComments
|
|
102
|
-
if (Array.isArray(pLeading) && pLeading.some(commentContainsReq)) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
p = p.parent;
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
|
|
111
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
112
|
-
* @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in fallback text window before node
|
|
113
|
-
*/
|
|
114
|
-
function fallbackTextBeforeHasReq(sourceCode, node) {
|
|
115
|
-
// Guard against unsupported sourceCode or nodes without a usable range.
|
|
116
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
117
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Ensure we only inspect text when range information is available
|
|
118
|
-
if (typeof sourceCode?.getText !== "function" ||
|
|
119
|
-
!Array.isArray((node && node.range) || [])) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
const range = node.range;
|
|
123
|
-
// Guard when the node range cannot provide a numeric start index.
|
|
124
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
125
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Avoid scanning when range start is not a number
|
|
126
|
-
if (!Array.isArray(range) || typeof range[0] !== "number") {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
|
|
131
|
-
const textBefore = sourceCode.getText().slice(start, range[0]);
|
|
132
|
-
// Detect @req in the bounded text window immediately preceding the node.
|
|
133
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
134
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in fallback text window
|
|
135
|
-
if (typeof textBefore === "string" && textBefore.includes("@req")) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Swallow detection errors to avoid breaking lint runs due to malformed source.
|
|
141
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
142
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Treat IO/detection failures as "no annotation" instead of throwing
|
|
143
|
-
/* noop */
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
|
|
149
|
-
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
150
|
-
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
151
|
-
*/
|
|
152
|
-
function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
153
|
-
try {
|
|
154
|
-
const sourceCode = context && typeof context.getSourceCode === "function"
|
|
155
|
-
? context.getSourceCode()
|
|
156
|
-
: undefined;
|
|
157
|
-
// Prefer robust, location-based heuristics when sourceCode and node are available.
|
|
158
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
159
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Use multiple heuristics to detect @req markers around the node
|
|
160
|
-
if (sourceCode && node) {
|
|
161
|
-
if (linesBeforeHasReq(sourceCode, node) ||
|
|
162
|
-
parentChainHasReq(sourceCode, node) ||
|
|
163
|
-
fallbackTextBeforeHasReq(sourceCode, node)) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
// Swallow detection errors and fall through to simple checks.
|
|
170
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
171
|
-
// @req REQ-ANNOTATION-REQ-DETECTION - Fail gracefully when advanced detection heuristics throw
|
|
172
|
-
}
|
|
173
|
-
// BRANCH @req detection on JSDoc or comments
|
|
174
|
-
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
175
|
-
// @req REQ-ANNOTATION-REQ-DETECTION
|
|
176
|
-
return ((jsdoc &&
|
|
177
|
-
typeof jsdoc.value === "string" &&
|
|
178
|
-
jsdoc.value.includes("@req")) ||
|
|
179
|
-
comments.some(commentContainsReq));
|
|
180
|
-
}
|
|
181
38
|
/**
|
|
182
39
|
* Determine the most appropriate node to attach an inserted JSDoc to.
|
|
183
40
|
* Prefers outer function-like constructs such as methods, variable declarators,
|
|
@@ -283,7 +140,7 @@ function checkReqAnnotation(context, node, options) {
|
|
|
283
140
|
const leading = getLeadingComments(node);
|
|
284
141
|
const comments = getCommentsBefore(sourceCode, node);
|
|
285
142
|
const all = combineComments(leading, comments);
|
|
286
|
-
const hasReq = hasReqAnnotation(jsdoc, all, context, node);
|
|
143
|
+
const hasReq = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, all, context, node);
|
|
287
144
|
// BRANCH when a @req annotation is missing and must be reported
|
|
288
145
|
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
289
146
|
// @req REQ-ANNOTATION-REQ-DETECTION
|
|
@@ -187,12 +187,11 @@ function reportMissingReq(context, node, options) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
/**
|
|
190
|
-
*
|
|
190
|
+
* Compute annotation-related metadata for a branch node.
|
|
191
191
|
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
192
192
|
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
193
193
|
*/
|
|
194
|
-
function
|
|
195
|
-
const sourceCode = context.getSourceCode();
|
|
194
|
+
function getBranchAnnotationInfo(sourceCode, node) {
|
|
196
195
|
const text = gatherBranchCommentText(sourceCode, node);
|
|
197
196
|
const missingStory = !/@story\b/.test(text);
|
|
198
197
|
const missingReq = !/@req\b/.test(text);
|
|
@@ -201,6 +200,16 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
|
201
200
|
line: node.loc.start.line,
|
|
202
201
|
column: 0,
|
|
203
202
|
});
|
|
203
|
+
return { missingStory, missingReq, indent, insertPos };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Report missing annotations on a branch node.
|
|
207
|
+
* @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
208
|
+
* @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
|
|
209
|
+
*/
|
|
210
|
+
function reportMissingAnnotations(context, node, storyFixCountRef) {
|
|
211
|
+
const sourceCode = context.getSourceCode();
|
|
212
|
+
const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node);
|
|
204
213
|
const actions = [
|
|
205
214
|
{
|
|
206
215
|
missing: missingStory,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
|
|
3
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
|
+
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
5
|
+
*/
|
|
6
|
+
export declare function hasReqAnnotation(jsdoc: any, comments: any[], context?: any, node?: any): boolean;
|