eslint-plugin-test-governance 0.3.1 → 0.5.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 +114 -1
- package/README.md +202 -83
- package/dist/index.d.ts +12 -11
- package/dist/index.js +28 -14
- package/dist/rules/marker-matches-state.d.ts +0 -1
- package/dist/rules/marker-matches-state.js +4 -4
- package/dist/rules/no-conflicting-states.d.ts +0 -1
- package/dist/rules/no-conflicting-states.js +17 -19
- package/dist/rules/no-orphaned-marker.d.ts +0 -1
- package/dist/rules/no-orphaned-marker.js +12 -7
- package/dist/rules/require-ticket-in-comments.d.ts +0 -1
- package/dist/rules/require-ticket-in-comments.js +8 -13
- package/dist/rules/require-ticket.d.ts +1 -2
- package/dist/rules/require-ticket.js +22 -10
- package/dist/utils/analyze.d.ts +29 -12
- package/dist/utils/analyze.js +526 -181
- package/dist/utils/constants.d.ts +5 -3
- package/dist/utils/constants.js +4 -3
- package/dist/utils/create-rule.d.ts +7 -8
- package/dist/utils/create-rule.js +3 -9
- package/dist/utils/distance.d.ts +0 -1
- package/dist/utils/distance.js +0 -1
- package/dist/utils/errors.d.ts +0 -1
- package/dist/utils/errors.js +0 -1
- package/dist/utils/fix.d.ts +0 -3
- package/dist/utils/fix.js +0 -8
- package/dist/utils/frameworks.d.ts +68 -0
- package/dist/utils/frameworks.js +108 -0
- package/dist/utils/options.d.ts +33 -12
- package/dist/utils/options.js +9 -5
- package/dist/utils/schema.d.ts +0 -1
- package/dist/utils/schema.js +9 -3
- package/dist/utils/tickets.d.ts +0 -1
- package/dist/utils/tickets.js +3 -3
- package/dist/utils/validate.d.ts +4 -1
- package/dist/utils/validate.js +9 -4
- package/docs/rules/marker-matches-state.md +19 -11
- package/docs/rules/no-conflicting-states.md +18 -10
- package/docs/rules/no-orphaned-marker.md +59 -18
- package/docs/rules/require-ticket-in-comments.md +27 -23
- package/docs/rules/require-ticket.md +135 -49
- package/docs/upgrading.md +131 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import noConflictingStates from "./rules/no-conflicting-states.js";
|
|
|
4
4
|
import noOrphanedMarker from "./rules/no-orphaned-marker.js";
|
|
5
5
|
import requireTicketInComments from "./rules/require-ticket-in-comments.js";
|
|
6
6
|
import requireTicket from "./rules/require-ticket.js";
|
|
7
|
+
import { FRAMEWORK_NAMES } from "./utils/constants.js";
|
|
8
|
+
import { ConfigError } from "./utils/errors.js";
|
|
7
9
|
import { compileOptions, SETTINGS_KEY } from "./utils/options.js";
|
|
8
10
|
const PLUGIN_NAME = 'test-governance';
|
|
9
11
|
// Read at load time so the reported version always matches the published package.
|
|
@@ -17,7 +19,8 @@ const ruleModules = {
|
|
|
17
19
|
};
|
|
18
20
|
// Public types come from `eslint` itself, so users don't need typescript-eslint installed.
|
|
19
21
|
export const rules = ruleModules;
|
|
20
|
-
|
|
22
|
+
// Every framework config turns on all the rules, at the severity in each rule's `meta.docs.recommended`.
|
|
23
|
+
const configRules = Object.fromEntries(Object.keys(ruleModules).map((name) => [
|
|
21
24
|
`${PLUGIN_NAME}/${name}`,
|
|
22
25
|
ruleModules[name].meta.docs.recommended,
|
|
23
26
|
]));
|
|
@@ -28,27 +31,38 @@ const plugin = {
|
|
|
28
31
|
configure,
|
|
29
32
|
};
|
|
30
33
|
/**
|
|
31
|
-
* Returns the
|
|
34
|
+
* Returns the config of `options.framework` with shared options in `settings['test-governance']`,
|
|
32
35
|
* so all rules read the same ticket formats and states.
|
|
33
36
|
*/
|
|
34
|
-
export function configure(options
|
|
37
|
+
export function configure(options) {
|
|
35
38
|
compileOptions(options); // fail on a bad config at load time, not on the first lint
|
|
36
39
|
return {
|
|
37
|
-
name: `${PLUGIN_NAME}
|
|
40
|
+
name: `${PLUGIN_NAME}/${options.framework}`,
|
|
38
41
|
plugins: { [PLUGIN_NAME]: plugin },
|
|
39
|
-
rules: { ...
|
|
42
|
+
rules: { ...configRules },
|
|
40
43
|
settings: { [SETTINGS_KEY]: options },
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Each sets its framework, so it wins over a `configure()` that applies to all files when listed after it.
|
|
47
|
+
for (const framework of FRAMEWORK_NAMES) {
|
|
48
|
+
plugin.configs[framework] = configure({ framework });
|
|
49
|
+
}
|
|
50
|
+
// `configs.recommended` was removed in 0.5.0. Reading it throws, so `...configs.recommended` fails with this
|
|
51
|
+
// message instead of spreading nothing and turning the rules off without a word. It isn't enumerable, so
|
|
52
|
+
// tools that list the configs don't see it.
|
|
53
|
+
Object.defineProperty(plugin.configs, 'recommended', {
|
|
54
|
+
get() {
|
|
55
|
+
throw new ConfigError('configs.recommended was removed in 0.5.0. Use the config for your framework: configs.playwright, configs.jest or configs.vitest. See https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md');
|
|
56
|
+
},
|
|
57
|
+
});
|
|
48
58
|
export const configs = plugin.configs;
|
|
49
59
|
export default plugin;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Makes `require('eslint-plugin-test-governance')` return the plugin itself rather than the module
|
|
62
|
+
* namespace. With the namespace, a CommonJS config that registers the plugin next to
|
|
63
|
+
* `configs.playwright` fails with "Cannot redefine plugin".
|
|
64
|
+
*
|
|
65
|
+
* @internal Left out of the type declarations: TypeScript before 5.6 can't parse a string export
|
|
66
|
+
* name, so it would reject the whole file.
|
|
67
|
+
*/
|
|
53
68
|
export { plugin as 'module.exports' };
|
|
54
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -2,14 +2,15 @@ import { appliesTo, describeSubject, isStateMarker, missingStates, strayMarkers
|
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
import { indentOf, keywordRange } from "../utils/fix.js";
|
|
4
4
|
export default createRule({
|
|
5
|
-
name: 'marker-matches-state',
|
|
6
5
|
meta: {
|
|
7
6
|
type: 'problem',
|
|
8
|
-
hasSuggestions: true,
|
|
9
7
|
docs: {
|
|
10
8
|
description: "Require the marker keyword to match the test's state",
|
|
11
9
|
recommended: 'error',
|
|
10
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/marker-matches-state.md',
|
|
12
11
|
},
|
|
12
|
+
hasSuggestions: true,
|
|
13
|
+
schema: [],
|
|
13
14
|
messages: {
|
|
14
15
|
wrongMarker: "`{{found}}:` doesn't match this code: {{subject}} is in the '{{state}}' state, so the marker should be `{{expected}}:`.",
|
|
15
16
|
sharedMarker: "`{{found}}:` covers only the '{{foundState}}' state. {{subject}} is also in the '{{state}}' state, which needs its own `// {{expected}}: <ticket>` marker.",
|
|
@@ -17,7 +18,7 @@ export default createRule({
|
|
|
17
18
|
addMarker: 'Add `// {{expected}}: {{tickets}}` with the same ticket.',
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
|
-
|
|
21
|
+
create(context, analysis, options) {
|
|
21
22
|
const { sourceCode } = context;
|
|
22
23
|
for (const subject of analysis.subjects) {
|
|
23
24
|
const missing = missingStates(subject);
|
|
@@ -78,4 +79,3 @@ export default createRule({
|
|
|
78
79
|
}
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
|
-
//# sourceMappingURL=marker-matches-state.js.map
|
|
@@ -2,16 +2,16 @@ import { headLoc } from "../utils/analyze.js";
|
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
import { isTagState } from "../utils/options.js";
|
|
4
4
|
import { editDistance, typoThreshold } from "../utils/distance.js";
|
|
5
|
-
import { tagRange } from "../utils/fix.js";
|
|
6
5
|
export default createRule({
|
|
7
|
-
name: 'no-conflicting-states',
|
|
8
6
|
meta: {
|
|
9
7
|
type: 'problem',
|
|
10
|
-
fixable: 'code',
|
|
11
8
|
docs: {
|
|
12
9
|
description: 'Disallow state tags that contradict each other, differ in case or look like typos',
|
|
13
10
|
recommended: 'warn',
|
|
11
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/no-conflicting-states.md',
|
|
14
12
|
},
|
|
13
|
+
fixable: 'code',
|
|
14
|
+
schema: [],
|
|
15
15
|
messages: {
|
|
16
16
|
newAndUnstable: '`{{new}}` and `{{unstable}}` are separate stages; a test should be in only one of them.',
|
|
17
17
|
newSkipped: "`{{new}}` on a skipped test: a new test that doesn't run can't be promoted. Fix the test or drop the tag.",
|
|
@@ -19,19 +19,13 @@ export default createRule({
|
|
|
19
19
|
tagTypo: '`{{found}}` looks like a typo of `{{expected}}`.',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
|
-
|
|
23
|
-
const { sourceCode } = context;
|
|
22
|
+
create(context, analysis, options) {
|
|
24
23
|
const tagStates = options.states.filter(isTagState);
|
|
25
24
|
const newTag = options.states.find((s) => s.name === 'new')?.tag;
|
|
26
25
|
const unstableTag = options.states.find((s) => s.name === 'unstable')?.tag;
|
|
27
26
|
for (const subject of analysis.subjects) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const key = `${occurrence.tag}\0${occurrence.node.range[0]}`;
|
|
31
|
-
const index = ownIndex.get(key) ?? 0;
|
|
32
|
-
ownIndex.set(key, index + 1);
|
|
33
|
-
checkSpelling(occurrence, index);
|
|
34
|
-
}
|
|
27
|
+
for (const occurrence of subject.tags)
|
|
28
|
+
checkSpelling(occurrence);
|
|
35
29
|
if (newTag === undefined)
|
|
36
30
|
continue;
|
|
37
31
|
const own = (tag) => subject.tags.find((t) => t.tag === tag);
|
|
@@ -55,19 +49,20 @@ export default createRule({
|
|
|
55
49
|
});
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
|
-
function checkSpelling(occurrence
|
|
59
|
-
const { tag } = occurrence;
|
|
52
|
+
function checkSpelling(occurrence) {
|
|
53
|
+
const { tag, range } = occurrence;
|
|
54
|
+
// A Vitest tag name is written without its `@`, so show it, and fix it, the same way.
|
|
55
|
+
const written = (name) => (occurrence.plain ? name.slice(1) : name);
|
|
60
56
|
if (tagStates.some((s) => s.tag === tag))
|
|
61
57
|
return;
|
|
62
58
|
const lower = tag.toLowerCase();
|
|
63
59
|
const sameLetters = tagStates.find((s) => s.tag.toLowerCase() === lower);
|
|
64
60
|
if (sameLetters) {
|
|
65
|
-
const range = tagRange(sourceCode, occurrence.node, tag, index);
|
|
66
61
|
context.report({
|
|
67
62
|
node: occurrence.node,
|
|
68
63
|
messageId: 'tagCase',
|
|
69
|
-
data: { found: tag, expected: sameLetters.tag },
|
|
70
|
-
fix: range && ((fixer) => fixer.replaceTextRange(range, sameLetters.tag)),
|
|
64
|
+
data: { found: written(tag), expected: written(sameLetters.tag) },
|
|
65
|
+
fix: range && ((fixer) => fixer.replaceTextRange(range, written(sameLetters.tag))),
|
|
71
66
|
});
|
|
72
67
|
return;
|
|
73
68
|
}
|
|
@@ -79,9 +74,12 @@ export default createRule({
|
|
|
79
74
|
editDistance(lower, expected) <= typoThreshold(expected.length - 1));
|
|
80
75
|
});
|
|
81
76
|
if (near) {
|
|
82
|
-
context.report({
|
|
77
|
+
context.report({
|
|
78
|
+
node: occurrence.node,
|
|
79
|
+
messageId: 'tagTypo',
|
|
80
|
+
data: { found: written(tag), expected: written(near.tag) },
|
|
81
|
+
});
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
},
|
|
86
85
|
});
|
|
87
|
-
//# sourceMappingURL=no-conflicting-states.js.map
|
|
@@ -2,28 +2,29 @@ import { describeSubject, isProse, missingStates, strayMarkers } from "../utils/
|
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
import { removeComment } from "../utils/fix.js";
|
|
4
4
|
export default createRule({
|
|
5
|
-
name: 'no-orphaned-marker',
|
|
6
5
|
meta: {
|
|
7
6
|
type: 'problem',
|
|
8
|
-
hasSuggestions: true,
|
|
9
7
|
docs: {
|
|
10
8
|
description: 'Disallow marker comments that no longer match a test state',
|
|
11
9
|
recommended: 'error',
|
|
10
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/no-orphaned-marker.md',
|
|
12
11
|
},
|
|
12
|
+
hasSuggestions: true,
|
|
13
|
+
schema: [],
|
|
13
14
|
messages: {
|
|
14
15
|
orphaned: "`{{marker}}:` is left over: {{subject}} is not in the '{{state}}' state.",
|
|
15
16
|
detached: '`{{marker}}:` is not directly above a test, describe or skip call, so it has no effect.',
|
|
16
17
|
removeMarker: 'Remove this comment.',
|
|
17
18
|
},
|
|
18
19
|
},
|
|
19
|
-
|
|
20
|
-
// Files without
|
|
20
|
+
create(context, analysis, options) {
|
|
21
|
+
// Files without tests, such as application code, have no markers to check.
|
|
21
22
|
if (!analysis.isTestFile)
|
|
22
23
|
return;
|
|
23
24
|
const { sourceCode } = context;
|
|
24
25
|
const markers = [];
|
|
25
26
|
for (const marker of analysis.detachedMarkers) {
|
|
26
|
-
// `// FIXME:
|
|
27
|
+
// `// FIXME: TRADE-12` above a helper is a tracked work comment, checked by require-ticket-in-comments.
|
|
27
28
|
if (options.workCommentKeywords.has(marker.keyword))
|
|
28
29
|
continue;
|
|
29
30
|
markers.push({ marker, messageId: 'detached', subject: '' });
|
|
@@ -33,8 +34,13 @@ export default createRule({
|
|
|
33
34
|
if (missingStates(subject).length > 0)
|
|
34
35
|
continue;
|
|
35
36
|
const name = describeSubject(subject).replace(/^This/, 'this');
|
|
36
|
-
for (const marker of strayMarkers(subject, options))
|
|
37
|
+
for (const marker of strayMarkers(subject, options)) {
|
|
38
|
+
// TODO is the everyday work-comment keyword, so `// TODO: TRADE-12` above a test that isn't a todo
|
|
39
|
+
// is a tracked note about it far more often than a todo test's forgotten marker.
|
|
40
|
+
if (marker.state.name === 'todo' && options.workCommentKeywords.has(marker.keyword.toUpperCase()))
|
|
41
|
+
continue;
|
|
37
42
|
markers.push({ marker, messageId: 'orphaned', subject: name });
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
for (const { marker, messageId, subject } of markers) {
|
|
40
46
|
// `// SKIP: this is flaky` is prose, not a leftover ticket.
|
|
@@ -49,4 +55,3 @@ export default createRule({
|
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
});
|
|
52
|
-
//# sourceMappingURL=no-orphaned-marker.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appliesTo, isStateMarker, parseTickets } from "../utils/analyze.js";
|
|
1
|
+
import { appliesTo, commentLines, isStateMarker, parseTickets } from "../utils/analyze.js";
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
import { compileTicketSpec } from "../utils/tickets.js";
|
|
4
4
|
/** Recognises anything shaped like a ticket, to tell a wrong-format ticket from plain prose. */
|
|
@@ -7,14 +7,15 @@ function escapeRegExp(text) {
|
|
|
7
7
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
8
|
}
|
|
9
9
|
export default createRule({
|
|
10
|
-
name: 'require-ticket-in-comments',
|
|
11
10
|
meta: {
|
|
12
11
|
type: 'suggestion',
|
|
13
|
-
hasSuggestions: true,
|
|
14
12
|
docs: {
|
|
15
13
|
description: 'Require FIXME and TODO comments in test files to start with a ticket',
|
|
16
14
|
recommended: 'error',
|
|
15
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/require-ticket-in-comments.md',
|
|
17
16
|
},
|
|
17
|
+
hasSuggestions: true,
|
|
18
|
+
schema: [],
|
|
18
19
|
messages: {
|
|
19
20
|
missingTicket: '`{{keyword}}` comments must start with a ticket, e.g. `// {{upper}}: {{example}}`. Put the details in the ticket.',
|
|
20
21
|
invalidTicket: "'{{ticket}}' is not a valid ticket. Expected {{expected}}.",
|
|
@@ -23,7 +24,7 @@ export default createRule({
|
|
|
23
24
|
removeExtraText: 'Remove `{{text}}`.',
|
|
24
25
|
},
|
|
25
26
|
},
|
|
26
|
-
|
|
27
|
+
create(context, analysis, options) {
|
|
27
28
|
const keywords = options.commentKeywords;
|
|
28
29
|
if (keywords.length === 0 || !analysis.isTestFile)
|
|
29
30
|
return;
|
|
@@ -39,12 +40,12 @@ export default createRule({
|
|
|
39
40
|
}
|
|
40
41
|
// `TODO`/`FIXME` in capitals counts with or without a colon. Other casings need the colon, so a
|
|
41
42
|
// sentence that happens to start with "Todo" is not mistaken for a work comment. The keyword can
|
|
42
|
-
// carry its ticket in parentheses, `TODO(
|
|
43
|
+
// carry its ticket in parentheses, `TODO(TRADE-123): ...`, a common style that tools also use for
|
|
43
44
|
// a name, `TODO(alice)`, which is not a ticket. Some teams put a space before the parenthesis.
|
|
44
45
|
const alternatives = keywords.map(escapeRegExp).join('|');
|
|
45
46
|
const keywordRe = new RegExp(`^(\\s*\\*?\\s*)(${alternatives})(?:\\s*\\(([^)]*)\\))?(?:\\s*:|(?=\\s|$))`, 'i');
|
|
46
47
|
/**
|
|
47
|
-
* `TODO(
|
|
48
|
+
* `TODO(TRADE-1, TRADE-2): text`: the tickets are in the parentheses, and anything after them, from the
|
|
48
49
|
* closing parenthesis on, is a note.
|
|
49
50
|
*/
|
|
50
51
|
function parseParenthesized(line, start, open, inParens) {
|
|
@@ -61,12 +62,7 @@ export default createRule({
|
|
|
61
62
|
for (const comment of sourceCode.getAllComments()) {
|
|
62
63
|
if (owned.has(comment))
|
|
63
64
|
continue;
|
|
64
|
-
|
|
65
|
-
let lineStart = comment.range[0] + 2;
|
|
66
|
-
for (const rawLine of comment.value.split('\n')) {
|
|
67
|
-
const line = rawLine.replace(/\r$/, '');
|
|
68
|
-
const start = lineStart;
|
|
69
|
-
lineStart += rawLine.length + 1;
|
|
65
|
+
for (const { text: line, start } of commentLines(comment)) {
|
|
70
66
|
const match = keywordRe.exec(line);
|
|
71
67
|
if (!match)
|
|
72
68
|
continue;
|
|
@@ -114,4 +110,3 @@ export default createRule({
|
|
|
114
110
|
}
|
|
115
111
|
},
|
|
116
112
|
});
|
|
117
|
-
//# sourceMappingURL=require-ticket-in-comments.js.map
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "missingMarker" | "markerCase" | "dynamicTitle">;
|
|
1
|
+
declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "missingMarker" | "markerCase" | "dynamicTitle" | "dynamicTags">;
|
|
2
2
|
export default _default;
|
|
3
|
-
//# sourceMappingURL=require-ticket.d.ts.map
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { appliesTo, describeSubject, evaluate, headLoc } from "../utils/analyze.js";
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
export default createRule({
|
|
4
|
-
name: 'require-ticket',
|
|
5
4
|
meta: {
|
|
6
5
|
type: 'problem',
|
|
7
|
-
hasSuggestions: true,
|
|
8
6
|
docs: {
|
|
9
7
|
description: 'Require a ticket marker comment above skipped, fixme and tagged tests',
|
|
10
8
|
recommended: 'error',
|
|
9
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/require-ticket.md',
|
|
11
10
|
},
|
|
11
|
+
hasSuggestions: true,
|
|
12
|
+
schema: [],
|
|
12
13
|
messages: {
|
|
13
14
|
missingMarker: "{{subject}} is in the '{{state}}' state but has no `// {{marker}}: <ticket>` comment directly above it, e.g. `// {{marker}}: {{example}}`.",
|
|
14
15
|
missingTicket: '`{{marker}}:` needs a ticket right after the colon, e.g. `// {{marker}}: {{example}}`.',
|
|
@@ -18,16 +19,20 @@ export default createRule({
|
|
|
18
19
|
extraText: 'Only ticket IDs are allowed after `{{marker}}:`, separated by commas. Put details in the ticket instead of `{{text}}`.',
|
|
19
20
|
removeExtraText: 'Remove `{{text}}`.',
|
|
20
21
|
dynamicTitle: "This title isn't static text, so its tags can't be checked. Use a string or template literal.",
|
|
22
|
+
dynamicTags: "These tags can't be read in this file, so they can't be checked. Use strings, or constants declared in this file.",
|
|
21
23
|
},
|
|
22
24
|
},
|
|
23
|
-
|
|
25
|
+
create(context, analysis, options) {
|
|
24
26
|
const hasTagStates = options.states.some((s) => s.tag !== undefined);
|
|
25
27
|
// A broken marker on a describe can be reached from several tests; report it once.
|
|
26
28
|
const reported = new Set();
|
|
27
29
|
const reportOnce = (key) => !reported.has(key) && Boolean(reported.add(key));
|
|
28
30
|
for (const subject of analysis.subjects) {
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
+
if (options.reportUnreadableTags && hasTagStates) {
|
|
32
|
+
if (subject.dynamicTitle)
|
|
33
|
+
context.report({ node: subject.node.arguments[0], messageId: 'dynamicTitle' });
|
|
34
|
+
for (const node of subject.unreadTags)
|
|
35
|
+
context.report({ node, messageId: 'dynamicTags' });
|
|
31
36
|
}
|
|
32
37
|
for (const { state, required } of subject.states) {
|
|
33
38
|
if (!required)
|
|
@@ -38,7 +43,6 @@ export default createRule({
|
|
|
38
43
|
!reportOnce(`${result.marker.comment.range[0]}:${state.name}`)) {
|
|
39
44
|
continue;
|
|
40
45
|
}
|
|
41
|
-
const data = { state: state.name, marker: state.marker, example: state.ticket.example };
|
|
42
46
|
switch (result.kind) {
|
|
43
47
|
case 'ok':
|
|
44
48
|
break;
|
|
@@ -46,7 +50,12 @@ export default createRule({
|
|
|
46
50
|
context.report({
|
|
47
51
|
loc: headLoc(subject),
|
|
48
52
|
messageId: 'missingMarker',
|
|
49
|
-
data: {
|
|
53
|
+
data: {
|
|
54
|
+
subject: describeSubject(subject),
|
|
55
|
+
state: state.name,
|
|
56
|
+
marker: state.marker,
|
|
57
|
+
example: state.ticket.example,
|
|
58
|
+
},
|
|
50
59
|
});
|
|
51
60
|
break;
|
|
52
61
|
case 'case':
|
|
@@ -57,13 +66,17 @@ export default createRule({
|
|
|
57
66
|
});
|
|
58
67
|
break;
|
|
59
68
|
case 'no-ticket':
|
|
60
|
-
context.report({
|
|
69
|
+
context.report({
|
|
70
|
+
loc: result.marker.comment.loc,
|
|
71
|
+
messageId: 'missingTicket',
|
|
72
|
+
data: { marker: state.marker, example: state.ticket.example },
|
|
73
|
+
});
|
|
61
74
|
break;
|
|
62
75
|
case 'bad-ticket':
|
|
63
76
|
context.report({
|
|
64
77
|
loc: result.marker.comment.loc,
|
|
65
78
|
messageId: result.ticket.result === 'placeholder' ? 'placeholderTicket' : 'invalidTicket',
|
|
66
|
-
data: {
|
|
79
|
+
data: { ticket: result.ticket.text, state: state.name, expected: state.ticket.expected },
|
|
67
80
|
});
|
|
68
81
|
break;
|
|
69
82
|
}
|
|
@@ -97,4 +110,3 @@ export default createRule({
|
|
|
97
110
|
}
|
|
98
111
|
},
|
|
99
112
|
});
|
|
100
|
-
//# sourceMappingURL=require-ticket.js.map
|
package/dist/utils/analyze.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TicketCheck, TicketMatcher } from './tickets.ts';
|
|
|
4
4
|
type Node = TSESTree.Node;
|
|
5
5
|
type Comment = TSESTree.Comment;
|
|
6
6
|
type SourceCode = Readonly<TSESLint.SourceCode>;
|
|
7
|
-
|
|
7
|
+
interface MarkerTicket {
|
|
8
8
|
text: string;
|
|
9
9
|
result: TicketCheck;
|
|
10
10
|
}
|
|
@@ -30,15 +30,23 @@ export type StateMarker = Marker & {
|
|
|
30
30
|
export declare function isStateMarker(marker: Marker): marker is StateMarker;
|
|
31
31
|
export interface TagOccurrence {
|
|
32
32
|
tag: string;
|
|
33
|
-
|
|
33
|
+
/** Where to report the tag: the text it is written in, or the name it comes from, as `Tags.NEW`. */
|
|
34
|
+
node: Node;
|
|
35
|
+
/**
|
|
36
|
+
* Where the tag is written in the source. Unset when it comes from a constant, or when an escape such
|
|
37
|
+
* as `\u002D` comes before the tag or inside it, since the source then no longer lines up with the title.
|
|
38
|
+
*/
|
|
39
|
+
range?: TSESTree.Range;
|
|
40
|
+
/** Set for a Vitest tag name such as `'flaky'` in `tags`, which is written without its `@`. */
|
|
41
|
+
plain?: true;
|
|
34
42
|
}
|
|
35
|
-
|
|
43
|
+
interface StateSource {
|
|
36
44
|
state: StateDef;
|
|
37
45
|
/** False for conditional skips unless `requireTicketForConditional` is set. */
|
|
38
46
|
required: boolean;
|
|
39
47
|
}
|
|
40
48
|
export interface Subject {
|
|
41
|
-
kind: 'test' | 'describe' | 'runtime';
|
|
49
|
+
kind: 'test' | 'describe' | 'step' | 'runtime';
|
|
42
50
|
node: TSESTree.CallExpression;
|
|
43
51
|
/** The node the marker comment block sits above. */
|
|
44
52
|
anchor: Node;
|
|
@@ -53,6 +61,13 @@ export interface Subject {
|
|
|
53
61
|
tags: TagOccurrence[];
|
|
54
62
|
/** Tags this node inherits from enclosing describes. */
|
|
55
63
|
inheritedTags: Set<string>;
|
|
64
|
+
/** Tag values, or a whole details object, on this node that can't be read, as `{ tag: TAGS.NEW }` with `TAGS` imported. */
|
|
65
|
+
unreadTags: Node[];
|
|
66
|
+
/**
|
|
67
|
+
* States this node may be in without the code showing it: tag states when tags can't be read, and
|
|
68
|
+
* Vitest's option states when its options object can't. Includes those of enclosing describes.
|
|
69
|
+
*/
|
|
70
|
+
unknownStates: Set<string>;
|
|
56
71
|
/** True when this node is skipped or fixme'd by any means, whether or not those states are on. */
|
|
57
72
|
skipped: boolean;
|
|
58
73
|
ownSkip: boolean;
|
|
@@ -63,7 +78,7 @@ export interface Analysis {
|
|
|
63
78
|
subjects: Subject[];
|
|
64
79
|
/** Markers that are not in the comment block of any test, describe or runtime call. */
|
|
65
80
|
detachedMarkers: StateMarker[];
|
|
66
|
-
/** Whether the file uses
|
|
81
|
+
/** Whether the file uses the test framework at all: an import from it or a call to a test function. */
|
|
67
82
|
isTestFile: boolean;
|
|
68
83
|
}
|
|
69
84
|
export type Evaluation = {
|
|
@@ -86,6 +101,11 @@ export type Evaluation = {
|
|
|
86
101
|
* of `text[0]`. Anything after the last ticket, including a trailing comma, is returned as `extra`.
|
|
87
102
|
*/
|
|
88
103
|
export declare function parseTickets(text: string, offset: number, matcher: TicketMatcher): Pick<Marker, 'tickets' | 'extra'>;
|
|
104
|
+
/** The lines of a comment and where each starts in the source, split at every line break JavaScript has. */
|
|
105
|
+
export declare function commentLines(comment: Comment): {
|
|
106
|
+
text: string;
|
|
107
|
+
start: number;
|
|
108
|
+
}[];
|
|
89
109
|
export declare function analyze(sourceCode: SourceCode, options: ResolvedOptions): Analysis;
|
|
90
110
|
/** Whether `subject` is in `stateName` at all, required or not, including inherited and runtime states. */
|
|
91
111
|
export declare function appliesTo(subject: Subject, stateName: string): boolean;
|
|
@@ -99,19 +119,16 @@ export declare function missingStates(subject: Subject): StateDef[];
|
|
|
99
119
|
*/
|
|
100
120
|
export declare function isProse(marker: Marker): boolean;
|
|
101
121
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
122
|
+
* Markers in the subject's own block for states that don't apply to it, leaving out work comments and
|
|
123
|
+
* states its code may hide, such as a tag from a constant imported from another file. A marker in the
|
|
124
|
+
* wrong case, as `// skip: TRADE-1`, counts when it starts with a valid ticket.
|
|
105
125
|
*/
|
|
106
|
-
export declare function isWorkComment(marker: Marker, options: ResolvedOptions): boolean;
|
|
107
|
-
/** Markers in the subject's own block for states that don't apply to it, leaving out work comments. */
|
|
108
126
|
export declare function strayMarkers(subject: Subject, options: ResolvedOptions): StateMarker[];
|
|
109
127
|
/**
|
|
110
128
|
* Where to report a problem with a test or describe: from the callee to the end of the title
|
|
111
|
-
* (`test.skip('
|
|
129
|
+
* (`test.skip('places a limit order'`), so editors underline the declaration rather than the whole body.
|
|
112
130
|
* Runtime calls such as `test.skip()` are short, so they are reported whole.
|
|
113
131
|
*/
|
|
114
132
|
export declare function headLoc(subject: Subject): TSESTree.SourceLocation;
|
|
115
133
|
export declare function describeSubject(subject: Subject): string;
|
|
116
134
|
export {};
|
|
117
|
-
//# sourceMappingURL=analyze.d.ts.map
|