eslint-plugin-traceability 1.6.3 → 1.6.4
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/lib/src/maintenance/detect.js +91 -21
- package/lib/src/rules/helpers/require-story-io.js +26 -2
- package/lib/src/rules/helpers/require-story-utils.d.ts +1 -1
- package/lib/src/rules/helpers/require-story-utils.js +7 -7
- package/lib/src/rules/helpers/require-story-visitors.js +11 -3
- package/lib/src/rules/require-story-annotation.js +13 -3
- package/lib/src/utils/annotation-checker.d.ts +4 -0
- package/lib/src/utils/annotation-checker.js +64 -4
- package/lib/src/utils/storyReferenceUtils.js +30 -1
- package/package.json +1 -1
|
@@ -37,37 +37,107 @@ exports.detectStaleAnnotations = detectStaleAnnotations;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const utils_1 = require("./utils");
|
|
40
|
+
const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
|
|
40
41
|
/**
|
|
41
42
|
* Detect stale annotation references that point to moved or deleted story files
|
|
42
43
|
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
43
44
|
* @req REQ-MAINT-DETECT - Detect stale annotation references
|
|
44
45
|
*/
|
|
45
46
|
function detectStaleAnnotations(codebasePath) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
49
|
+
// @req REQ-MAINT-DETECT - Treat codebasePath as a workspace root resolved from process.cwd()
|
|
50
|
+
const workspaceRoot = path.resolve(cwd, codebasePath);
|
|
51
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
52
|
+
// @req REQ-MAINT-DETECT - Return empty result if workspaceRoot does not exist or is not a directory
|
|
53
|
+
if (!fs.existsSync(workspaceRoot) ||
|
|
54
|
+
!fs.statSync(workspaceRoot).isDirectory()) {
|
|
49
55
|
return [];
|
|
50
56
|
}
|
|
51
|
-
const cwd = process.cwd();
|
|
52
|
-
const baseDir = path.resolve(cwd, codebasePath);
|
|
53
57
|
const stale = new Set();
|
|
54
|
-
|
|
55
|
-
// @
|
|
58
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
59
|
+
// @req REQ-MAINT-DETECT - Iterate over all files in the isolated workspace root
|
|
60
|
+
const files = (0, utils_1.getAllFiles)(workspaceRoot);
|
|
56
61
|
for (const file of files) {
|
|
57
|
-
|
|
58
|
-
const regex = /@story\s+([^\s]+)/g;
|
|
59
|
-
let match;
|
|
60
|
-
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Iterate over regex matches for @story annotations
|
|
61
|
-
while ((match = regex.exec(content)) !== null) {
|
|
62
|
-
const storyPath = match[1];
|
|
63
|
-
const storyProjectPath = path.resolve(cwd, storyPath);
|
|
64
|
-
const storyCodebasePath = path.resolve(baseDir, storyPath);
|
|
65
|
-
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Check if referenced story file is missing in both project and codebase paths
|
|
66
|
-
if (!fs.existsSync(storyProjectPath) &&
|
|
67
|
-
!fs.existsSync(storyCodebasePath)) {
|
|
68
|
-
stale.add(storyPath);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
62
|
+
processFileForStaleAnnotations(file, workspaceRoot, cwd, stale);
|
|
71
63
|
}
|
|
72
64
|
return Array.from(stale);
|
|
73
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
68
|
+
* @req REQ-MAINT-DETECT - Process individual files to detect stale @story annotations
|
|
69
|
+
*/
|
|
70
|
+
function processFileForStaleAnnotations(file, workspaceRoot, cwd, stale) {
|
|
71
|
+
let content;
|
|
72
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
73
|
+
// @req REQ-MAINT-DETECT - Handle file read errors gracefully
|
|
74
|
+
try {
|
|
75
|
+
content = fs.readFileSync(file, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const regex = /@story\s+([^\s]+)/g;
|
|
81
|
+
let match;
|
|
82
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
83
|
+
// @req REQ-MAINT-DETECT - Iterate over regex matches for @story annotations
|
|
84
|
+
while ((match = regex.exec(content)) !== null) {
|
|
85
|
+
handleStoryMatch(match[1], workspaceRoot, cwd, stale);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
90
|
+
* @req REQ-MAINT-DETECT - Handle individual @story matches within a file
|
|
91
|
+
*/
|
|
92
|
+
function handleStoryMatch(storyPath, workspaceRoot, cwd, stale) {
|
|
93
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
94
|
+
// @req REQ-MAINT-DETECT - Skip traversal/absolute-unsafe story paths before any filesystem or boundary checks
|
|
95
|
+
if ((0, storyReferenceUtils_1.isTraversalUnsafe)(storyPath)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
99
|
+
// @req REQ-MAINT-DETECT - Compute project and codebase candidates relative to cwd and workspaceRoot
|
|
100
|
+
const storyProjectCandidate = path.resolve(cwd, storyPath);
|
|
101
|
+
const storyCodebaseCandidate = path.resolve(workspaceRoot, storyPath);
|
|
102
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
103
|
+
// @req REQ-MAINT-DETECT - Enforce workspaceRoot as the project boundary for resolved story paths
|
|
104
|
+
let projectBoundary;
|
|
105
|
+
let codebaseBoundary;
|
|
106
|
+
try {
|
|
107
|
+
projectBoundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(storyProjectCandidate, workspaceRoot);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
projectBoundary = {
|
|
111
|
+
isWithinProject: false,
|
|
112
|
+
candidate: storyProjectCandidate,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
codebaseBoundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(storyCodebaseCandidate, workspaceRoot);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
codebaseBoundary = {
|
|
120
|
+
isWithinProject: false,
|
|
121
|
+
candidate: storyCodebaseCandidate,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const inProjectCandidates = [];
|
|
125
|
+
if (projectBoundary.isWithinProject) {
|
|
126
|
+
inProjectCandidates.push(projectBoundary.candidate);
|
|
127
|
+
}
|
|
128
|
+
if (codebaseBoundary.isWithinProject) {
|
|
129
|
+
inProjectCandidates.push(codebaseBoundary.candidate);
|
|
130
|
+
}
|
|
131
|
+
// If both candidates are out-of-project, do not mark as stale and skip FS checks
|
|
132
|
+
if (inProjectCandidates.length === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
136
|
+
// @req REQ-MAINT-DETECT - Only check existence for in-project candidates
|
|
137
|
+
const anyExists = inProjectCandidates.some((p) => fs.existsSync(p));
|
|
138
|
+
// @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
139
|
+
// @req REQ-MAINT-DETECT - Mark story as stale if any in-project candidate exists conceptually but none exist on disk
|
|
140
|
+
if (!anyExists) {
|
|
141
|
+
stale.add(storyPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -31,13 +31,22 @@ function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES
|
|
|
31
31
|
const startLine = node && node.loc && typeof node.loc.start?.line === "number"
|
|
32
32
|
? node.loc.start.line
|
|
33
33
|
: null;
|
|
34
|
+
// Guard against missing or non-array source lines or an invalid start line before scanning.
|
|
35
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
36
|
+
// @req REQ-ANNOTATION-REQUIRED - Fail gracefully when source lines or locations are unavailable
|
|
34
37
|
if (!Array.isArray(lines) || typeof startLine !== "number") {
|
|
35
38
|
return false;
|
|
36
39
|
}
|
|
37
40
|
const from = Math.max(0, startLine - 1 - lookback);
|
|
38
41
|
const to = Math.max(0, startLine - 1);
|
|
42
|
+
// Walk each physical line in the configured lookback window to search for an inline @story marker.
|
|
43
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
44
|
+
// @req REQ-ANNOTATION-REQUIRED - Scan preceding lines for existing story annotations
|
|
39
45
|
for (let i = from; i < to; i++) {
|
|
40
46
|
const text = lines[i];
|
|
47
|
+
// Treat any line containing "@story" as evidence that the function is already annotated.
|
|
48
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
49
|
+
// @req REQ-ANNOTATION-REQUIRED - Detect explicit @story markers in raw source text
|
|
41
50
|
if (typeof text === "string" && text.includes("@story")) {
|
|
42
51
|
return true;
|
|
43
52
|
}
|
|
@@ -84,24 +93,39 @@ function parentChainHasStory(sourceCode, node) {
|
|
|
84
93
|
* @req REQ-ANNOTATION-REQUIRED - Provide fallback textual inspection when other heuristics fail
|
|
85
94
|
*/
|
|
86
95
|
function fallbackTextBeforeHasStory(sourceCode, node) {
|
|
96
|
+
// Skip fallback text inspection when the sourceCode API or node range information is not available.
|
|
97
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
98
|
+
// @req REQ-ANNOTATION-REQUIRED - Avoid throwing when source text or range metadata cannot be read
|
|
87
99
|
if (typeof sourceCode?.getText !== "function" ||
|
|
88
100
|
!Array.isArray((node && node.range) || [])) {
|
|
89
101
|
return false;
|
|
90
102
|
}
|
|
91
103
|
const range = node.range;
|
|
104
|
+
// Guard against malformed range values that cannot provide a numeric start index for slicing.
|
|
105
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
106
|
+
// @req REQ-ANNOTATION-REQUIRED - Validate node range structure before computing fallback window
|
|
92
107
|
if (!Array.isArray(range) || typeof range[0] !== "number") {
|
|
93
108
|
return false;
|
|
94
109
|
}
|
|
95
110
|
try {
|
|
96
|
-
// Limit the fallback
|
|
111
|
+
// Limit the fallback inspection window to a bounded region immediately preceding the node.
|
|
112
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
113
|
+
// @req REQ-ANNOTATION-REQUIRED - Restrict fallback text scanning to a safe, fixed-size window
|
|
97
114
|
const start = Math.max(0, range[0] - exports.FALLBACK_WINDOW);
|
|
98
115
|
const textBefore = sourceCode.getText().slice(start, range[0]);
|
|
116
|
+
// Detect any @story marker that appears within the bounded fallback window.
|
|
117
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
118
|
+
// @req REQ-ANNOTATION-REQUIRED - Recognize story annotations discovered via fallback text scanning
|
|
99
119
|
if (typeof textBefore === "string" && textBefore.includes("@story")) {
|
|
100
120
|
return true;
|
|
101
121
|
}
|
|
102
122
|
}
|
|
103
123
|
catch {
|
|
104
|
-
/*
|
|
124
|
+
/*
|
|
125
|
+
* Swallow low-level IO or slicing errors so annotation detection never breaks lint execution.
|
|
126
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
127
|
+
* @req REQ-ANNOTATION-REQUIRED - Treat fallback text inspection failures as "no annotation" instead of raising
|
|
128
|
+
*/
|
|
105
129
|
}
|
|
106
130
|
return false;
|
|
107
131
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* Get a readable name for a given AST node.
|
|
27
27
|
*
|
|
28
28
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
29
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
29
|
+
* @req REQ-ANNOTATION-REQUIRED - Provide a unified way to obtain a stable, human-readable name from AST nodes
|
|
30
30
|
*
|
|
31
31
|
* @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
|
|
32
32
|
* @returns The resolved name string, or null if a stable name cannot be determined.
|
|
@@ -29,7 +29,7 @@ exports.getNodeName = getNodeName;
|
|
|
29
29
|
* Check for identifier-like nodes and return their name when available.
|
|
30
30
|
*
|
|
31
31
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
32
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
32
|
+
* @req REQ-ANNOTATION-REQUIRED - Recognize Identifier/JSXIdentifier nodes and return their name
|
|
33
33
|
*
|
|
34
34
|
* @param node - AST node to inspect
|
|
35
35
|
* @returns the identifier name or null
|
|
@@ -47,7 +47,7 @@ function isIdentifierLike(node) {
|
|
|
47
47
|
* Convert a Literal node to a string when it represents a stable primitive (string/number/boolean).
|
|
48
48
|
*
|
|
49
49
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
50
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
50
|
+
* @req REQ-ANNOTATION-REQUIRED - Convert simple Literal nodes into stable string names when possible
|
|
51
51
|
*
|
|
52
52
|
* @param node - AST node expected to be a Literal
|
|
53
53
|
* @returns the literal as string or null if not stable/resolvable
|
|
@@ -67,7 +67,7 @@ function literalToString(node) {
|
|
|
67
67
|
* Convert a TemplateLiteral node to a string if it contains no expressions.
|
|
68
68
|
*
|
|
69
69
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
70
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
70
|
+
* @req REQ-ANNOTATION-REQUIRED - Support simple, expression-free TemplateLiteral names for reporting
|
|
71
71
|
*
|
|
72
72
|
* @param node - AST node expected to be a TemplateLiteral
|
|
73
73
|
* @returns the cooked/raw concatenated template string or null if it contains expressions
|
|
@@ -95,7 +95,7 @@ function templateLiteralToString(node) {
|
|
|
95
95
|
* Resolve a MemberExpression / TSQualifiedName / JSXMemberExpression-like node to a name when non-computed.
|
|
96
96
|
*
|
|
97
97
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
98
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
98
|
+
* @req REQ-ANNOTATION-REQUIRED - Resolve non-computed member-like nodes into property names when safe
|
|
99
99
|
*
|
|
100
100
|
* @param node - AST node to inspect
|
|
101
101
|
* @returns resolved member/property name or null
|
|
@@ -121,7 +121,7 @@ function memberExpressionName(node) {
|
|
|
121
121
|
* Extract the key name from Property/ObjectProperty nodes.
|
|
122
122
|
*
|
|
123
123
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
124
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
124
|
+
* @req REQ-ANNOTATION-REQUIRED - Extract key names from Property/ObjectProperty nodes used in function containers
|
|
125
125
|
*
|
|
126
126
|
* @param node - AST node expected to be Property/ObjectProperty
|
|
127
127
|
* @returns the resolved key name or null
|
|
@@ -141,7 +141,7 @@ function propertyKeyName(node) {
|
|
|
141
141
|
* Branch-level traceability: prefer direct .key.name early (common on variable declarators, properties)
|
|
142
142
|
*
|
|
143
143
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
144
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
144
|
+
* @req REQ-ANNOTATION-REQUIRED - Prefer direct id/key names before falling back to deeper AST inspection
|
|
145
145
|
*
|
|
146
146
|
* @param node - AST node to inspect for direct .id/.key name
|
|
147
147
|
* @returns the resolved direct name or null
|
|
@@ -173,7 +173,7 @@ function directName(node) {
|
|
|
173
173
|
* Get a readable name for a given AST node.
|
|
174
174
|
*
|
|
175
175
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
176
|
-
* @req REQ-ANNOTATION-REQUIRED
|
|
176
|
+
* @req REQ-ANNOTATION-REQUIRED - Provide a unified way to obtain a stable, human-readable name from AST nodes
|
|
177
177
|
*
|
|
178
178
|
* @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
|
|
179
179
|
* @returns The resolved name string, or null if a stable name cannot be determined.
|
|
@@ -13,6 +13,12 @@ const require_story_helpers_1 = require("./require-story-helpers");
|
|
|
13
13
|
* @req REQ-BUILD-VISITORS-FNDECL - Provide visitor for FunctionDeclaration
|
|
14
14
|
*/
|
|
15
15
|
function buildFunctionDeclarationVisitor(context, sourceCode, options) {
|
|
16
|
+
/**
|
|
17
|
+
* Debug flag for optional visitor logging.
|
|
18
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
19
|
+
* @req REQ-DEBUG-LOG-TOGGLE - Allow opt-in debug logging via TRACEABILITY_DEBUG
|
|
20
|
+
*/
|
|
21
|
+
const debugEnabled = process.env.TRACEABILITY_DEBUG === "1";
|
|
16
22
|
/**
|
|
17
23
|
* Handle FunctionDeclaration nodes.
|
|
18
24
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -24,9 +30,11 @@ function buildFunctionDeclarationVisitor(context, sourceCode, options) {
|
|
|
24
30
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
25
31
|
* @req REQ-DEBUG-LOG - Provide debug logging for visitor entry
|
|
26
32
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
if (debugEnabled) {
|
|
34
|
+
console.debug("require-story-annotation:FunctionDeclaration", typeof context.getFilename === "function"
|
|
35
|
+
? context.getFilename()
|
|
36
|
+
: "<unknown>", node && node.id ? node.id.name : "<anonymous>");
|
|
37
|
+
}
|
|
30
38
|
if (!options.shouldProcessNode(node))
|
|
31
39
|
return;
|
|
32
40
|
const target = (0, require_story_helpers_1.resolveTargetNode)(sourceCode, node);
|
|
@@ -63,15 +63,25 @@ const rule = {
|
|
|
63
63
|
const opts = (context.options && context.options[0]) || {};
|
|
64
64
|
const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
|
|
65
65
|
const exportPriority = opts.exportPriority || "all";
|
|
66
|
+
/**
|
|
67
|
+
* Environment-gated debug logging to avoid leaking file paths unless
|
|
68
|
+
* explicitly enabled.
|
|
69
|
+
*
|
|
70
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
71
|
+
* @req REQ-DEBUG-LOG
|
|
72
|
+
*/
|
|
73
|
+
const debugEnabled = process.env.TRACEABILITY_DEBUG === "1";
|
|
66
74
|
/**
|
|
67
75
|
* Debug log at the start of create to help diagnose rule activation in tests.
|
|
68
76
|
*
|
|
69
77
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
70
78
|
* @req REQ-DEBUG-LOG
|
|
71
79
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
if (debugEnabled) {
|
|
81
|
+
console.debug("require-story-annotation:create", typeof context.getFilename === "function"
|
|
82
|
+
? context.getFilename()
|
|
83
|
+
: "<unknown>");
|
|
84
|
+
}
|
|
75
85
|
// Local closure that binds configured scope and export priority to the helper.
|
|
76
86
|
const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority);
|
|
77
87
|
// Delegate visitor construction to helper to keep this file concise.
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
|
|
7
7
|
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
8
8
|
* @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
|
|
9
|
+
* @param context - ESLint rule context used to obtain source and report problems
|
|
10
|
+
* @param node - Function-like AST node whose surrounding comments should be inspected
|
|
11
|
+
* @param options - Optional configuration controlling behaviour (e.g., enableFix)
|
|
12
|
+
* @returns void
|
|
9
13
|
*/
|
|
10
14
|
export declare function checkReqAnnotation(context: any, node: any, options?: {
|
|
11
15
|
enableFix?: boolean;
|
|
@@ -45,19 +45,30 @@ function commentContainsReq(c) {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
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
|
|
48
50
|
*/
|
|
49
51
|
function linesBeforeHasReq(sourceCode, node) {
|
|
50
52
|
const lines = sourceCode && sourceCode.lines;
|
|
51
53
|
const startLine = node && node.loc && typeof node.loc.start?.line === "number"
|
|
52
54
|
? node.loc.start.line
|
|
53
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
|
|
54
59
|
if (!Array.isArray(lines) || typeof startLine !== "number") {
|
|
55
60
|
return false;
|
|
56
61
|
}
|
|
57
62
|
const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
|
|
58
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
|
|
59
67
|
for (let i = from; i < to; i++) {
|
|
60
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
|
|
61
72
|
if (typeof text === "string" && text.includes("@req")) {
|
|
62
73
|
return true;
|
|
63
74
|
}
|
|
@@ -66,20 +77,29 @@ function linesBeforeHasReq(sourceCode, node) {
|
|
|
66
77
|
}
|
|
67
78
|
/**
|
|
68
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
|
|
69
82
|
*/
|
|
70
83
|
function parentChainHasReq(sourceCode, node) {
|
|
71
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
|
|
72
88
|
while (p) {
|
|
73
89
|
const pComments = typeof sourceCode?.getCommentsBefore === "function"
|
|
74
90
|
? sourceCode.getCommentsBefore(p) || []
|
|
75
91
|
: [];
|
|
76
|
-
|
|
77
|
-
|
|
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)) {
|
|
78
96
|
return true;
|
|
79
97
|
}
|
|
80
98
|
const pLeading = p.leadingComments || [];
|
|
81
|
-
|
|
82
|
-
|
|
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)) {
|
|
83
103
|
return true;
|
|
84
104
|
}
|
|
85
105
|
p = p.parent;
|
|
@@ -88,24 +108,38 @@ function parentChainHasReq(sourceCode, node) {
|
|
|
88
108
|
}
|
|
89
109
|
/**
|
|
90
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
|
|
91
113
|
*/
|
|
92
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
|
|
93
118
|
if (typeof sourceCode?.getText !== "function" ||
|
|
94
119
|
!Array.isArray((node && node.range) || [])) {
|
|
95
120
|
return false;
|
|
96
121
|
}
|
|
97
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
|
|
98
126
|
if (!Array.isArray(range) || typeof range[0] !== "number") {
|
|
99
127
|
return false;
|
|
100
128
|
}
|
|
101
129
|
try {
|
|
102
130
|
const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
|
|
103
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
|
|
104
135
|
if (typeof textBefore === "string" && textBefore.includes("@req")) {
|
|
105
136
|
return true;
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
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
|
|
109
143
|
/* noop */
|
|
110
144
|
}
|
|
111
145
|
return false;
|
|
@@ -120,6 +154,9 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
|
120
154
|
const sourceCode = context && typeof context.getSourceCode === "function"
|
|
121
155
|
? context.getSourceCode()
|
|
122
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
|
|
123
160
|
if (sourceCode && node) {
|
|
124
161
|
if (linesBeforeHasReq(sourceCode, node) ||
|
|
125
162
|
parentChainHasReq(sourceCode, node) ||
|
|
@@ -130,6 +167,8 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
|
130
167
|
}
|
|
131
168
|
catch {
|
|
132
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
|
|
133
172
|
}
|
|
134
173
|
// BRANCH @req detection on JSDoc or comments
|
|
135
174
|
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
@@ -148,19 +187,28 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
|
|
|
148
187
|
*/
|
|
149
188
|
function getFixTargetNode(node) {
|
|
150
189
|
const parent = node && node.parent;
|
|
190
|
+
// When there is no parent, attach the annotation directly to the node itself.
|
|
191
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
192
|
+
// @req REQ-ANNOTATION-AUTOFIX - Default to annotating the node when it has no parent
|
|
151
193
|
if (!parent) {
|
|
152
194
|
return node;
|
|
153
195
|
}
|
|
154
196
|
// If the node is part of a class/obj method definition, attach to the MethodDefinition
|
|
197
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
198
|
+
// @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the MethodDefinition wrapper for methods
|
|
155
199
|
if (parent.type === "MethodDefinition") {
|
|
156
200
|
return parent;
|
|
157
201
|
}
|
|
158
202
|
// If the node is the init of a variable declarator, attach to the VariableDeclarator
|
|
203
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
204
|
+
// @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the VariableDeclarator for function initializers
|
|
159
205
|
if (parent.type === "VariableDeclarator" && parent.init === node) {
|
|
160
206
|
return parent;
|
|
161
207
|
}
|
|
162
208
|
// If the parent is an expression statement (e.g. IIFE or assigned via expression),
|
|
163
209
|
// attach to the outer ExpressionStatement.
|
|
210
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
211
|
+
// @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the ExpressionStatement wrapper for IIFEs
|
|
164
212
|
if (parent.type === "ExpressionStatement") {
|
|
165
213
|
return parent;
|
|
166
214
|
}
|
|
@@ -174,6 +222,11 @@ function getFixTargetNode(node) {
|
|
|
174
222
|
*/
|
|
175
223
|
function createMissingReqFix(node) {
|
|
176
224
|
const target = getFixTargetNode(node);
|
|
225
|
+
/**
|
|
226
|
+
* Fixer used to insert a default @req annotation before the chosen target node.
|
|
227
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
228
|
+
* @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
|
|
229
|
+
*/
|
|
177
230
|
return function missingReqFix(fixer) {
|
|
178
231
|
return fixer.insertTextBefore(target, "/** @req <REQ-ID> */\n");
|
|
179
232
|
};
|
|
@@ -202,6 +255,9 @@ function reportMissing(context, node, enableFix = true) {
|
|
|
202
255
|
messageId: "missingReq",
|
|
203
256
|
data: { name, functionName: name },
|
|
204
257
|
};
|
|
258
|
+
// Conditionally attach an autofix only when enabled in the rule options.
|
|
259
|
+
// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
260
|
+
// @req REQ-ANNOTATION-AUTOFIX - Only provide autofix suggestions when explicitly enabled
|
|
205
261
|
if (enableFix) {
|
|
206
262
|
reportOptions.fix = createMissingReqFix(node);
|
|
207
263
|
}
|
|
@@ -215,6 +271,10 @@ function reportMissing(context, node, enableFix = true) {
|
|
|
215
271
|
* @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
|
|
216
272
|
* @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
|
|
217
273
|
* @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
|
|
274
|
+
* @param context - ESLint rule context used to obtain source and report problems
|
|
275
|
+
* @param node - Function-like AST node whose surrounding comments should be inspected
|
|
276
|
+
* @param options - Optional configuration controlling behaviour (e.g., enableFix)
|
|
277
|
+
* @returns void
|
|
218
278
|
*/
|
|
219
279
|
function checkReqAnnotation(context, node, options) {
|
|
220
280
|
const { enableFix = true } = options ?? {};
|
|
@@ -61,11 +61,20 @@ function __resetStoryExistenceCacheForTests() {
|
|
|
61
61
|
*/
|
|
62
62
|
function buildStoryCandidates(storyPath, cwd, storyDirs) {
|
|
63
63
|
const candidates = [];
|
|
64
|
+
// When the story path is already explicitly relative, resolve it only against the current working directory.
|
|
65
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
66
|
+
// @req REQ-PATH-RESOLUTION - Preserve explicit relative story path semantics when building candidate locations
|
|
64
67
|
if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
|
|
65
68
|
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
66
69
|
}
|
|
67
70
|
else {
|
|
71
|
+
// For bare paths, first try resolving directly under the current working directory.
|
|
72
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
73
|
+
// @req REQ-PATH-RESOLUTION - Attempt direct resolution from cwd before probing configured story directories
|
|
68
74
|
candidates.push(path_1.default.resolve(cwd, storyPath));
|
|
75
|
+
// Probe each configured story directory for a matching story filename.
|
|
76
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
77
|
+
// @req REQ-PATH-RESOLUTION - Expand search across configured storyDirectories while staying within project
|
|
69
78
|
for (const dir of storyDirs) {
|
|
70
79
|
candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
|
|
71
80
|
}
|
|
@@ -91,17 +100,26 @@ const fileExistStatusCache = new Map();
|
|
|
91
100
|
*/
|
|
92
101
|
function checkSingleCandidate(candidate) {
|
|
93
102
|
const cached = fileExistStatusCache.get(candidate);
|
|
103
|
+
// Reuse any cached filesystem result to avoid redundant disk IO for the same candidate.
|
|
104
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
105
|
+
// @req REQ-PERFORMANCE-OPTIMIZATION - Short-circuit on cached existence checks
|
|
94
106
|
if (cached) {
|
|
95
107
|
return cached;
|
|
96
108
|
}
|
|
97
109
|
let result;
|
|
98
110
|
try {
|
|
99
111
|
const exists = fs_1.default.existsSync(candidate);
|
|
112
|
+
// When the path does not exist at all, record a simple "missing" status.
|
|
113
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
114
|
+
// @req REQ-FILE-EXISTENCE - Distinguish non-existent story paths from other failure modes
|
|
100
115
|
if (!exists) {
|
|
101
116
|
result = { path: candidate, status: "missing" };
|
|
102
117
|
}
|
|
103
118
|
else {
|
|
104
119
|
const stat = fs_1.default.statSync(candidate);
|
|
120
|
+
// Treat existing regular files as valid story candidates; other entry types are considered missing.
|
|
121
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
122
|
+
// @req REQ-FILE-EXISTENCE - Only regular files may satisfy a story path reference
|
|
105
123
|
if (stat.isFile()) {
|
|
106
124
|
result = { path: candidate, status: "exists" };
|
|
107
125
|
}
|
|
@@ -112,7 +130,9 @@ function checkSingleCandidate(candidate) {
|
|
|
112
130
|
}
|
|
113
131
|
}
|
|
114
132
|
catch (error) {
|
|
115
|
-
// Any filesystem error is captured and surfaced as fs-error.
|
|
133
|
+
// Any filesystem error is captured and surfaced as an fs-error status instead of throwing.
|
|
134
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
135
|
+
// @req REQ-ERROR-HANDLING - Represent filesystem failures as fs-error results while keeping callers resilient
|
|
116
136
|
result = { path: candidate, status: "fs-error", error };
|
|
117
137
|
}
|
|
118
138
|
fileExistStatusCache.set(candidate, result);
|
|
@@ -136,6 +156,9 @@ function getStoryExistence(candidates) {
|
|
|
136
156
|
let firstFsError;
|
|
137
157
|
for (const candidate of candidates) {
|
|
138
158
|
const res = checkSingleCandidate(candidate);
|
|
159
|
+
// As soon as a candidate file is confirmed to exist, return a successful existence result.
|
|
160
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
161
|
+
// @req REQ-FILE-EXISTENCE - Prefer the first positively-matched story file
|
|
139
162
|
if (res.status === "exists") {
|
|
140
163
|
return {
|
|
141
164
|
candidates,
|
|
@@ -143,10 +166,16 @@ function getStoryExistence(candidates) {
|
|
|
143
166
|
matchedPath: res.path,
|
|
144
167
|
};
|
|
145
168
|
}
|
|
169
|
+
// Remember the first filesystem error so callers can inspect a representative failure if no files exist.
|
|
170
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
171
|
+
// @req REQ-ERROR-HANDLING - Surface a single representative filesystem error without failing fast
|
|
146
172
|
if (res.status === "fs-error" && !firstFsError) {
|
|
147
173
|
firstFsError = res;
|
|
148
174
|
}
|
|
149
175
|
}
|
|
176
|
+
// Prefer reporting a filesystem error over a generic missing status when at least one candidate failed to read.
|
|
177
|
+
// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
178
|
+
// @req REQ-ERROR-HANDLING - Distinguish IO failures from simple "missing" results in existence checks
|
|
150
179
|
if (firstFsError) {
|
|
151
180
|
return {
|
|
152
181
|
candidates,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
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",
|