eslint-plugin-traceability 1.15.0 → 1.16.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/CHANGELOG.md +2 -2
- package/README.md +61 -13
- package/lib/src/index.js +59 -0
- package/lib/src/rules/helpers/require-story-core.js +1 -1
- package/lib/src/rules/helpers/require-story-helpers.d.ts +10 -3
- package/lib/src/rules/helpers/require-story-helpers.js +66 -7
- package/lib/src/rules/no-redundant-annotation.js +73 -55
- package/lib/src/rules/require-branch-annotation.js +2 -2
- package/lib/src/rules/require-req-annotation.js +2 -2
- package/lib/src/rules/require-story-annotation.js +8 -3
- package/lib/src/rules/require-traceability.js +8 -9
- package/lib/src/utils/annotation-checker.js +23 -4
- package/lib/tests/cli-error-handling.test.js +10 -1
- package/lib/tests/integration/cli-integration.test.js +5 -0
- package/lib/tests/integration/require-traceability-aliases.integration.test.js +126 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +23 -0
- package/lib/tests/rules/auto-fix-behavior-008.test.js +7 -7
- package/lib/tests/rules/error-reporting.test.js +1 -1
- package/lib/tests/rules/no-redundant-annotation.test.js +20 -0
- package/lib/tests/rules/require-story-annotation.test.js +49 -10
- package/lib/tests/rules/require-story-helpers.test.js +32 -0
- package/lib/tests/rules/require-story-utils.test.d.ts +7 -0
- package/lib/tests/rules/require-story-utils.test.js +158 -0
- package/lib/tests/utils/annotation-checker-branches.test.d.ts +5 -0
- package/lib/tests/utils/annotation-checker-branches.test.js +103 -0
- package/lib/tests/utils/annotation-scope-analyzer.test.js +134 -0
- package/lib/tests/utils/branch-annotation-helpers.test.js +66 -0
- package/package.json +2 -2
- package/user-docs/api-reference.md +71 -15
- package/user-docs/examples.md +24 -13
- package/user-docs/migration-guide.md +127 -4
- package/user-docs/traceability-overview.md +116 -0
- package/lib/tests/integration/dogfooding-validation.test.js +0 -129
- /package/lib/tests/integration/{dogfooding-validation.test.d.ts → require-traceability-aliases.integration.test.d.ts} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
4
|
+
* @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
|
|
5
|
+
* @req REQ-ANNOTATION-REQUIRED - Verify getNodeName resolves names for diverse AST node shapes
|
|
6
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const require_story_utils_1 = require("../../src/rules/helpers/require-story-utils");
|
|
10
|
+
describe("Require Story Utils - getNodeName (Story 003.0)", () => {
|
|
11
|
+
it("[REQ-ANNOTATION-REQUIRED] returns identifier name for Identifier and JSXIdentifier", () => {
|
|
12
|
+
const idNode = { type: "Identifier", name: "foo" };
|
|
13
|
+
const jsxIdNode = { type: "JSXIdentifier", name: "Bar" };
|
|
14
|
+
expect((0, require_story_utils_1.getNodeName)(idNode)).toBe("foo");
|
|
15
|
+
expect((0, require_story_utils_1.getNodeName)(jsxIdNode)).toBe("Bar");
|
|
16
|
+
});
|
|
17
|
+
it("[REQ-ANNOTATION-REQUIRED] returns null for identifier-like nodes without string name", () => {
|
|
18
|
+
const badId = { type: "Identifier", name: 123 };
|
|
19
|
+
const badJsxId = { type: "JSXIdentifier", name: null };
|
|
20
|
+
expect((0, require_story_utils_1.getNodeName)(badId)).toBeNull();
|
|
21
|
+
expect((0, require_story_utils_1.getNodeName)(badJsxId)).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it("[REQ-ANNOTATION-REQUIRED] converts simple Literal values into string names", () => {
|
|
24
|
+
const stringLit = { type: "Literal", value: "name" };
|
|
25
|
+
const numberLit = { type: "Literal", value: 42 };
|
|
26
|
+
const boolLit = { type: "Literal", value: true };
|
|
27
|
+
const nullLit = { type: "Literal", value: null };
|
|
28
|
+
const objLit = { type: "Literal", value: { a: 1 } };
|
|
29
|
+
expect((0, require_story_utils_1.getNodeName)(stringLit)).toBe("name");
|
|
30
|
+
expect((0, require_story_utils_1.getNodeName)(numberLit)).toBe("42");
|
|
31
|
+
expect((0, require_story_utils_1.getNodeName)(boolLit)).toBe("true");
|
|
32
|
+
expect((0, require_story_utils_1.getNodeName)(nullLit)).toBeNull();
|
|
33
|
+
expect((0, require_story_utils_1.getNodeName)(objLit)).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
it("[REQ-ANNOTATION-REQUIRED] resolves simple, expression-free TemplateLiteral names", () => {
|
|
36
|
+
const tplNode = {
|
|
37
|
+
type: "TemplateLiteral",
|
|
38
|
+
expressions: [],
|
|
39
|
+
quasis: [
|
|
40
|
+
{ value: { cooked: "hello", raw: "hello" } },
|
|
41
|
+
{ value: { cooked: "-world", raw: "-world" } },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
const withExpressions = {
|
|
45
|
+
type: "TemplateLiteral",
|
|
46
|
+
expressions: [{}],
|
|
47
|
+
quasis: [{ value: { cooked: "ignored", raw: "ignored" } }],
|
|
48
|
+
};
|
|
49
|
+
expect((0, require_story_utils_1.getNodeName)(tplNode)).toBe("hello-world");
|
|
50
|
+
expect((0, require_story_utils_1.getNodeName)(withExpressions)).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it("[REQ-ANNOTATION-REQUIRED] resolves non-computed member/qualified names and rejects computed", () => {
|
|
53
|
+
const memberExpr = {
|
|
54
|
+
type: "MemberExpression",
|
|
55
|
+
object: { type: "Identifier", name: "obj" },
|
|
56
|
+
property: { type: "Identifier", name: "prop" },
|
|
57
|
+
computed: false,
|
|
58
|
+
};
|
|
59
|
+
const computedMember = {
|
|
60
|
+
type: "MemberExpression",
|
|
61
|
+
object: { type: "Identifier", name: "obj" },
|
|
62
|
+
property: { type: "Literal", value: "dynamic" },
|
|
63
|
+
computed: true,
|
|
64
|
+
};
|
|
65
|
+
const tsQualified = {
|
|
66
|
+
type: "TSQualifiedName",
|
|
67
|
+
left: { type: "Identifier", name: "Ns" },
|
|
68
|
+
right: { type: "Identifier", name: "Type" },
|
|
69
|
+
};
|
|
70
|
+
const jsxMember = {
|
|
71
|
+
type: "JSXMemberExpression",
|
|
72
|
+
object: { type: "JSXIdentifier", name: "Ns" },
|
|
73
|
+
property: { type: "JSXIdentifier", name: "Component" },
|
|
74
|
+
};
|
|
75
|
+
expect((0, require_story_utils_1.getNodeName)(memberExpr)).toBe("prop");
|
|
76
|
+
expect((0, require_story_utils_1.getNodeName)(computedMember)).toBeNull();
|
|
77
|
+
expect((0, require_story_utils_1.getNodeName)(tsQualified)).toBe("Type");
|
|
78
|
+
expect((0, require_story_utils_1.getNodeName)(jsxMember)).toBe("Component");
|
|
79
|
+
});
|
|
80
|
+
it("[REQ-ANNOTATION-REQUIRED] extracts names from Property/ObjectProperty keys", () => {
|
|
81
|
+
const prop = {
|
|
82
|
+
type: "Property",
|
|
83
|
+
key: { type: "Identifier", name: "propName" },
|
|
84
|
+
};
|
|
85
|
+
const objProp = {
|
|
86
|
+
type: "ObjectProperty",
|
|
87
|
+
key: { type: "Literal", value: "literalKey" },
|
|
88
|
+
};
|
|
89
|
+
const notProp = { type: "MethodDefinition", key: { name: "method" } };
|
|
90
|
+
expect((0, require_story_utils_1.getNodeName)(prop)).toBe("propName");
|
|
91
|
+
expect((0, require_story_utils_1.getNodeName)(objProp)).toBe("literalKey");
|
|
92
|
+
expect((0, require_story_utils_1.getNodeName)(notProp)).toBe("method");
|
|
93
|
+
});
|
|
94
|
+
it("[REQ-ANNOTATION-REQUIRED] prefers direct id/key names before deeper inspection", () => {
|
|
95
|
+
const funcNode = {
|
|
96
|
+
type: "FunctionDeclaration",
|
|
97
|
+
id: { type: "Identifier", name: "directName" },
|
|
98
|
+
key: { type: "Identifier", name: "ignored" },
|
|
99
|
+
};
|
|
100
|
+
const keyNode = {
|
|
101
|
+
type: "MethodDefinition",
|
|
102
|
+
key: { type: "Identifier", name: "keyName" },
|
|
103
|
+
};
|
|
104
|
+
expect((0, require_story_utils_1.getNodeName)(funcNode)).toBe("directName");
|
|
105
|
+
expect((0, require_story_utils_1.getNodeName)(keyNode)).toBe("keyName");
|
|
106
|
+
});
|
|
107
|
+
it("[REQ-ANNOTATION-REQUIRED] unwraps TSLiteralType and JSXNamespacedName wrappers", () => {
|
|
108
|
+
const tsLiteral = {
|
|
109
|
+
type: "TSLiteralType",
|
|
110
|
+
literal: { type: "Literal", value: "wrapped" },
|
|
111
|
+
};
|
|
112
|
+
const jsxNamespaced = {
|
|
113
|
+
type: "JSXNamespacedName",
|
|
114
|
+
name: { type: "JSXIdentifier", name: "NsComponent" },
|
|
115
|
+
};
|
|
116
|
+
expect((0, require_story_utils_1.getNodeName)(tsLiteral)).toBe("wrapped");
|
|
117
|
+
expect((0, require_story_utils_1.getNodeName)(jsxNamespaced)).toBe("NsComponent");
|
|
118
|
+
});
|
|
119
|
+
it("[REQ-ANNOTATION-REQUIRED] returns null for non-TemplateLiteral nodes passed to templateLiteralToString via getNodeName", () => {
|
|
120
|
+
const fakeTemplate = {
|
|
121
|
+
type: "Literal",
|
|
122
|
+
value: "no-template",
|
|
123
|
+
quasis: [{ value: { cooked: "ignored", raw: "ignored" } }],
|
|
124
|
+
};
|
|
125
|
+
const realTemplateWithExpr = {
|
|
126
|
+
type: "TemplateLiteral",
|
|
127
|
+
expressions: [{ type: "Identifier", name: "expr" }],
|
|
128
|
+
quasis: [{ value: { cooked: "start", raw: "start" } }],
|
|
129
|
+
};
|
|
130
|
+
expect((0, require_story_utils_1.getNodeName)(fakeTemplate)).toBe("no-template");
|
|
131
|
+
expect((0, require_story_utils_1.getNodeName)(realTemplateWithExpr)).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
it("[REQ-ANNOTATION-REQUIRED] handles nullish and missing .value in TemplateLiteral quasis defensively", () => {
|
|
134
|
+
const defensiveTemplate = {
|
|
135
|
+
type: "TemplateLiteral",
|
|
136
|
+
expressions: [],
|
|
137
|
+
quasis: [
|
|
138
|
+
null,
|
|
139
|
+
{ value: null },
|
|
140
|
+
{ value: { cooked: "part1", raw: "raw1" } },
|
|
141
|
+
{ value: { raw: "-only-raw" } },
|
|
142
|
+
{},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
expect((0, require_story_utils_1.getNodeName)(defensiveTemplate)).toBe("part1-only-raw");
|
|
146
|
+
});
|
|
147
|
+
it("[REQ-ANNOTATION-REQUIRED] follows generic .key fallback for other shapes", () => {
|
|
148
|
+
const genericWithKey = {
|
|
149
|
+
type: "SomeNode",
|
|
150
|
+
key: { type: "Identifier", name: "viaKey" },
|
|
151
|
+
};
|
|
152
|
+
const genericWithoutKey = {
|
|
153
|
+
type: "SomeNode",
|
|
154
|
+
};
|
|
155
|
+
expect((0, require_story_utils_1.getNodeName)(genericWithKey)).toBe("viaKey");
|
|
156
|
+
expect((0, require_story_utils_1.getNodeName)(genericWithoutKey)).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Focused branch coverage tests for annotation-checker helper.
|
|
4
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-AUTOFIX REQ-ANNOTATION-REPORTING
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
jest.mock("../../src/utils/reqAnnotationDetection", () => ({
|
|
8
|
+
// Always report that no requirement annotation is present so we exercise
|
|
9
|
+
// the missing-annotation reporting and autofix paths in the helper.
|
|
10
|
+
hasReqAnnotation: jest.fn(() => false),
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("../../src/rules/helpers/require-story-utils", () => ({
|
|
13
|
+
// Provide a stable, human-readable name so reporting paths are predictable
|
|
14
|
+
// without depending on the full real implementation.
|
|
15
|
+
getNodeName: jest.fn(() => "mockName"),
|
|
16
|
+
}));
|
|
17
|
+
const annotation_checker_1 = require("../../src/utils/annotation-checker");
|
|
18
|
+
/**
|
|
19
|
+
* Build a minimal ESLint rule context stub that captures report() calls.
|
|
20
|
+
*
|
|
21
|
+
* @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REPORTING
|
|
22
|
+
*/
|
|
23
|
+
function createContextStub() {
|
|
24
|
+
const report = jest.fn();
|
|
25
|
+
const sourceCode = {
|
|
26
|
+
getJSDocComment: jest.fn(() => null),
|
|
27
|
+
getCommentsBefore: jest.fn(() => []),
|
|
28
|
+
};
|
|
29
|
+
const context = {
|
|
30
|
+
getSourceCode() {
|
|
31
|
+
return sourceCode;
|
|
32
|
+
},
|
|
33
|
+
report,
|
|
34
|
+
};
|
|
35
|
+
return { context, report };
|
|
36
|
+
}
|
|
37
|
+
describe("annotation-checker helper branch coverage (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
|
|
38
|
+
it("[REQ-ANNOTATION-AUTOFIX] attaches fix directly to node when parent is missing", () => {
|
|
39
|
+
const { context, report } = createContextStub();
|
|
40
|
+
const node = { type: "FunctionDeclaration" }; // no parent property
|
|
41
|
+
(0, annotation_checker_1.checkReqAnnotation)(context, node, { enableFix: true });
|
|
42
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
43
|
+
const reportArg = report.mock.calls[0][0];
|
|
44
|
+
expect(reportArg).toHaveProperty("fix");
|
|
45
|
+
const fixer = { insertTextBefore: jest.fn() };
|
|
46
|
+
reportArg.fix(fixer);
|
|
47
|
+
expect(fixer.insertTextBefore).toHaveBeenCalledWith(node, "/** @req <REQ-ID> */\n");
|
|
48
|
+
});
|
|
49
|
+
it("[REQ-ANNOTATION-AUTOFIX] attaches fix to MethodDefinition wrapper when parent is a method", () => {
|
|
50
|
+
const { context, report } = createContextStub();
|
|
51
|
+
const methodParent = { type: "MethodDefinition" };
|
|
52
|
+
const node = {
|
|
53
|
+
type: "FunctionExpression",
|
|
54
|
+
parent: methodParent,
|
|
55
|
+
id: { type: "Identifier", name: "methodImpl" },
|
|
56
|
+
};
|
|
57
|
+
(0, annotation_checker_1.checkReqAnnotation)(context, node, { enableFix: true });
|
|
58
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
59
|
+
const reportArg = report.mock.calls[0][0];
|
|
60
|
+
const fixer = { insertTextBefore: jest.fn() };
|
|
61
|
+
reportArg.fix(fixer);
|
|
62
|
+
expect(fixer.insertTextBefore).toHaveBeenCalledWith(methodParent, "/** @req <REQ-ID> */\n");
|
|
63
|
+
});
|
|
64
|
+
it("[REQ-ANNOTATION-AUTOFIX] attaches fix to VariableDeclarator when node is its init", () => {
|
|
65
|
+
const { context, report } = createContextStub();
|
|
66
|
+
const declarator = { type: "VariableDeclarator" };
|
|
67
|
+
const node = { type: "FunctionExpression", parent: declarator };
|
|
68
|
+
declarator.init = node;
|
|
69
|
+
(0, annotation_checker_1.checkReqAnnotation)(context, node, { enableFix: true });
|
|
70
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
71
|
+
const reportArg = report.mock.calls[0][0];
|
|
72
|
+
const fixer = { insertTextBefore: jest.fn() };
|
|
73
|
+
reportArg.fix(fixer);
|
|
74
|
+
expect(fixer.insertTextBefore).toHaveBeenCalledWith(declarator, "/** @req <REQ-ID> */\n");
|
|
75
|
+
});
|
|
76
|
+
it("[REQ-ANNOTATION-AUTOFIX] attaches fix to ExpressionStatement wrapper when parent is an expression", () => {
|
|
77
|
+
const { context, report } = createContextStub();
|
|
78
|
+
const expressionParent = { type: "ExpressionStatement" };
|
|
79
|
+
const node = {
|
|
80
|
+
type: "FunctionExpression",
|
|
81
|
+
parent: expressionParent,
|
|
82
|
+
id: { type: "Identifier", name: "iife" },
|
|
83
|
+
};
|
|
84
|
+
(0, annotation_checker_1.checkReqAnnotation)(context, node, { enableFix: true });
|
|
85
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
86
|
+
const reportArg = report.mock.calls[0][0];
|
|
87
|
+
const fixer = { insertTextBefore: jest.fn() };
|
|
88
|
+
reportArg.fix(fixer);
|
|
89
|
+
expect(fixer.insertTextBefore).toHaveBeenCalledWith(expressionParent, "/** @req <REQ-ID> */\n");
|
|
90
|
+
});
|
|
91
|
+
it("[REQ-ANNOTATION-AUTOFIX] omits fix when enableFix is false", () => {
|
|
92
|
+
const { context, report } = createContextStub();
|
|
93
|
+
const node = {
|
|
94
|
+
type: "FunctionDeclaration",
|
|
95
|
+
parent: { type: "Program" },
|
|
96
|
+
id: { type: "Identifier", name: "noFix" },
|
|
97
|
+
};
|
|
98
|
+
(0, annotation_checker_1.checkReqAnnotation)(context, node, { enableFix: false });
|
|
99
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
100
|
+
const reportArg = report.mock.calls[0][0];
|
|
101
|
+
expect(reportArg.fix).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -6,6 +6,12 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
6
6
|
const key = (0, annotation_scope_analyzer_1.toStoryReqKey)("docs/stories/001.story.md", "REQ-ONE");
|
|
7
7
|
expect(key).toBe("docs/stories/001.story.md|REQ-ONE");
|
|
8
8
|
});
|
|
9
|
+
it("[REQ-DUPLICATION-DETECTION] normalizes missing story or requirement to empty segments", () => {
|
|
10
|
+
const noStory = (0, annotation_scope_analyzer_1.toStoryReqKey)(null, "REQ-ONE");
|
|
11
|
+
const noReq = (0, annotation_scope_analyzer_1.toStoryReqKey)("docs/stories/001.story.md", undefined);
|
|
12
|
+
expect(noStory).toBe("|REQ-ONE");
|
|
13
|
+
expect(noReq).toBe("docs/stories/001.story.md|");
|
|
14
|
+
});
|
|
9
15
|
it("[REQ-DUPLICATION-DETECTION] extracts pairs from @story/@req sequences", () => {
|
|
10
16
|
const text = `// @story docs/stories/001.story.md\n// @req REQ-ONE`;
|
|
11
17
|
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
|
|
@@ -13,6 +19,10 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
13
19
|
"docs/stories/001.story.md|REQ-ONE",
|
|
14
20
|
]);
|
|
15
21
|
});
|
|
22
|
+
it("[REQ-DUPLICATION-DETECTION] returns empty set when text has no annotations", () => {
|
|
23
|
+
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)("");
|
|
24
|
+
expect(pairs.size).toBe(0);
|
|
25
|
+
});
|
|
16
26
|
it("[REQ-SCOPE-ANALYSIS] extracts pairs from @supports lines", () => {
|
|
17
27
|
const text = `// @supports docs/stories/002.story.md REQ-A REQ-B OTHER`;
|
|
18
28
|
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
|
|
@@ -27,6 +37,10 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
27
37
|
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
|
|
28
38
|
expect(pairs.size).toBe(2);
|
|
29
39
|
});
|
|
40
|
+
it("[REQ-DUPLICATION-DETECTION] returns empty set for empty comments list", () => {
|
|
41
|
+
const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)([]);
|
|
42
|
+
expect(pairs.size).toBe(0);
|
|
43
|
+
});
|
|
30
44
|
it("[REQ-DUPLICATION-DETECTION] determines full coverage correctly", () => {
|
|
31
45
|
const parent = new Set([
|
|
32
46
|
"story|REQ-ONE",
|
|
@@ -37,6 +51,11 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
37
51
|
expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childCovered, parent)).toBe(true);
|
|
38
52
|
expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childNotCovered, parent)).toBe(false);
|
|
39
53
|
});
|
|
54
|
+
it("[REQ-DUPLICATION-DETECTION] treats empty child or parent as not covered", () => {
|
|
55
|
+
const nonEmpty = new Set(["story|REQ-ONE"]);
|
|
56
|
+
expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(new Set(), nonEmpty)).toBe(false);
|
|
57
|
+
expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(nonEmpty, new Set())).toBe(false);
|
|
58
|
+
});
|
|
40
59
|
it("[REQ-STATEMENT-SIGNIFICANCE] respects alwaysCovered and strictness levels", () => {
|
|
41
60
|
const base = {
|
|
42
61
|
strictness: "moderate",
|
|
@@ -49,6 +68,39 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
49
68
|
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, base, branchTypes)).toBe(true);
|
|
50
69
|
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "IfStatement" }, base, branchTypes)).toBe(false);
|
|
51
70
|
});
|
|
71
|
+
it("[REQ-CONFIGURABLE-STRICTNESS] treats permissive mode as only honoring alwaysCovered list", () => {
|
|
72
|
+
const options = {
|
|
73
|
+
strictness: "permissive",
|
|
74
|
+
allowEmphasisDuplication: false,
|
|
75
|
+
maxScopeDepth: 3,
|
|
76
|
+
alwaysCovered: ["ReturnStatement"],
|
|
77
|
+
};
|
|
78
|
+
const branchTypes = ["IfStatement"];
|
|
79
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ReturnStatement" }, options, branchTypes)).toBe(true);
|
|
80
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, options, branchTypes)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it("[REQ-CONFIGURABLE-STRICTNESS] treats strict mode as allowing any non-branch statement", () => {
|
|
83
|
+
const options = {
|
|
84
|
+
strictness: "strict",
|
|
85
|
+
allowEmphasisDuplication: false,
|
|
86
|
+
maxScopeDepth: 3,
|
|
87
|
+
alwaysCovered: [],
|
|
88
|
+
};
|
|
89
|
+
const branchTypes = ["IfStatement"];
|
|
90
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, options, branchTypes)).toBe(true);
|
|
91
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "IfStatement" }, options, branchTypes)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it("[REQ-STATEMENT-SIGNIFICANCE] returns false for null or non-node values", () => {
|
|
94
|
+
const options = {
|
|
95
|
+
strictness: "moderate",
|
|
96
|
+
allowEmphasisDuplication: false,
|
|
97
|
+
maxScopeDepth: 3,
|
|
98
|
+
alwaysCovered: [],
|
|
99
|
+
};
|
|
100
|
+
const branchTypes = [];
|
|
101
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)(null, options, branchTypes)).toBe(false);
|
|
102
|
+
expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({}, options, branchTypes)).toBe(false);
|
|
103
|
+
});
|
|
52
104
|
it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment", () => {
|
|
53
105
|
const source = `const x = 1;\n// @story docs/stories/001.story.md\nconst y = 2;\n`;
|
|
54
106
|
const sourceCode = {
|
|
@@ -63,6 +115,77 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
63
115
|
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
64
116
|
expect(removed).toBe("const x = 1;\nconst y = 2;\n");
|
|
65
117
|
});
|
|
118
|
+
it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment with Windows newlines", () => {
|
|
119
|
+
const source = "const x = 1;\r\n// @story docs/stories/001.story.md\r\nconst y = 2;\r\n";
|
|
120
|
+
const sourceCode = {
|
|
121
|
+
getText() {
|
|
122
|
+
return source;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const start = source.indexOf("// @story");
|
|
126
|
+
const end = start + "// @story docs/stories/001.story.md".length;
|
|
127
|
+
const comment = { range: [start, end] };
|
|
128
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
129
|
+
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
130
|
+
expect(removed).toBe("const x = 1;\r\nconst y = 2;\r\n");
|
|
131
|
+
});
|
|
132
|
+
it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment with standalone CR newline", () => {
|
|
133
|
+
const source = "const x = 1;\r// @story docs/stories/001.story.md\rconst y = 2;\r";
|
|
134
|
+
const sourceCode = {
|
|
135
|
+
getText() {
|
|
136
|
+
return source;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const start = source.indexOf("// @story");
|
|
140
|
+
const end = start + "// @story docs/stories/001.story.md".length;
|
|
141
|
+
const comment = { range: [start, end] };
|
|
142
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
143
|
+
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
144
|
+
expect(removed).toBe("const x = 1;\rconst y = 2;\r");
|
|
145
|
+
});
|
|
146
|
+
it("[REQ-SAFE-REMOVAL] computes removal range for inline comment", () => {
|
|
147
|
+
const source = "const x = 1; // @story docs/stories/001.story.md\nconst y = 2;\n";
|
|
148
|
+
const sourceCode = {
|
|
149
|
+
getText() {
|
|
150
|
+
return source;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const start = source.indexOf("// @story");
|
|
154
|
+
const end = start + "// @story docs/stories/001.story.md".length;
|
|
155
|
+
const comment = { range: [start, end] };
|
|
156
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
157
|
+
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
158
|
+
expect(removed).toBe("const x = 1; \nconst y = 2;\n");
|
|
159
|
+
});
|
|
160
|
+
it("[REQ-SAFE-REMOVAL] consumes trailing spaces and tabs following a full-line comment", () => {
|
|
161
|
+
const source = "const x = 1;\n// @story docs/stories/001.story.md \nconst y = 2;\n";
|
|
162
|
+
const sourceCode = {
|
|
163
|
+
getText() {
|
|
164
|
+
return source;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const start = source.indexOf("// @story");
|
|
168
|
+
const end = start + "// @story docs/stories/001.story.md".length;
|
|
169
|
+
const comment = { range: [start, end] };
|
|
170
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
171
|
+
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
172
|
+
expect(removed).toBe("const x = 1;\nconst y = 2;\n");
|
|
173
|
+
});
|
|
174
|
+
it("[REQ-SAFE-REMOVAL] handles full-line comment at end of file without trailing newline", () => {
|
|
175
|
+
const source = "const x = 1;\n// @story docs/stories/001.story.md";
|
|
176
|
+
const sourceCode = {
|
|
177
|
+
getText() {
|
|
178
|
+
return source;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
const start = source.indexOf("// @story");
|
|
182
|
+
const end = start + "// @story docs/stories/001.story.md".length;
|
|
183
|
+
const comment = { range: [start, end] };
|
|
184
|
+
const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
185
|
+
const removed = source.slice(0, removalStart) + source.slice(removalEnd);
|
|
186
|
+
expect(removed).toBe("const x = 1;\n");
|
|
187
|
+
expect(removalEnd).toBe(source.length);
|
|
188
|
+
});
|
|
66
189
|
it("[REQ-SAFE-REMOVAL] returns [0, 0] for comments with invalid range length (EXPECTS EXPECTED_RANGE_LENGTH usage)", () => {
|
|
67
190
|
const source = "const x = 1;";
|
|
68
191
|
const sourceCode = {
|
|
@@ -74,4 +197,15 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
|
|
|
74
197
|
const range = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
75
198
|
expect(range).toEqual([0, 0]);
|
|
76
199
|
});
|
|
200
|
+
it("[REQ-SAFE-REMOVAL] returns [0, 0] when comment range is not an array", () => {
|
|
201
|
+
const source = "const x = 1;";
|
|
202
|
+
const sourceCode = {
|
|
203
|
+
getText() {
|
|
204
|
+
return source;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const comment = { range: null };
|
|
208
|
+
const range = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
|
|
209
|
+
expect(range).toEqual([0, 0]);
|
|
210
|
+
});
|
|
77
211
|
});
|
|
@@ -44,4 +44,70 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
|
|
|
44
44
|
}));
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
|
+
it("should gather SwitchCase comment text via gatherBranchCommentText (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
|
|
48
|
+
// Fake SourceCode-like object with lines aligned to PRE_COMMENT_OFFSET logic
|
|
49
|
+
const sourceCode = {
|
|
50
|
+
lines: [
|
|
51
|
+
"// @story first part",
|
|
52
|
+
"// continuation second part",
|
|
53
|
+
"case 1:",
|
|
54
|
+
],
|
|
55
|
+
getCommentsBefore: () => [],
|
|
56
|
+
getText: jest.fn(),
|
|
57
|
+
};
|
|
58
|
+
// SwitchCase-like node with loc.start.line corresponding to "case 1:" line (line 3)
|
|
59
|
+
const switchCaseNode = {
|
|
60
|
+
type: "SwitchCase",
|
|
61
|
+
loc: {
|
|
62
|
+
start: { line: 3, column: 0 },
|
|
63
|
+
end: { line: 3, column: 7 },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, switchCaseNode);
|
|
67
|
+
// Expect combined text using space separator and preserving leading //
|
|
68
|
+
expect(text).toBe("// @story first part // continuation second part");
|
|
69
|
+
});
|
|
70
|
+
it("should gather comment text for CatchClause and loop nodes via gatherBranchCommentText (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
|
|
71
|
+
// CatchClause: comments come from getCommentsBefore when beforeText already contains @story
|
|
72
|
+
const catchComments = [
|
|
73
|
+
{ type: "Line", value: "@story catch branch story" },
|
|
74
|
+
{ type: "Line", value: "additional info" },
|
|
75
|
+
];
|
|
76
|
+
const sourceCodeCatch = {
|
|
77
|
+
getCommentsBefore: jest.fn().mockReturnValue(catchComments),
|
|
78
|
+
getText: jest.fn().mockReturnValue("@story existing beforeText"),
|
|
79
|
+
lines: [],
|
|
80
|
+
};
|
|
81
|
+
const catchNode = {
|
|
82
|
+
type: "CatchClause",
|
|
83
|
+
loc: {
|
|
84
|
+
start: { line: 10, column: 0 },
|
|
85
|
+
end: { line: 12, column: 1 },
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const catchText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCodeCatch, catchNode);
|
|
89
|
+
expect(sourceCodeCatch.getCommentsBefore).toHaveBeenCalledWith(catchNode);
|
|
90
|
+
expect(catchText).toContain("@story catch branch story");
|
|
91
|
+
expect(catchText).toContain("additional info");
|
|
92
|
+
// Loop node: ForStatement currently uses beforeComments.map(extractCommentValue).join(" ")
|
|
93
|
+
const loopComments = [
|
|
94
|
+
{ type: "Line", value: "@story loop branch story" },
|
|
95
|
+
{ type: "Block", value: "loop details" },
|
|
96
|
+
];
|
|
97
|
+
const sourceCodeLoop = {
|
|
98
|
+
getCommentsBefore: jest.fn().mockReturnValue(loopComments),
|
|
99
|
+
getText: jest.fn().mockReturnValue("@story loop beforeText"),
|
|
100
|
+
lines: [],
|
|
101
|
+
};
|
|
102
|
+
const forNode = {
|
|
103
|
+
type: "ForStatement",
|
|
104
|
+
loc: {
|
|
105
|
+
start: { line: 20, column: 0 },
|
|
106
|
+
end: { line: 25, column: 1 },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const loopText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCodeLoop, forNode);
|
|
110
|
+
expect(sourceCodeLoop.getCommentsBefore).toHaveBeenCalledWith(forNode);
|
|
111
|
+
expect(loopText).toBe("@story loop branch story loop details");
|
|
112
|
+
});
|
|
47
113
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
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",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"lint-staged": "^16.2.7",
|
|
91
91
|
"prettier": "^3.6.2",
|
|
92
92
|
"semantic-release": "25.0.2",
|
|
93
|
-
"ts-jest": "^29.4.
|
|
93
|
+
"ts-jest": "^29.4.6",
|
|
94
94
|
"typescript": "^5.9.3",
|
|
95
95
|
"secretlint": "11.2.5",
|
|
96
96
|
"@secretlint/secretlint-rule-preset-recommend": "11.2.5"
|