eslint-plugin-test-governance 0.3.1 → 0.4.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 +62 -1
- package/README.md +166 -65
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -6
- 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 +8 -16
- 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 +0 -1
- package/dist/rules/require-ticket.js +16 -8
- package/dist/utils/analyze.d.ts +15 -12
- package/dist/utils/analyze.js +358 -167
- 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 +59 -0
- package/dist/utils/frameworks.js +102 -0
- package/dist/utils/options.d.ts +24 -9
- package/dist/utils/options.js +8 -4
- package/dist/utils/schema.d.ts +0 -1
- package/dist/utils/schema.js +2 -2
- package/dist/utils/tickets.d.ts +0 -1
- package/dist/utils/tickets.js +3 -3
- package/dist/utils/validate.d.ts +0 -1
- package/dist/utils/validate.js +2 -2
- package/docs/rules/marker-matches-state.md +18 -10
- package/docs/rules/no-conflicting-states.md +14 -8
- package/docs/rules/no-orphaned-marker.md +31 -17
- package/docs/rules/require-ticket-in-comments.md +26 -22
- package/docs/rules/require-ticket.md +107 -47
- package/package.json +5 -4
|
@@ -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,14 +49,13 @@ 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;
|
|
60
54
|
if (tagStates.some((s) => s.tag === tag))
|
|
61
55
|
return;
|
|
62
56
|
const lower = tag.toLowerCase();
|
|
63
57
|
const sameLetters = tagStates.find((s) => s.tag.toLowerCase() === lower);
|
|
64
58
|
if (sameLetters) {
|
|
65
|
-
const range = tagRange(sourceCode, occurrence.node, tag, index);
|
|
66
59
|
context.report({
|
|
67
60
|
node: occurrence.node,
|
|
68
61
|
messageId: 'tagCase',
|
|
@@ -84,4 +77,3 @@ export default createRule({
|
|
|
84
77
|
}
|
|
85
78
|
},
|
|
86
79
|
});
|
|
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))
|
|
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
1
|
declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "missingMarker" | "markerCase" | "dynamicTitle">;
|
|
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}}`.',
|
|
@@ -20,7 +21,7 @@ export default createRule({
|
|
|
20
21
|
dynamicTitle: "This title isn't static text, so its tags can't be checked. Use a string or template literal.",
|
|
21
22
|
},
|
|
22
23
|
},
|
|
23
|
-
|
|
24
|
+
create(context, analysis, options) {
|
|
24
25
|
const hasTagStates = options.states.some((s) => s.tag !== undefined);
|
|
25
26
|
// A broken marker on a describe can be reached from several tests; report it once.
|
|
26
27
|
const reported = new Set();
|
|
@@ -38,7 +39,6 @@ export default createRule({
|
|
|
38
39
|
!reportOnce(`${result.marker.comment.range[0]}:${state.name}`)) {
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
|
-
const data = { state: state.name, marker: state.marker, example: state.ticket.example };
|
|
42
42
|
switch (result.kind) {
|
|
43
43
|
case 'ok':
|
|
44
44
|
break;
|
|
@@ -46,7 +46,12 @@ export default createRule({
|
|
|
46
46
|
context.report({
|
|
47
47
|
loc: headLoc(subject),
|
|
48
48
|
messageId: 'missingMarker',
|
|
49
|
-
data: {
|
|
49
|
+
data: {
|
|
50
|
+
subject: describeSubject(subject),
|
|
51
|
+
state: state.name,
|
|
52
|
+
marker: state.marker,
|
|
53
|
+
example: state.ticket.example,
|
|
54
|
+
},
|
|
50
55
|
});
|
|
51
56
|
break;
|
|
52
57
|
case 'case':
|
|
@@ -57,13 +62,17 @@ export default createRule({
|
|
|
57
62
|
});
|
|
58
63
|
break;
|
|
59
64
|
case 'no-ticket':
|
|
60
|
-
context.report({
|
|
65
|
+
context.report({
|
|
66
|
+
loc: result.marker.comment.loc,
|
|
67
|
+
messageId: 'missingTicket',
|
|
68
|
+
data: { marker: state.marker, example: state.ticket.example },
|
|
69
|
+
});
|
|
61
70
|
break;
|
|
62
71
|
case 'bad-ticket':
|
|
63
72
|
context.report({
|
|
64
73
|
loc: result.marker.comment.loc,
|
|
65
74
|
messageId: result.ticket.result === 'placeholder' ? 'placeholderTicket' : 'invalidTicket',
|
|
66
|
-
data: {
|
|
75
|
+
data: { ticket: result.ticket.text, state: state.name, expected: state.ticket.expected },
|
|
67
76
|
});
|
|
68
77
|
break;
|
|
69
78
|
}
|
|
@@ -97,4 +106,3 @@ export default createRule({
|
|
|
97
106
|
}
|
|
98
107
|
},
|
|
99
108
|
});
|
|
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
|
}
|
|
@@ -31,14 +31,19 @@ export declare function isStateMarker(marker: Marker): marker is StateMarker;
|
|
|
31
31
|
export interface TagOccurrence {
|
|
32
32
|
tag: string;
|
|
33
33
|
node: TSESTree.Literal | TSESTree.TemplateLiteral;
|
|
34
|
+
/**
|
|
35
|
+
* Where the tag is written in the source. Unset when an escape such as `\u002D` comes before the
|
|
36
|
+
* tag or inside it, since the source then no longer lines up with the title.
|
|
37
|
+
*/
|
|
38
|
+
range?: TSESTree.Range;
|
|
34
39
|
}
|
|
35
|
-
|
|
40
|
+
interface StateSource {
|
|
36
41
|
state: StateDef;
|
|
37
42
|
/** False for conditional skips unless `requireTicketForConditional` is set. */
|
|
38
43
|
required: boolean;
|
|
39
44
|
}
|
|
40
45
|
export interface Subject {
|
|
41
|
-
kind: 'test' | 'describe' | 'runtime';
|
|
46
|
+
kind: 'test' | 'describe' | 'step' | 'runtime';
|
|
42
47
|
node: TSESTree.CallExpression;
|
|
43
48
|
/** The node the marker comment block sits above. */
|
|
44
49
|
anchor: Node;
|
|
@@ -63,7 +68,7 @@ export interface Analysis {
|
|
|
63
68
|
subjects: Subject[];
|
|
64
69
|
/** Markers that are not in the comment block of any test, describe or runtime call. */
|
|
65
70
|
detachedMarkers: StateMarker[];
|
|
66
|
-
/** Whether the file uses
|
|
71
|
+
/** Whether the file uses the test framework at all: an import from it or a call to a test function. */
|
|
67
72
|
isTestFile: boolean;
|
|
68
73
|
}
|
|
69
74
|
export type Evaluation = {
|
|
@@ -86,6 +91,11 @@ export type Evaluation = {
|
|
|
86
91
|
* of `text[0]`. Anything after the last ticket, including a trailing comma, is returned as `extra`.
|
|
87
92
|
*/
|
|
88
93
|
export declare function parseTickets(text: string, offset: number, matcher: TicketMatcher): Pick<Marker, 'tickets' | 'extra'>;
|
|
94
|
+
/** The lines of a comment and where each starts in the source, split at every line break JavaScript has. */
|
|
95
|
+
export declare function commentLines(comment: Comment): {
|
|
96
|
+
text: string;
|
|
97
|
+
start: number;
|
|
98
|
+
}[];
|
|
89
99
|
export declare function analyze(sourceCode: SourceCode, options: ResolvedOptions): Analysis;
|
|
90
100
|
/** Whether `subject` is in `stateName` at all, required or not, including inherited and runtime states. */
|
|
91
101
|
export declare function appliesTo(subject: Subject, stateName: string): boolean;
|
|
@@ -98,20 +108,13 @@ export declare function missingStates(subject: Subject): StateDef[];
|
|
|
98
108
|
* after it.
|
|
99
109
|
*/
|
|
100
110
|
export declare function isProse(marker: Marker): boolean;
|
|
101
|
-
/**
|
|
102
|
-
* Whether a marker is really an ordinary work comment such as `// FIXME: refactor this`: its keyword
|
|
103
|
-
* is also a work-comment keyword and it doesn't start with a valid ticket. `require-ticket-in-comments`
|
|
104
|
-
* reports those; the marker rules leave them alone.
|
|
105
|
-
*/
|
|
106
|
-
export declare function isWorkComment(marker: Marker, options: ResolvedOptions): boolean;
|
|
107
111
|
/** Markers in the subject's own block for states that don't apply to it, leaving out work comments. */
|
|
108
112
|
export declare function strayMarkers(subject: Subject, options: ResolvedOptions): StateMarker[];
|
|
109
113
|
/**
|
|
110
114
|
* Where to report a problem with a test or describe: from the callee to the end of the title
|
|
111
|
-
* (`test.skip('
|
|
115
|
+
* (`test.skip('places a limit order'`), so editors underline the declaration rather than the whole body.
|
|
112
116
|
* Runtime calls such as `test.skip()` are short, so they are reported whole.
|
|
113
117
|
*/
|
|
114
118
|
export declare function headLoc(subject: Subject): TSESTree.SourceLocation;
|
|
115
119
|
export declare function describeSubject(subject: Subject): string;
|
|
116
120
|
export {};
|
|
117
|
-
//# sourceMappingURL=analyze.d.ts.map
|