eslint-plugin-traceability 1.11.1 → 1.11.3
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 +3 -3
- package/lib/src/index.d.ts +10 -5
- package/lib/src/index.js +71 -6
- package/lib/src/maintenance/commands.js +2 -3
- package/lib/src/maintenance/update.js +1 -14
- package/lib/src/rules/helpers/require-story-core.d.ts +12 -4
- package/lib/src/rules/helpers/require-story-core.js +59 -30
- package/lib/src/rules/helpers/require-story-helpers.d.ts +7 -41
- package/lib/src/rules/helpers/require-story-helpers.js +13 -70
- package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +12 -13
- package/lib/src/rules/helpers/valid-annotation-format-internal.js +21 -16
- package/lib/src/rules/helpers/valid-annotation-format-validators.d.ts +29 -3
- package/lib/src/rules/helpers/valid-annotation-format-validators.js +29 -3
- package/lib/src/rules/helpers/valid-annotation-utils.d.ts +3 -3
- package/lib/src/rules/helpers/valid-annotation-utils.js +10 -10
- package/lib/src/rules/helpers/valid-req-reference-helpers.d.ts +11 -0
- package/lib/src/rules/helpers/valid-req-reference-helpers.js +362 -0
- package/lib/src/rules/prefer-implements-annotation.js +7 -7
- package/lib/src/rules/require-story-annotation.d.ts +2 -0
- package/lib/src/rules/require-story-annotation.js +1 -1
- package/lib/src/rules/valid-req-reference.d.ts +4 -0
- package/lib/src/rules/valid-req-reference.js +5 -349
- package/lib/src/rules/valid-story-reference.d.ts +1 -1
- package/lib/src/rules/valid-story-reference.js +17 -10
- package/lib/src/utils/branch-annotation-helpers.d.ts +2 -2
- package/lib/src/utils/branch-annotation-helpers.js +96 -17
- package/lib/tests/cli-error-handling.test.js +1 -1
- package/lib/tests/config/eslint-config-validation.test.js +73 -0
- package/lib/tests/fixtures/stale/example.js +1 -1
- package/lib/tests/fixtures/update/example.js +1 -1
- package/lib/tests/integration/catch-annotation-prettier.integration.test.d.ts +1 -0
- package/lib/tests/integration/catch-annotation-prettier.integration.test.js +131 -0
- package/lib/tests/integration/dogfooding-validation.test.d.ts +1 -0
- package/lib/tests/integration/dogfooding-validation.test.js +94 -0
- package/lib/tests/maintenance/cli.test.js +37 -0
- package/lib/tests/maintenance/detect-isolated.test.js +5 -5
- package/lib/tests/perf/maintenance-cli-large-workspace.test.js +18 -0
- package/lib/tests/perf/require-branch-annotation-large-file.test.d.ts +1 -0
- package/lib/tests/perf/require-branch-annotation-large-file.test.js +67 -0
- package/lib/tests/perf/valid-annotation-format-large-file.test.d.ts +1 -0
- package/lib/tests/perf/valid-annotation-format-large-file.test.js +74 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +1 -0
- package/lib/tests/plugin-setup.test.js +12 -1
- package/lib/tests/rules/prefer-implements-annotation.test.js +84 -70
- package/lib/tests/rules/require-branch-annotation.test.js +33 -1
- package/lib/tests/rules/valid-annotation-format-internal.test.d.ts +8 -0
- package/lib/tests/rules/valid-annotation-format-internal.test.js +47 -0
- package/lib/tests/utils/branch-annotation-catch-insert-position.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-catch-insert-position.test.js +68 -0
- package/lib/tests/utils/branch-annotation-catch-position.test.d.ts +1 -0
- package/lib/tests/utils/branch-annotation-catch-position.test.js +115 -0
- package/lib/tests/utils/req-annotation-detection.test.d.ts +1 -0
- package/lib/tests/utils/req-annotation-detection.test.js +247 -0
- package/package.json +4 -4
- package/user-docs/api-reference.md +20 -12
- package/user-docs/examples.md +2 -1
- package/user-docs/migration-guide.md +11 -7
|
@@ -12,6 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
const valid_story_reference_1 = __importDefault(require("../../src/rules/valid-story-reference"));
|
|
15
|
+
const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
|
|
16
|
+
const index_1 = __importDefault(require("../../src/index"));
|
|
15
17
|
/** @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md */
|
|
16
18
|
describe("ESLint Configuration Setup (Story 002.0-DEV-ESLINT-CONFIG)", () => {
|
|
17
19
|
it("[REQ-RULE-OPTIONS] rule meta.schema defines expected properties", () => {
|
|
@@ -24,4 +26,75 @@ describe("ESLint Configuration Setup (Story 002.0-DEV-ESLINT-CONFIG)", () => {
|
|
|
24
26
|
const schema = valid_story_reference_1.default.meta.schema[0];
|
|
25
27
|
expect(schema.additionalProperties).toBe(false);
|
|
26
28
|
});
|
|
29
|
+
it("[REQ-CONFIG-VALIDATION] ESLint throws on unknown rule option", async () => {
|
|
30
|
+
const eslint = new use_at_your_own_risk_1.FlatESLint({
|
|
31
|
+
overrideConfig: [
|
|
32
|
+
{
|
|
33
|
+
plugins: {
|
|
34
|
+
traceability: index_1.default,
|
|
35
|
+
},
|
|
36
|
+
rules: {
|
|
37
|
+
"traceability/valid-story-reference": [
|
|
38
|
+
"error",
|
|
39
|
+
{
|
|
40
|
+
storyDirectories: ["stories"],
|
|
41
|
+
allowAbsolutePaths: false,
|
|
42
|
+
requireStoryExtension: true,
|
|
43
|
+
unknownOptionKey: true,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
overrideConfigFile: true,
|
|
50
|
+
ignore: false,
|
|
51
|
+
});
|
|
52
|
+
let caughtError;
|
|
53
|
+
try {
|
|
54
|
+
await eslint.lintText("const x = 1;");
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
caughtError = err;
|
|
58
|
+
}
|
|
59
|
+
expect(caughtError).toBeInstanceOf(Error);
|
|
60
|
+
const message = String(caughtError.message || caughtError);
|
|
61
|
+
expect(message).toContain("traceability/valid-story-reference");
|
|
62
|
+
expect(message.toLowerCase()).toContain("additional");
|
|
63
|
+
expect(message.toLowerCase()).toContain("unexpected property");
|
|
64
|
+
expect(message).toContain("unknownOptionKey");
|
|
65
|
+
});
|
|
66
|
+
it("[REQ-CONFIG-VALIDATION] ESLint throws on invalid option type", async () => {
|
|
67
|
+
const eslint = new use_at_your_own_risk_1.FlatESLint({
|
|
68
|
+
overrideConfig: [
|
|
69
|
+
{
|
|
70
|
+
plugins: {
|
|
71
|
+
traceability: index_1.default,
|
|
72
|
+
},
|
|
73
|
+
rules: {
|
|
74
|
+
"traceability/valid-story-reference": [
|
|
75
|
+
"error",
|
|
76
|
+
{
|
|
77
|
+
// storyDirectories must be an array, not a string
|
|
78
|
+
storyDirectories: "not-an-array",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
overrideConfigFile: true,
|
|
85
|
+
ignore: false,
|
|
86
|
+
});
|
|
87
|
+
let caughtError;
|
|
88
|
+
try {
|
|
89
|
+
await eslint.lintText("const y = 2;");
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
caughtError = err;
|
|
93
|
+
}
|
|
94
|
+
expect(caughtError).toBeInstanceOf(Error);
|
|
95
|
+
const message = String(caughtError.message || caughtError);
|
|
96
|
+
expect(message).toContain("traceability/valid-story-reference");
|
|
97
|
+
expect(message).toContain("not-an-array");
|
|
98
|
+
expect(message.toLowerCase()).toContain("array");
|
|
99
|
+
});
|
|
27
100
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* Prettier integration tests for CatchClause annotation positions.
|
|
8
|
+
* @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
9
|
+
* @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-COMPATIBILITY
|
|
10
|
+
*/
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
describe("CatchClause annotations with Prettier (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
|
|
14
|
+
const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
|
|
15
|
+
const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
|
|
16
|
+
const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
|
|
17
|
+
const prettierPackageJson = require.resolve("prettier/package.json");
|
|
18
|
+
const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
|
|
19
|
+
function runEslintWithRequireBranchAnnotation(code) {
|
|
20
|
+
const args = [
|
|
21
|
+
"--no-config-lookup",
|
|
22
|
+
"--config",
|
|
23
|
+
configPath,
|
|
24
|
+
"--stdin",
|
|
25
|
+
"--stdin-filename",
|
|
26
|
+
"catch.js",
|
|
27
|
+
"--rule",
|
|
28
|
+
"no-unused-vars:off",
|
|
29
|
+
"--rule",
|
|
30
|
+
"no-magic-numbers:off",
|
|
31
|
+
"--rule",
|
|
32
|
+
"no-undef:off",
|
|
33
|
+
"--rule",
|
|
34
|
+
"no-console:off",
|
|
35
|
+
"--rule",
|
|
36
|
+
"traceability/require-branch-annotation:error",
|
|
37
|
+
];
|
|
38
|
+
return (0, child_process_1.spawnSync)(process.execPath, [eslintCliPath, ...args], {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
input: code,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function formatWithPrettier(source) {
|
|
44
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
input: source,
|
|
47
|
+
});
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
|
|
50
|
+
}
|
|
51
|
+
return result.stdout;
|
|
52
|
+
}
|
|
53
|
+
it("[REQ-PRETTIER-COMPATIBILITY-BEFORE] accepts code where annotations start before catch but are moved inside by Prettier", () => {
|
|
54
|
+
const original = `
|
|
55
|
+
function doSomething() {
|
|
56
|
+
return 42;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleError(error) {
|
|
60
|
+
console.error(error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
64
|
+
// @req REQ-BRANCH-TRY
|
|
65
|
+
try {
|
|
66
|
+
doSomething();
|
|
67
|
+
}
|
|
68
|
+
// @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
69
|
+
// @req REQ-CATCH-PATH
|
|
70
|
+
catch (error) {
|
|
71
|
+
handleError(error);
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
const formatted = formatWithPrettier(original);
|
|
75
|
+
// Sanity check: Prettier should move the branch annotations inside the catch body.
|
|
76
|
+
expect(formatted).toContain("catch (error) {");
|
|
77
|
+
const catchIndex = formatted.indexOf("catch (error) {");
|
|
78
|
+
const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
|
|
79
|
+
expect(storyIndex).toBeGreaterThan(catchIndex);
|
|
80
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
81
|
+
expect(result.status).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
it("[REQ-PRETTIER-COMPATIBILITY-INSIDE] accepts code where annotations start inside the catch body and are preserved by Prettier", () => {
|
|
84
|
+
const original = `
|
|
85
|
+
function doSomething() {
|
|
86
|
+
return 42;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleError(error) {
|
|
90
|
+
console.error(error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
94
|
+
// @req REQ-BRANCH-TRY
|
|
95
|
+
try {
|
|
96
|
+
doSomething();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
99
|
+
// @req REQ-CATCH-INSIDE
|
|
100
|
+
handleError(error);
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
const formatted = formatWithPrettier(original);
|
|
104
|
+
// Sanity: annotations should still be associated with the catch body after formatting.
|
|
105
|
+
expect(formatted).toContain("catch (error) {");
|
|
106
|
+
const catchIndex = formatted.indexOf("catch (error) {");
|
|
107
|
+
const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
|
|
108
|
+
expect(storyIndex).toBeGreaterThan(catchIndex);
|
|
109
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
110
|
+
expect(result.status).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
it("[REQ-PRETTIER-COMPATIBILITY-EMPTY] accepts empty catch blocks with inside-catch annotations after Prettier formatting", () => {
|
|
113
|
+
const original = `
|
|
114
|
+
function doSomething() {
|
|
115
|
+
return 42;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
|
|
119
|
+
// @req REQ-BRANCH-TRY
|
|
120
|
+
try {
|
|
121
|
+
doSomething();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
|
|
124
|
+
// @req REQ-CATCH-EMPTY
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
const formatted = formatWithPrettier(original);
|
|
128
|
+
const result = runEslintWithRequireBranchAnnotation(formatted);
|
|
129
|
+
expect(result.status).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* Dogfooding validation integration tests
|
|
38
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI
|
|
39
|
+
*/
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
/**
|
|
43
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
|
|
44
|
+
*/
|
|
45
|
+
function getTsConfigFromEslintConfig(eslintConfig) {
|
|
46
|
+
const configs = Array.isArray(eslintConfig) ? eslintConfig : [eslintConfig];
|
|
47
|
+
return configs.find((config) => {
|
|
48
|
+
if (!config || !config.files)
|
|
49
|
+
return false;
|
|
50
|
+
const files = config.files;
|
|
51
|
+
return files.includes("**/*.ts") && files.includes("**/*.tsx");
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
describe("Dogfooding Validation (Story 023.0-MAINT-DOGFOODING-VALIDATION)", () => {
|
|
55
|
+
it("[REQ-DOGFOODING-TEST] should have traceability/require-story-annotation enabled for TS sources", () => {
|
|
56
|
+
/**
|
|
57
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
|
|
58
|
+
*/
|
|
59
|
+
// Require the project's eslint.config.js and find the TS-specific config
|
|
60
|
+
// that applies to *.ts and *.tsx files.
|
|
61
|
+
const eslintConfig = require("../../eslint.config.js");
|
|
62
|
+
const tsConfig = getTsConfigFromEslintConfig(eslintConfig);
|
|
63
|
+
expect(tsConfig).toBeDefined();
|
|
64
|
+
const rules = tsConfig.rules || {};
|
|
65
|
+
const ruleEntry = rules["traceability/require-story-annotation"];
|
|
66
|
+
expect(ruleEntry).toBeDefined();
|
|
67
|
+
const severity = Array.isArray(ruleEntry) && ruleEntry.length > 0
|
|
68
|
+
? ruleEntry[0]
|
|
69
|
+
: ruleEntry;
|
|
70
|
+
expect(severity).toBe("error");
|
|
71
|
+
});
|
|
72
|
+
it("[REQ-DOGFOODING-CI] should run traceability/require-story-annotation via ESLint CLI on TS sources", () => {
|
|
73
|
+
/**
|
|
74
|
+
* @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-CI
|
|
75
|
+
*/
|
|
76
|
+
const eslintBin = path.resolve(__dirname, "../../node_modules/.bin/eslint");
|
|
77
|
+
const configPath = path.resolve(__dirname, "../../eslint.config.js");
|
|
78
|
+
const tsSnippet = `
|
|
79
|
+
const x: number = 42;
|
|
80
|
+
export function foo() {
|
|
81
|
+
return x;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
const result = (0, child_process_1.spawnSync)(process.platform === "win32" ? `${eslintBin}.cmd` : eslintBin, ["--config", configPath, "--stdin", "--stdin-filename", "src/dogfood.ts"], {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
input: tsSnippet,
|
|
87
|
+
});
|
|
88
|
+
// The snippet intentionally lacks @story annotations, so the rule should
|
|
89
|
+
// report an error for the generated `src/dogfood.ts` virtual file.
|
|
90
|
+
expect(result.status).not.toBe(0);
|
|
91
|
+
expect(result.stdout).toContain("error");
|
|
92
|
+
expect(result.stdout).toContain("src/dogfood.ts");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -58,6 +58,26 @@ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
|
58
58
|
temp.cleanup();
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
|
+
it("[REQ-MAINT-VERIFY] verify exits with code 1 and prints guidance when annotations are stale or invalid", () => {
|
|
62
|
+
const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
|
|
63
|
+
const dir = temp.dir;
|
|
64
|
+
process.chdir(dir);
|
|
65
|
+
const tsContent = `/**\n * @story missing.story.md\n */`;
|
|
66
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
|
|
67
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
68
|
+
const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "verify"]);
|
|
69
|
+
try {
|
|
70
|
+
expect(code).toBe(1);
|
|
71
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
72
|
+
const message = String(logSpy.mock.calls[0][0]);
|
|
73
|
+
expect(message).toContain("Stale or invalid traceability annotations detected under");
|
|
74
|
+
expect(message).toContain("Run 'traceability-maint detect' or 'traceability-maint report' for details.");
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
logSpy.mockRestore();
|
|
78
|
+
temp.cleanup();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
61
81
|
it("[REQ-MAINT-REPORT] report prints human-readable summary and exits 0", () => {
|
|
62
82
|
const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
|
|
63
83
|
const dir = temp.dir;
|
|
@@ -77,6 +97,23 @@ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
|
77
97
|
temp.cleanup();
|
|
78
98
|
}
|
|
79
99
|
});
|
|
100
|
+
it("[REQ-MAINT-REPORT] report prints 'nothing to report' when no stale annotations exist", () => {
|
|
101
|
+
const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
|
|
102
|
+
const dir = temp.dir;
|
|
103
|
+
process.chdir(dir);
|
|
104
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
105
|
+
const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "report"]);
|
|
106
|
+
try {
|
|
107
|
+
expect(code).toBe(0);
|
|
108
|
+
expect(logSpy).toHaveBeenCalled();
|
|
109
|
+
const allMessages = logSpy.mock.calls.flat().join("\n");
|
|
110
|
+
expect(allMessages).toContain("No stale @story annotations found. Nothing to report.");
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
logSpy.mockRestore();
|
|
114
|
+
temp.cleanup();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
80
117
|
it("[REQ-MAINT-UPDATE] update performs replacements and exits 0", () => {
|
|
81
118
|
const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
|
|
82
119
|
const dir = temp.dir;
|
|
@@ -57,26 +57,26 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
|
|
|
57
57
|
const filePath2 = path.join(nestedDir, "file2.ts");
|
|
58
58
|
const content1 = `
|
|
59
59
|
/**
|
|
60
|
-
* @story
|
|
60
|
+
* @story docs/stories/non-existent-story.story.md
|
|
61
61
|
*/
|
|
62
62
|
`;
|
|
63
63
|
fs.writeFileSync(filePath1, content1, "utf8");
|
|
64
64
|
const content2 = `
|
|
65
65
|
/**
|
|
66
|
-
* @story
|
|
66
|
+
* @story docs/stories/another-non-existent.story.md
|
|
67
67
|
*/
|
|
68
68
|
`;
|
|
69
69
|
fs.writeFileSync(filePath2, content2, "utf8");
|
|
70
70
|
const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
|
|
71
71
|
expect(result).toHaveLength(2);
|
|
72
|
-
expect(result).toContain("
|
|
73
|
-
expect(result).toContain("
|
|
72
|
+
expect(result).toContain("docs/stories/non-existent-story.story.md");
|
|
73
|
+
expect(result).toContain("docs/stories/another-non-existent.story.md");
|
|
74
74
|
}
|
|
75
75
|
finally {
|
|
76
76
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
|
-
it("[REQ-MAINT-DETECT]
|
|
79
|
+
it("[REQ-MAINT-DETECT] handles permission denied errors by returning an empty result", () => {
|
|
80
80
|
const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
|
|
81
81
|
const dir = path.join(tmpDir2, "subdir");
|
|
82
82
|
fs.mkdirSync(dir);
|
|
@@ -127,4 +127,22 @@ describe("Maintenance CLI on large workspaces (Story 009.0-DEV-MAINTENANCE-TOOLS
|
|
|
127
127
|
expect(typeof payload.report).toBe("string");
|
|
128
128
|
logSpy.mockRestore();
|
|
129
129
|
});
|
|
130
|
+
it("[REQ-MAINT-VERIFY] verify completes within a generous time budget and reports stale annotations", () => {
|
|
131
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
132
|
+
const start = perf_hooks_1.performance.now();
|
|
133
|
+
const exitCode = (0, cli_1.runMaintenanceCli)([
|
|
134
|
+
"node",
|
|
135
|
+
"traceability-maint",
|
|
136
|
+
"verify",
|
|
137
|
+
"--root",
|
|
138
|
+
workspace.root,
|
|
139
|
+
]);
|
|
140
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
141
|
+
expect(exitCode).toBe(1);
|
|
142
|
+
expect(durationMs).toBeLessThan(5000);
|
|
143
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
144
|
+
const message = String(logSpy.mock.calls[0][0]);
|
|
145
|
+
expect(message).toContain("Stale or invalid traceability annotations detected under");
|
|
146
|
+
logSpy.mockRestore();
|
|
147
|
+
});
|
|
130
148
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* Performance tests for require-branch-annotation on large nested-branch files.
|
|
8
|
+
*
|
|
9
|
+
* @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-PERFORMANCE-OPTIMIZATION REQ-NESTED-HANDLING
|
|
10
|
+
*/
|
|
11
|
+
const eslint_1 = require("eslint");
|
|
12
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
13
|
+
const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));
|
|
14
|
+
/**
|
|
15
|
+
* Build a large source file containing many nested branch structures
|
|
16
|
+
* (if-statements within if-statements) to exercise the rule at scale.
|
|
17
|
+
*
|
|
18
|
+
* The generated code intentionally omits annotations so that the rule
|
|
19
|
+
* produces diagnostics for both outer and inner branches.
|
|
20
|
+
*/
|
|
21
|
+
function buildLargeNestedBranchSource(functionCount, nestingDepth) {
|
|
22
|
+
const lines = [];
|
|
23
|
+
for (let i = 0; i < functionCount; i += 1) {
|
|
24
|
+
lines.push(`function fn_${i}() {`);
|
|
25
|
+
lines.push(" let x = 0;");
|
|
26
|
+
// Create a staircase of nested if-statements.
|
|
27
|
+
for (let depth = 0; depth < nestingDepth; depth += 1) {
|
|
28
|
+
const indent = " ".repeat(depth + 1);
|
|
29
|
+
lines.push(`${indent}if (x > ${depth}) {`);
|
|
30
|
+
}
|
|
31
|
+
const innerIndent = " ".repeat(nestingDepth + 1);
|
|
32
|
+
lines.push(`${innerIndent}if (x % 2 === 0) {`);
|
|
33
|
+
lines.push(`${innerIndent} x++;`);
|
|
34
|
+
lines.push(`${innerIndent}} else {`);
|
|
35
|
+
lines.push(`${innerIndent} x--;`);
|
|
36
|
+
lines.push(`${innerIndent}}`);
|
|
37
|
+
// Close all nested if blocks.
|
|
38
|
+
for (let depth = nestingDepth - 1; depth >= 0; depth -= 1) {
|
|
39
|
+
const indent = " ".repeat(depth + 1);
|
|
40
|
+
lines.push(`${indent}}`);
|
|
41
|
+
}
|
|
42
|
+
lines.push("}");
|
|
43
|
+
}
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
describe("require-branch-annotation performance on large nested-branch files (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
|
|
47
|
+
const ruleName = "traceability/require-branch-annotation";
|
|
48
|
+
it("[REQ-PERFORMANCE-OPTIMIZATION] analyzes a large nested-branch file within a generous time budget", () => {
|
|
49
|
+
const linter = new eslint_1.Linter({ configType: "eslintrc" });
|
|
50
|
+
linter.defineRule(ruleName, require_branch_annotation_1.default);
|
|
51
|
+
// 200 functions each with several nested branches gives us
|
|
52
|
+
// a substantial number of branch nodes without being extreme.
|
|
53
|
+
const source = buildLargeNestedBranchSource(200, 3);
|
|
54
|
+
const start = perf_hooks_1.performance.now();
|
|
55
|
+
const messages = linter.verify(source, {
|
|
56
|
+
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
|
|
57
|
+
rules: {
|
|
58
|
+
[ruleName]: "error",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
62
|
+
// Sanity check: we expect diagnostics for many branches.
|
|
63
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
64
|
+
// Guardrail: keep analysis comfortably under ~5 seconds on CI hardware.
|
|
65
|
+
expect(durationMs).toBeLessThan(5000);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* Performance tests for valid-annotation-format on large annotated files.
|
|
8
|
+
*
|
|
9
|
+
* @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-MULTILINE-SUPPORT REQ-FLEXIBLE-PARSING REQ-SYNTAX-VALIDATION
|
|
10
|
+
*/
|
|
11
|
+
const eslint_1 = require("eslint");
|
|
12
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
13
|
+
const valid_annotation_format_1 = __importDefault(require("../../src/rules/valid-annotation-format"));
|
|
14
|
+
/**
|
|
15
|
+
* Build a large source file containing many functions with traceability
|
|
16
|
+
* annotations in both line and block comments.
|
|
17
|
+
*
|
|
18
|
+
* The generated code mixes valid and invalid annotation formats to exercise
|
|
19
|
+
* parsing, multi-line handling, and error-reporting paths at scale without
|
|
20
|
+
* relying on auto-fix.
|
|
21
|
+
*/
|
|
22
|
+
function buildLargeAnnotatedSource(functionCount, annotationsPerFunction) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
for (let i = 0; i < functionCount; i += 1) {
|
|
25
|
+
// JSDoc-style block comment with multi-line @story/@req values.
|
|
26
|
+
lines.push("/**");
|
|
27
|
+
lines.push(" * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md");
|
|
28
|
+
lines.push(" * @req REQ-FORMAT-SPECIFICATION");
|
|
29
|
+
lines.push(" */");
|
|
30
|
+
// Additional line comments with a mix of valid and intentionally
|
|
31
|
+
// invalid formats (missing extensions, traversal, malformed IDs).
|
|
32
|
+
for (let j = 0; j < annotationsPerFunction; j += 1) {
|
|
33
|
+
const selector = (i + j) % 4;
|
|
34
|
+
if (selector === 0) {
|
|
35
|
+
lines.push("// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story");
|
|
36
|
+
}
|
|
37
|
+
else if (selector === 1) {
|
|
38
|
+
lines.push("// @req REQ-EXAMPLE-" + i.toString(10));
|
|
39
|
+
}
|
|
40
|
+
else if (selector === 2) {
|
|
41
|
+
lines.push("// @story ../outside-project.story.md");
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lines.push("// @req invalid-format-id");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
lines.push(`function annotated_fn_${i}() {`);
|
|
48
|
+
lines.push(' return "ok";\n}');
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
describe("valid-annotation-format performance on large annotated files (Story 005.0-DEV-ANNOTATION-VALIDATION)", () => {
|
|
53
|
+
const ruleName = "traceability/valid-annotation-format";
|
|
54
|
+
it("[REQ-MULTILINE-SUPPORT][REQ-FLEXIBLE-PARSING] analyzes a large annotated file within a generous time budget", () => {
|
|
55
|
+
const linter = new eslint_1.Linter({ configType: "eslintrc" });
|
|
56
|
+
linter.defineRule(ruleName, valid_annotation_format_1.default);
|
|
57
|
+
// 150 functions each with several annotations provides a substantial
|
|
58
|
+
// volume of comments and annotation patterns without being extreme.
|
|
59
|
+
const source = buildLargeAnnotatedSource(150, 3);
|
|
60
|
+
const start = perf_hooks_1.performance.now();
|
|
61
|
+
const messages = linter.verify(source, {
|
|
62
|
+
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
|
|
63
|
+
rules: {
|
|
64
|
+
[ruleName]: "error",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const durationMs = perf_hooks_1.performance.now() - start;
|
|
68
|
+
// Sanity check: we expect diagnostics for some invalid annotations so the
|
|
69
|
+
// rule is definitely executing its validation logic.
|
|
70
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
71
|
+
// Guardrail: keep analysis comfortably under ~5 seconds on CI hardware.
|
|
72
|
+
expect(durationMs).toBeLessThan(5000);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -59,6 +59,7 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
|
|
|
59
59
|
"valid-req-reference",
|
|
60
60
|
"prefer-implements-annotation",
|
|
61
61
|
"require-test-traceability",
|
|
62
|
+
"prefer-supports-annotation",
|
|
62
63
|
];
|
|
63
64
|
// Act: get actual rule names from plugin
|
|
64
65
|
const actual = Object.keys(index_1.rules);
|
|
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
/**
|
|
37
37
|
* Tests for: docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
|
|
38
38
|
* @story docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
|
|
39
|
-
* @supports docs/stories/001.0-DEV-PLUGIN-SETUP.story.md REQ-PLUGIN-STRUCTURE
|
|
39
|
+
* @supports docs/stories/001.0-DEV-PLUGIN-SETUP.story.md REQ-PLUGIN-STRUCTURE REQ-NPM-PACKAGE
|
|
40
40
|
*/
|
|
41
41
|
const index_1 = __importStar(require("../src/index"));
|
|
42
42
|
describe("Traceability ESLint Plugin (Story 001.0-DEV-PLUGIN-SETUP)", () => {
|
|
@@ -48,4 +48,15 @@ describe("Traceability ESLint Plugin (Story 001.0-DEV-PLUGIN-SETUP)", () => {
|
|
|
48
48
|
expect(index_1.default.rules).toBe(index_1.rules);
|
|
49
49
|
expect(index_1.default.configs).toBe(index_1.configs);
|
|
50
50
|
});
|
|
51
|
+
it("[REQ-PLUGIN-STRUCTURE][REQ-NPM-PACKAGE] plugin exposes meta with name, namespace, and version", () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const pkg = require("../package.json");
|
|
54
|
+
// Act
|
|
55
|
+
const meta = index_1.default.meta;
|
|
56
|
+
// Assert
|
|
57
|
+
expect(meta).toBeDefined();
|
|
58
|
+
expect(meta.name).toBe(pkg.name);
|
|
59
|
+
expect(meta.version).toBe(pkg.version);
|
|
60
|
+
expect(meta.namespace).toBe("traceability");
|
|
61
|
+
});
|
|
51
62
|
});
|