eslint-plugin-traceability 1.12.0 → 1.12.1
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/CHANGELOG.md +3 -3
- package/lib/src/rules/helpers/require-story-core.js +61 -52
- package/lib/src/utils/branch-annotation-helpers.js +71 -29
- package/lib/tests/integration/dogfooding-validation.test.js +33 -1
- package/lib/tests/integration/else-if-annotation-prettier.integration.test.js +18 -79
- package/lib/tests/rules/auto-fix-behavior-008.test.js +87 -1
- package/lib/tests/rules/require-story-core.autofix.test.js +26 -0
- package/lib/tests/utils/branch-annotation-else-if-position.test.js +38 -0
- package/lib/tests/utils/req-annotation-detection.test.js +46 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
## [1.12.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.12.0...v1.12.1) (2025-12-07)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* support single-line else-if annotations and enable Prettier tests ([967b7e0](https://github.com/voder-ai/eslint-plugin-traceability/commit/967b7e02e12c4415efa0df2772ccde77b94cd1a8))
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
@@ -98,6 +98,46 @@ exports.STORY_PATH = "docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md";
|
|
|
98
98
|
* Allowed values for export priority option.
|
|
99
99
|
*/
|
|
100
100
|
exports.EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
|
|
101
|
+
/**
|
|
102
|
+
* Safely execute a reporting operation, swallowing unexpected errors so that
|
|
103
|
+
* traceability rules never break ESLint runs. When TRACEABILITY_DEBUG=1 is
|
|
104
|
+
* set in the environment, a diagnostic message is logged to stderr.
|
|
105
|
+
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-RESILIENCE
|
|
106
|
+
*/
|
|
107
|
+
function withSafeReporting(label, fn) {
|
|
108
|
+
try {
|
|
109
|
+
fn();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
113
|
+
// Debug logging only when explicitly enabled for troubleshooting helper failures.
|
|
114
|
+
console.error(`[traceability] ${label} failed`, error?.message ?? error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the shared ESLint report descriptor for a missing @story annotation.
|
|
120
|
+
* This keeps the core helpers focused on computing names, targets, and
|
|
121
|
+
* templates while centralizing the diagnostic wiring.
|
|
122
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ERROR-SPECIFIC
|
|
123
|
+
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-RESILIENCE
|
|
124
|
+
*/
|
|
125
|
+
function createMissingStoryReportDescriptor(config) {
|
|
126
|
+
const { nameNode, name, resolvedTarget, effectiveTemplate, allowFix, createFix, } = config;
|
|
127
|
+
const baseFix = createFix(resolvedTarget, effectiveTemplate);
|
|
128
|
+
return {
|
|
129
|
+
node: nameNode,
|
|
130
|
+
messageId: "missingStory",
|
|
131
|
+
data: { name, functionName: name },
|
|
132
|
+
fix: allowFix ? baseFix : undefined,
|
|
133
|
+
suggest: [
|
|
134
|
+
{
|
|
135
|
+
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
|
|
136
|
+
fix: baseFix,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
101
141
|
/**
|
|
102
142
|
* Core helper to report a missing @story annotation for a function-like node.
|
|
103
143
|
* This reporting utility delegates behavior to injected dependencies so that
|
|
@@ -111,7 +151,7 @@ exports.EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
|
|
|
111
151
|
*/
|
|
112
152
|
function coreReportMissing(deps, context, sourceCode, config) {
|
|
113
153
|
const { node, target: passedTarget, options = {} } = config;
|
|
114
|
-
|
|
154
|
+
withSafeReporting("coreReportMissing", () => {
|
|
115
155
|
if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
116
156
|
return;
|
|
117
157
|
}
|
|
@@ -120,30 +160,15 @@ function coreReportMissing(deps, context, sourceCode, config) {
|
|
|
120
160
|
const nameNode = deps.getNameNodeForReport(node);
|
|
121
161
|
const { effectiveTemplate, allowFix } = deps.buildTemplateConfig(options);
|
|
122
162
|
const name = functionName;
|
|
123
|
-
context.report({
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
|
|
133
|
-
fix: deps.createAddStoryFix(resolvedTarget, effectiveTemplate),
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
// Intentionally swallow unexpected helper errors so traceability checks never
|
|
140
|
-
// break lint runs. When TRACEABILITY_DEBUG=1 is set, log a debug message to
|
|
141
|
-
// help diagnose misbehaving helpers in local development without affecting
|
|
142
|
-
// normal CI or production usage.
|
|
143
|
-
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
144
|
-
console.error("[traceability] coreReportMissing failed for node", error?.message ?? error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
163
|
+
context.report(createMissingStoryReportDescriptor({
|
|
164
|
+
nameNode,
|
|
165
|
+
name,
|
|
166
|
+
resolvedTarget,
|
|
167
|
+
effectiveTemplate,
|
|
168
|
+
allowFix,
|
|
169
|
+
createFix: deps.createAddStoryFix,
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
147
172
|
}
|
|
148
173
|
/**
|
|
149
174
|
* Core helper to report a missing @story annotation for a method-like node.
|
|
@@ -158,37 +183,21 @@ function coreReportMissing(deps, context, sourceCode, config) {
|
|
|
158
183
|
*/
|
|
159
184
|
function coreReportMethod(deps, context, sourceCode, config) {
|
|
160
185
|
const { node, target: passedTarget, options = {} } = config;
|
|
161
|
-
|
|
186
|
+
withSafeReporting("coreReportMethod", () => {
|
|
162
187
|
if (deps.hasStoryAnnotation(sourceCode, node)) {
|
|
163
188
|
return;
|
|
164
189
|
}
|
|
165
190
|
const resolvedTarget = passedTarget ?? deps.resolveAnnotationTargetNode(sourceCode, node, null);
|
|
166
191
|
const name = deps.extractName(node);
|
|
167
192
|
const nameNode = (node.key && node.key.type === "Identifier" && node.key) || node;
|
|
168
|
-
const effectiveTemplate = deps.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{
|
|
179
|
-
desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
|
|
180
|
-
fix: deps.createMethodFix(resolvedTarget, effectiveTemplate),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
// Intentionally swallow unexpected helper errors so traceability checks never
|
|
187
|
-
// break lint runs. When TRACEABILITY_DEBUG=1 is set, log a debug message to
|
|
188
|
-
// help diagnose misbehaving helpers in local development without affecting
|
|
189
|
-
// normal CI or production usage.
|
|
190
|
-
if (process.env.TRACEABILITY_DEBUG === "1") {
|
|
191
|
-
console.error("[traceability] coreReportMethod failed for node", error?.message ?? error);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
193
|
+
const { effectiveTemplate, allowFix } = deps.buildTemplateConfig(options);
|
|
194
|
+
context.report(createMissingStoryReportDescriptor({
|
|
195
|
+
nameNode,
|
|
196
|
+
name,
|
|
197
|
+
resolvedTarget,
|
|
198
|
+
effectiveTemplate,
|
|
199
|
+
allowFix,
|
|
200
|
+
createFix: deps.createMethodFix,
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
194
203
|
}
|
|
@@ -87,24 +87,69 @@ function extractCommentValue(_c) {
|
|
|
87
87
|
return _c.value;
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Extract trimmed comment text for a given source line index or return null
|
|
91
|
+
* when the line is blank or not a comment. This helper centralizes the
|
|
92
|
+
* formatter-aware rules used by branch helpers when scanning for contiguous
|
|
93
|
+
* comment lines around branches.
|
|
93
94
|
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
94
95
|
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
|
|
95
96
|
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
|
|
96
97
|
*/
|
|
97
|
-
function
|
|
98
|
+
function getCommentTextAtLine(lines, index) {
|
|
98
99
|
const line = lines[index];
|
|
99
100
|
if (!line || !line.trim()) {
|
|
100
|
-
return
|
|
101
|
+
return null;
|
|
101
102
|
}
|
|
102
103
|
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return line.trim();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Collect a single contiguous comment line at the given index, appending its
|
|
110
|
+
* trimmed text to the accumulator. Returns true when a valid comment was
|
|
111
|
+
* collected and false when scanning should stop (blank or non-comment line).
|
|
112
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
113
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
|
|
114
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
|
|
115
|
+
*/
|
|
116
|
+
function collectCommentLine(lines, index, comments) {
|
|
117
|
+
const commentText = getCommentTextAtLine(lines, index);
|
|
118
|
+
if (!commentText) {
|
|
103
119
|
return false;
|
|
104
120
|
}
|
|
105
|
-
comments.push(
|
|
121
|
+
comments.push(commentText);
|
|
106
122
|
return true;
|
|
107
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Scan contiguous formatter-aware comment lines between the provided 0-based
|
|
126
|
+
* start and end indices (inclusive), stopping when a non-comment or blank line
|
|
127
|
+
* is encountered. This helper is used as a line-based fallback when
|
|
128
|
+
* structured comment APIs are not available for branch bodies.
|
|
129
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
|
|
130
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC
|
|
131
|
+
* @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC-ELSE-IF
|
|
132
|
+
*/
|
|
133
|
+
function scanCommentLinesInRange(lines, startIndex, endIndexInclusive) {
|
|
134
|
+
if (!Array.isArray(lines) || lines.length === 0) {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
if (startIndex < 0 ||
|
|
138
|
+
startIndex >= lines.length ||
|
|
139
|
+
startIndex > endIndexInclusive) {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
const comments = [];
|
|
143
|
+
const lastIndex = Math.min(endIndexInclusive, lines.length - 1);
|
|
144
|
+
let i = startIndex;
|
|
145
|
+
while (i <= lastIndex) {
|
|
146
|
+
if (!collectCommentLine(lines, i, comments)) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
return comments.join(" ");
|
|
152
|
+
}
|
|
108
153
|
function isElseIfBranch(node, parent) {
|
|
109
154
|
return (node &&
|
|
110
155
|
node.type === "IfStatement" &&
|
|
@@ -139,15 +184,7 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
|
|
|
139
184
|
const lines = sourceCode.lines;
|
|
140
185
|
const startIndex = node.body.loc.start.line - 1;
|
|
141
186
|
const endIndex = node.body.loc.end.line - 1;
|
|
142
|
-
const
|
|
143
|
-
let i = startIndex + 1;
|
|
144
|
-
while (i <= endIndex) {
|
|
145
|
-
if (!collectCommentLine(lines, i, comments)) {
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
i++;
|
|
149
|
-
}
|
|
150
|
-
const insideText = comments.join(" ");
|
|
187
|
+
const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
|
|
151
188
|
if (insideText) {
|
|
152
189
|
return insideText;
|
|
153
190
|
}
|
|
@@ -165,14 +202,11 @@ function scanElseIfPrecedingComments(sourceCode, node) {
|
|
|
165
202
|
let i = startLine - 1;
|
|
166
203
|
let scanned = 0;
|
|
167
204
|
while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
if (!/^\s*(\/\/|\/\*)/.test(line)) {
|
|
205
|
+
const commentText = getCommentTextAtLine(lines, i);
|
|
206
|
+
if (!commentText) {
|
|
173
207
|
break;
|
|
174
208
|
}
|
|
175
|
-
comments.unshift(
|
|
209
|
+
comments.unshift(commentText);
|
|
176
210
|
i--;
|
|
177
211
|
scanned++;
|
|
178
212
|
}
|
|
@@ -194,13 +228,16 @@ function scanElseIfBetweenConditionAndBody(sourceCode, node) {
|
|
|
194
228
|
const lines = sourceCode.lines;
|
|
195
229
|
const conditionEndLine = node.test.loc.end.line;
|
|
196
230
|
const consequentStartLine = node.consequent.loc.start.line;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
231
|
+
// Lines in sourceCode are 0-based indexes, but loc.line values are 1-based.
|
|
232
|
+
// We want to scan comments strictly between the condition and the
|
|
233
|
+
// consequent body, so we start at the line after the condition's end and
|
|
234
|
+
// stop at the line immediately before the consequent's starting line.
|
|
235
|
+
const startIndex = conditionEndLine; // already the next logical line index when 0-based
|
|
236
|
+
const endIndexExclusive = consequentStartLine - 1;
|
|
237
|
+
if (endIndexExclusive <= startIndex) {
|
|
238
|
+
return "";
|
|
202
239
|
}
|
|
203
|
-
return
|
|
240
|
+
return scanCommentLinesInRange(lines, startIndex, endIndexExclusive - 1);
|
|
204
241
|
}
|
|
205
242
|
/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
|
|
206
243
|
function scanElseIfInsideBlockComments(sourceCode, node) {
|
|
@@ -228,7 +265,10 @@ function scanElseIfInsideBlockComments(sourceCode, node) {
|
|
|
228
265
|
* @supports REQ-FALLBACK-LOGIC
|
|
229
266
|
*/
|
|
230
267
|
function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
|
|
231
|
-
if (
|
|
268
|
+
if (beforeText &&
|
|
269
|
+
(/@story\b/.test(beforeText) ||
|
|
270
|
+
/@req\b/.test(beforeText) ||
|
|
271
|
+
/@supports\b/.test(beforeText))) {
|
|
232
272
|
return beforeText;
|
|
233
273
|
}
|
|
234
274
|
if (!isElseIfBranch(node, parent)) {
|
|
@@ -236,7 +276,9 @@ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
|
|
|
236
276
|
}
|
|
237
277
|
const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
|
|
238
278
|
if (beforeElseText &&
|
|
239
|
-
(/@story\b/.test(beforeElseText) ||
|
|
279
|
+
(/@story\b/.test(beforeElseText) ||
|
|
280
|
+
/@req\b/.test(beforeElseText) ||
|
|
281
|
+
/@supports\b/.test(beforeElseText))) {
|
|
240
282
|
return beforeElseText;
|
|
241
283
|
}
|
|
242
284
|
if (!hasValidElseIfBlockLoc(node)) {
|
|
@@ -35,10 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
/**
|
|
37
37
|
* Dogfooding validation integration tests
|
|
38
|
-
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI
|
|
38
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI REQ-DOGFOODING-VERIFY REQ-DOGFOODING-PRESET
|
|
39
39
|
*/
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const child_process_1 = require("child_process");
|
|
42
|
+
const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
|
|
43
|
+
const index_1 = __importStar(require("../../src/index"));
|
|
42
44
|
/**
|
|
43
45
|
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
|
|
44
46
|
*/
|
|
@@ -91,4 +93,34 @@ describe("Dogfooding Validation (Story 023.0-MAINT-DOGFOODING-VALIDATION)", () =
|
|
|
91
93
|
expect(result.stdout).toContain("error");
|
|
92
94
|
expect(result.stdout).toContain("src/dogfood.ts");
|
|
93
95
|
});
|
|
96
|
+
it("[REQ-DOGFOODING-VERIFY] should report at least one traceability rule active for TS sources", () => {
|
|
97
|
+
/**
|
|
98
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-VERIFY
|
|
99
|
+
*/
|
|
100
|
+
const eslintConfig = require("../../eslint.config.js");
|
|
101
|
+
const tsConfig = getTsConfigFromEslintConfig(eslintConfig);
|
|
102
|
+
expect(tsConfig).toBeDefined();
|
|
103
|
+
const rules = tsConfig.rules || {};
|
|
104
|
+
const hasTraceabilityRule = Object.keys(rules).some((key) => key.startsWith("traceability/"));
|
|
105
|
+
expect(hasTraceabilityRule).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it("[REQ-DOGFOODING-PRESET] should be compatible with recommended preset usage without throwing", async () => {
|
|
108
|
+
/**
|
|
109
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-PRESET
|
|
110
|
+
*/
|
|
111
|
+
const config = [
|
|
112
|
+
{ plugins: { traceability: index_1.default }, rules: {} },
|
|
113
|
+
...index_1.configs.recommended,
|
|
114
|
+
];
|
|
115
|
+
const eslint = new use_at_your_own_risk_1.FlatESLint({
|
|
116
|
+
overrideConfig: config,
|
|
117
|
+
overrideConfigFile: true,
|
|
118
|
+
ignore: false,
|
|
119
|
+
});
|
|
120
|
+
const results = await eslint.lintText("function foo() {}", {
|
|
121
|
+
filePath: "example.ts",
|
|
122
|
+
});
|
|
123
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
124
|
+
expect(Array.isArray(results[0].messages)).toBe(true);
|
|
125
|
+
});
|
|
94
126
|
});
|
|
@@ -50,9 +50,8 @@ describe("Else-if annotations with Prettier (Story 026.0-DEV-ELSE-IF-ANNOTATION-
|
|
|
50
50
|
}
|
|
51
51
|
return result.stdout;
|
|
52
52
|
}
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
const original = `
|
|
53
|
+
it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
|
|
54
|
+
const original = `
|
|
56
55
|
function doA() {
|
|
57
56
|
return 1;
|
|
58
57
|
}
|
|
@@ -72,16 +71,16 @@ else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
|
72
71
|
doB();
|
|
73
72
|
}
|
|
74
73
|
`;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const formatted = formatWithPrettier(original);
|
|
75
|
+
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
76
|
+
// but the exact layout and comment movement may vary between versions.
|
|
77
|
+
expect(formatted).toContain("else if");
|
|
78
|
+
expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
79
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
80
|
+
expect(result.status).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
|
|
83
|
+
const original = `
|
|
85
84
|
function doA() {
|
|
86
85
|
return 1;
|
|
87
86
|
}
|
|
@@ -102,70 +101,10 @@ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherConditio
|
|
|
102
101
|
doB();
|
|
103
102
|
}
|
|
104
103
|
`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
|
|
114
|
-
const original = `
|
|
115
|
-
function doA() {
|
|
116
|
-
return 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function doB() {
|
|
120
|
-
return 2;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
124
|
-
// @req REQ-BRANCH-DETECTION
|
|
125
|
-
if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
|
|
126
|
-
doA();
|
|
127
|
-
}
|
|
128
|
-
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
129
|
-
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
130
|
-
else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
|
|
131
|
-
doB();
|
|
132
|
-
}
|
|
133
|
-
`;
|
|
134
|
-
const formatted = formatWithPrettier(original);
|
|
135
|
-
// Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
|
|
136
|
-
// but the exact layout and comment movement may vary between versions.
|
|
137
|
-
expect(formatted).toContain("else if");
|
|
138
|
-
expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
139
|
-
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
140
|
-
expect(result.status).toBe(0);
|
|
141
|
-
});
|
|
142
|
-
it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
|
|
143
|
-
const original = `
|
|
144
|
-
function doA() {
|
|
145
|
-
return 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function doB() {
|
|
149
|
-
return 2;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
153
|
-
// @req REQ-BRANCH-DETECTION
|
|
154
|
-
if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
|
|
155
|
-
doA();
|
|
156
|
-
} else if (
|
|
157
|
-
anotherVeryLongConditionThatForcesWrapping && someOtherCondition
|
|
158
|
-
) {
|
|
159
|
-
// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
|
|
160
|
-
// @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
|
|
161
|
-
doB();
|
|
162
|
-
}
|
|
163
|
-
`;
|
|
164
|
-
const formatted = formatWithPrettier(original);
|
|
165
|
-
// Note: Prettier's exact layout of the else-if and its comments may differ between versions;
|
|
166
|
-
// the rule should accept any of the supported annotation positions regardless of formatting.
|
|
167
|
-
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
168
|
-
expect(result.status).toBe(0);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
104
|
+
const formatted = formatWithPrettier(original);
|
|
105
|
+
// Note: Prettier's exact layout of the else-if and its comments may differ between versions;
|
|
106
|
+
// the rule should accept any of the supported annotation positions regardless of formatting.
|
|
107
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
108
|
+
expect(result.status).toBe(0);
|
|
109
|
+
});
|
|
171
110
|
});
|
|
@@ -8,7 +8,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
* @story docs/stories/008.0-DEV-AUTO-FIX.story.md
|
|
9
9
|
* @req REQ-AUTOFIX-MISSING - Verify ESLint --fix automatically adds missing @story annotations to functions
|
|
10
10
|
* @req REQ-AUTOFIX-FORMAT - Verify ESLint --fix corrects simple annotation format issues for @story annotations
|
|
11
|
-
* @
|
|
11
|
+
* @req REQ-AUTOFIX-IDEMPOTENT - Verify ESLint --fix is idempotent and produces no changes on subsequent runs
|
|
12
|
+
* @req REQ-AUTOFIX-SINGLE-APPLICATION - Verify ESLint --fix does not apply the same fix multiple times or create duplicate annotations
|
|
13
|
+
* @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-MISSING REQ-AUTOFIX-FORMAT REQ-AUTOFIX-IDEMPOTENT REQ-AUTOFIX-SINGLE-APPLICATION
|
|
12
14
|
*/
|
|
13
15
|
const eslint_1 = require("eslint");
|
|
14
16
|
const require_story_annotation_1 = __importDefault(require("../../src/rules/require-story-annotation"));
|
|
@@ -196,4 +198,88 @@ describe("Auto-fix behavior (Story 008.0-DEV-AUTO-FIX)", () => {
|
|
|
196
198
|
],
|
|
197
199
|
});
|
|
198
200
|
});
|
|
201
|
+
describe("[REQ-AUTOFIX-IDEMPOTENT] and [REQ-AUTOFIX-SINGLE-APPLICATION] require-story-annotation", () => {
|
|
202
|
+
functionRuleTester.run("require-story-annotation --fix idempotent behavior", require_story_annotation_1.default, {
|
|
203
|
+
valid: [
|
|
204
|
+
{
|
|
205
|
+
name: "[REQ-AUTOFIX-IDEMPOTENT] second run on already fixed function produces no changes",
|
|
206
|
+
code: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction fixedOnce() {}`,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "[REQ-AUTOFIX-SINGLE-APPLICATION] already annotated code does not receive duplicate annotations",
|
|
210
|
+
code: `class E {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
invalid: [
|
|
214
|
+
{
|
|
215
|
+
name: "[REQ-AUTOFIX-IDEMPOTENT] first run adds annotation; subsequent run is a no-op for function declarations",
|
|
216
|
+
code: `function needsFixOnce() {}`,
|
|
217
|
+
output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction needsFixOnce() {}`,
|
|
218
|
+
errors: [
|
|
219
|
+
{
|
|
220
|
+
messageId: "missingStory",
|
|
221
|
+
suggestions: [
|
|
222
|
+
{
|
|
223
|
+
desc: "Add JSDoc @story annotation for function 'needsFixOnce', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
|
|
224
|
+
output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction needsFixOnce() {}`,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "[REQ-AUTOFIX-SINGLE-APPLICATION] does not duplicate annotations for class methods on subsequent runs",
|
|
232
|
+
code: `class F {\n method() {}\n}`,
|
|
233
|
+
output: `class F {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
|
|
234
|
+
errors: [
|
|
235
|
+
{
|
|
236
|
+
messageId: "missingStory",
|
|
237
|
+
suggestions: [
|
|
238
|
+
{
|
|
239
|
+
desc: "Add JSDoc @story annotation for function 'method', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
|
|
240
|
+
output: `class F {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe("[REQ-AUTOFIX-IDEMPOTENT] and [REQ-AUTOFIX-SINGLE-APPLICATION] valid-annotation-format", () => {
|
|
250
|
+
formatRuleTester.run("valid-annotation-format --fix idempotent behavior", valid_annotation_format_1.default, {
|
|
251
|
+
valid: [
|
|
252
|
+
{
|
|
253
|
+
name: "[REQ-AUTOFIX-IDEMPOTENT] second run after suffix normalization produces no changes",
|
|
254
|
+
code: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md`,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "[REQ-AUTOFIX-SINGLE-APPLICATION] already-correct suffix is not altered or extended again",
|
|
258
|
+
code: `// @story docs/stories/005.0-DEV-EXAMPLE.story.md`,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
invalid: [
|
|
262
|
+
{
|
|
263
|
+
name: "[REQ-AUTOFIX-IDEMPOTENT] adds .story.md once; subsequent run sees no further change",
|
|
264
|
+
code: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION`,
|
|
265
|
+
output: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md`,
|
|
266
|
+
errors: [
|
|
267
|
+
{
|
|
268
|
+
messageId: "invalidStoryFormat",
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "[REQ-AUTOFIX-SINGLE-APPLICATION] converts .story to .story.md only once and does not double-append",
|
|
274
|
+
code: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story`,
|
|
275
|
+
output: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md`,
|
|
276
|
+
errors: [
|
|
277
|
+
{
|
|
278
|
+
messageId: "invalidStoryFormat",
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
});
|
|
284
|
+
});
|
|
199
285
|
});
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
5
5
|
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
6
6
|
* @req REQ-AUTOFIX - Cover additional branch cases in require-story-core (addStoryFixer/reportMissing)
|
|
7
7
|
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-AUTOFIX
|
|
8
|
+
* @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-RESILIENCE
|
|
8
9
|
*/
|
|
9
10
|
const require_story_core_1 = require("../../src/rules/helpers/require-story-core");
|
|
10
11
|
const require_story_helpers_1 = require("../../src/rules/helpers/require-story-helpers");
|
|
@@ -37,4 +38,29 @@ describe("Require Story Core (Story 003.0)", () => {
|
|
|
37
38
|
expect(call.node).toBe(node);
|
|
38
39
|
expect(call.messageId).toBe("missingStory");
|
|
39
40
|
});
|
|
41
|
+
test("coreReportMissing swallows dependency errors and does not break lint run", () => {
|
|
42
|
+
const deps = {
|
|
43
|
+
hasStoryAnnotation: () => {
|
|
44
|
+
throw new Error("boom");
|
|
45
|
+
},
|
|
46
|
+
getReportedFunctionName: () => "fnX",
|
|
47
|
+
resolveAnnotationTargetNode: () => ({ type: "FunctionDeclaration" }),
|
|
48
|
+
getNameNodeForReport: (node) => node,
|
|
49
|
+
buildTemplateConfig: () => ({
|
|
50
|
+
effectiveTemplate: "/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
|
|
51
|
+
allowFix: true,
|
|
52
|
+
}),
|
|
53
|
+
extractName: () => "fnX",
|
|
54
|
+
getAnnotationTemplate: () => "/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
|
|
55
|
+
shouldApplyAutoFix: () => true,
|
|
56
|
+
createAddStoryFix: () => () => ({}),
|
|
57
|
+
createMethodFix: () => () => ({}),
|
|
58
|
+
};
|
|
59
|
+
const context = {
|
|
60
|
+
report: jest.fn(),
|
|
61
|
+
};
|
|
62
|
+
const node = { type: "FunctionDeclaration" };
|
|
63
|
+
expect(() => (0, require_story_core_1.coreReportMissing)(deps, context, {}, { node })).not.toThrow();
|
|
64
|
+
expect(context.report).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
40
66
|
});
|
|
@@ -104,4 +104,42 @@ describe("gatherBranchCommentText else-if behavior (Story 026.0-DEV-ELSE-IF-ANNO
|
|
|
104
104
|
expect(text).toContain("@req REQ-POSITION-PRIORITY-ELSE-IF");
|
|
105
105
|
expect(text).not.toContain("REQ-POSITION-PRIORITY-ELSE-IF-BETWEEN");
|
|
106
106
|
});
|
|
107
|
+
it("[REQ-SINGLE-LINE-ELSE-IF-SUPPORT] detects annotations on single-line else-if without braces when placed before the else-if keyword", () => {
|
|
108
|
+
const lines = [
|
|
109
|
+
"let suggestion;",
|
|
110
|
+
"// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
111
|
+
"// @req REQ-SINGLE-LINE-ELSE-IF-SUPPORT",
|
|
112
|
+
"if (arg === \"--json\") suggestion = \"--format=json\";",
|
|
113
|
+
"// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
114
|
+
"// @req REQ-SINGLE-LINE-ELSE-IF-SUPPORT",
|
|
115
|
+
"else if (arg.startsWith(\"--format\")) suggestion = \"--format\";",
|
|
116
|
+
];
|
|
117
|
+
const sourceCode = createMockSourceCode({
|
|
118
|
+
commentsBefore: [
|
|
119
|
+
{
|
|
120
|
+
value: "@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
|
|
121
|
+
},
|
|
122
|
+
{ value: "@req REQ-SINGLE-LINE-ELSE-IF-SUPPORT" },
|
|
123
|
+
],
|
|
124
|
+
lines,
|
|
125
|
+
});
|
|
126
|
+
const node = {
|
|
127
|
+
type: "IfStatement",
|
|
128
|
+
loc: { start: { line: 7 } },
|
|
129
|
+
test: { loc: { end: { line: 7 } } },
|
|
130
|
+
consequent: {
|
|
131
|
+
// single-line consequent without BlockStatement braces in the real-world source;
|
|
132
|
+
// for this helper-level test we only care that loc values exist and are consistent.
|
|
133
|
+
type: "ExpressionStatement",
|
|
134
|
+
loc: { start: { line: 7 } },
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const parent = {
|
|
138
|
+
type: "IfStatement",
|
|
139
|
+
alternate: node,
|
|
140
|
+
};
|
|
141
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
|
|
142
|
+
expect(text).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
|
|
143
|
+
expect(text).toContain("@req REQ-SINGLE-LINE-ELSE-IF-SUPPORT");
|
|
144
|
+
});
|
|
107
145
|
});
|
|
@@ -309,4 +309,50 @@ describe("reqAnnotationDetection advanced heuristics (Story 003.0-DEV-FUNCTION-A
|
|
|
309
309
|
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], undefined, node);
|
|
310
310
|
expect(has).toBe(true);
|
|
311
311
|
});
|
|
312
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] hasReqAnnotation returns true when advanced heuristics find req via linesBeforeHasReq", () => {
|
|
313
|
+
const context = {
|
|
314
|
+
getSourceCode() {
|
|
315
|
+
return createMockSourceCode({
|
|
316
|
+
lines: [
|
|
317
|
+
"// header without req",
|
|
318
|
+
"/** @req REQ-ADV-LINES */",
|
|
319
|
+
"function bar() {}",
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
const node = {
|
|
325
|
+
loc: { start: { line: 3 } },
|
|
326
|
+
parent: {},
|
|
327
|
+
};
|
|
328
|
+
const jsdoc = { value: "/** no req here */" };
|
|
329
|
+
const comments = [{ value: "no req or supports here" }];
|
|
330
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, comments, context, node);
|
|
331
|
+
expect(has).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
it("[REQ-ANNOTATION-REQ-DETECTION] hasReqAnnotation returns true when advanced heuristics find req via parentChainHasReq", () => {
|
|
334
|
+
const sourceCode = {
|
|
335
|
+
getCommentsBefore(n) {
|
|
336
|
+
if (n && n.isReqParent) {
|
|
337
|
+
return [{ value: "/* @req REQ-ADV-PARENT */" }];
|
|
338
|
+
}
|
|
339
|
+
return [{ value: "no req here" }];
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
const context = {
|
|
343
|
+
getSourceCode() {
|
|
344
|
+
return sourceCode;
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
const node = {
|
|
348
|
+
parent: {
|
|
349
|
+
isReqParent: true,
|
|
350
|
+
parent: {},
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
const jsdoc = { value: "/** jsdoc without requirement */" };
|
|
354
|
+
const comments = [{ value: "comment without requirement" }];
|
|
355
|
+
const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, comments, context, node);
|
|
356
|
+
expect(has).toBe(true);
|
|
357
|
+
});
|
|
312
358
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.1",
|
|
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",
|