eslint-plugin-test-governance 0.13.0 → 0.13.2
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 +94 -1
- package/README.md +5 -4
- package/dist/index.js +23 -1
- package/dist/rules/marker-matches-state.js +8 -5
- package/dist/rules/no-conflicting-states.js +6 -0
- package/dist/rules/no-orphaned-marker.d.ts +1 -1
- package/dist/rules/no-orphaned-marker.js +11 -6
- package/dist/rules/require-ticket-in-comments.js +4 -4
- package/dist/rules/require-ticket.js +1 -1
- package/dist/utils/analyze.d.ts +11 -0
- package/dist/utils/analyze.js +214 -70
- package/dist/utils/create-rule.js +0 -1
- package/dist/utils/fix.d.ts +8 -5
- package/dist/utils/fix.js +35 -18
- package/dist/utils/options.d.ts +6 -4
- package/dist/utils/options.js +31 -10
- package/dist/utils/schema.d.ts +2 -0
- package/dist/utils/schema.js +1 -1
- package/dist/utils/tickets.d.ts +4 -0
- package/dist/utils/tickets.js +30 -19
- package/dist/utils/validate.js +1 -1
- package/docs/rules/no-orphaned-marker.md +5 -2
- package/docs/rules/require-ticket-for-commented-out-tests.md +2 -0
- package/docs/rules/require-ticket.md +9 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,97 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.13.2] - 2026-11-08
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Local aliases such as `const check = it; check.skip(...)`, and aliases of conditional test functions,
|
|
14
|
+
are checked like the functions they keep.
|
|
15
|
+
- A helper's parameter or block variable named like a test context no longer makes its `skip()` or
|
|
16
|
+
`test()` calls count as runtime skips or subtests.
|
|
17
|
+
- Runtime calls with a literal `false` condition, such as Playwright's `test.skip(false)`, need neither
|
|
18
|
+
a ticket nor a reason.
|
|
19
|
+
- Details, options and annotations use the last value of a property, including values from object
|
|
20
|
+
spreads. An unread spread or computed key no longer leaves an earlier value treated as certain.
|
|
21
|
+
- A tag read beyond an array's bounds no longer crashes linting. An index after a spread, or a string
|
|
22
|
+
such as `'01'` that isn't an array index, is left unread instead of using the wrong tag.
|
|
23
|
+
- Circular object spreads no longer overflow the stack during analysis.
|
|
24
|
+
- Playwright annotation arrays read tickets from spreads of arrays declared in the same file, and
|
|
25
|
+
report unread spreads with `reportUnreadableTags`.
|
|
26
|
+
- A ticket marker in a block comment no longer hides unrelated `TODO` or `FIXME` work comments on
|
|
27
|
+
other lines of that block.
|
|
28
|
+
- The `pattern` preset matches the whole ticket even with the multiline flag, and no longer accepts
|
|
29
|
+
trailing line breaks outside the pattern.
|
|
30
|
+
- Moving a ticket into an annotation preserves comments inside an empty details object.
|
|
31
|
+
- node:test's `todo` state no longer makes `no-conflicting-states` call a new test skipped: Node still
|
|
32
|
+
runs its body.
|
|
33
|
+
|
|
34
|
+
## [0.13.1] - 2026-11-06
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- The `rally` preset no longer takes a prefix with no number, such as `US` or `TC`, as a ticket, so
|
|
39
|
+
`// SKIP: TC` and, with `allowNotes`, a note that starts with "US" or "DE" need a real ticket.
|
|
40
|
+
- A second `configure()` for some of the same files, such as `{ preset: 'github' }` for a legacy folder after
|
|
41
|
+
`{ preset: 'jira', projects: ['TRADE'] }` for all files, replaces the earlier ticket format. ESLint merged the
|
|
42
|
+
two key by key, so linting failed with "ticket.projects is not an option of the github preset".
|
|
43
|
+
- The `github` preset reads Gitea's, Forgejo's and Codeberg's pull request links, `/<owner>/<repo>/pulls/12`.
|
|
44
|
+
- A `pattern` with an unmatched `)`, such as `TRADE-\d+)|(.*`, is a configuration error. It used to close the
|
|
45
|
+
group that anchors the pattern, so every ticket passed.
|
|
46
|
+
- `plane` checks its `projects` as `jira` does: a key must start with a letter and have two or more characters,
|
|
47
|
+
since no other key can match.
|
|
48
|
+
- An option named like a built-in object member, such as `customStates: { constructor: ... }`, is checked like
|
|
49
|
+
any other.
|
|
50
|
+
- A marker in an `eslint-disable` comment, as in `/* eslint-disable jest/no-disabled-tests -- SKIP: TRADE-1 */`,
|
|
51
|
+
covers every test up to the `eslint-enable`, also when the comment sits directly above a describe or a test that
|
|
52
|
+
isn't skipped. `no-orphaned-marker` called it left over there, and its suggestion removed the marker the
|
|
53
|
+
skipped tests below relied on. Such a marker is now reported only when none of the tests it covers is in its
|
|
54
|
+
state, and `require-ticket-in-comments` no longer reports its note a second time.
|
|
55
|
+
- `require-ticket-for-commented-out-tests` counts only a call that declares a test, describe or step. Everyday
|
|
56
|
+
lines such as `// test.slow();`, `// test.use({ ... })` and `// test.describe.configure({ ... })` were reported,
|
|
57
|
+
and in a comment block above a skipped test they took that test's marker, so `require-ticket` reported it too,
|
|
58
|
+
even with the commented-out rule off.
|
|
59
|
+
- A test function kept in a constant is read when a condition picks it, as
|
|
60
|
+
`const itUnlessWindows = isWindows ? it.skip : it`, when a call builds it, as `describe.runIf(hasDocker)`,
|
|
61
|
+
`it.skipIf(true)` or `it.skip.each(table)`, and when the constant is declared inside a describe. A marker above
|
|
62
|
+
such a test was reported as not directly above a test.
|
|
63
|
+
- With `requireReasonForConditional`, the description of an `eslint-disable` comment that covers a stretch of the
|
|
64
|
+
file no longer counts as the reason for every conditional skip in it. A reason written as a template literal
|
|
65
|
+
counts in Vitest's `context.skip(...)` and in node:test's `skip` option, and so does a comment at the end of a
|
|
66
|
+
one-line `test.skipIf(isMobile)(...)` or after an `if`'s opening line, as `if (isMobile) { // why`.
|
|
67
|
+
- Suggestions edit only the marker's own line: removing a leftover marker from a block comment that says more,
|
|
68
|
+
such as a doc comment, another marker or an ESLint directive's description, keeps the rest of the comment, and
|
|
69
|
+
renaming a marker no longer renames the same word in prose above it.
|
|
70
|
+
- With `annotations: { only: true }`, there is no suggestion to move a ticket into the details when it would break
|
|
71
|
+
the call: a Vitest test with a timeout or options after its function, or details with a computed key such as
|
|
72
|
+
`['annotation']`.
|
|
73
|
+
- The suggestion to add a marker uses the file's own line breaks, so it no longer leaves a bare line feed in a
|
|
74
|
+
file with CRLF line endings.
|
|
75
|
+
- Playwright's `test.describe.configure({ retries })` is read, with `states: { retry: true }`, when the options
|
|
76
|
+
sit in a constant or a spread, and when `describe` comes from `const { describe } = test`.
|
|
77
|
+
- A marker above `test.describe('orders', () => test.skip(...))`, whose arrow body is the test, belongs to the test
|
|
78
|
+
as well, so `no-orphaned-marker` no longer calls it left over on the describe.
|
|
79
|
+
- node:test's `t.skip(message)` and `t.todo(message)` and Jasmine's `pending(message)` take no condition, so they
|
|
80
|
+
need a ticket whatever the message is, also one held in a name or built in a template. A message in a name was
|
|
81
|
+
read as a condition.
|
|
82
|
+
- A tag constant used in several titles, as `` `buys ${TAG}` ``, is reported once, where it is written.
|
|
83
|
+
- The example in the `missingReason` message is written on one line, also when the call is split over lines.
|
|
84
|
+
- With `requireReasonForConditional`, a comment above the declaration of a constant that picks a test function,
|
|
85
|
+
as `const posixTest = isWindows ? test.skip : test`, or at the end of its line, is the reason for every test
|
|
86
|
+
that uses it.
|
|
87
|
+
- A link whose `id` query is zero, such as `https://www.wrike.com/open.htm?id=0`, is a placeholder, as any ticket
|
|
88
|
+
numbered 0 is. Other zero queries, such as `?page=0`, are left alone.
|
|
89
|
+
- A placeholder written with a leading `#`, such as `#TBD`, also matches `TBD`: a leading `#` is ignored on both
|
|
90
|
+
the ticket and the placeholder.
|
|
91
|
+
- A link whose scheme isn't in lowercase, such as `HTTPS://...`, is read as a link.
|
|
92
|
+
- Linear, Trello, Shortcut and YouTrack links that end in a slash after the title, as
|
|
93
|
+
`.../ENG-1/fix-rounding/`, are accepted.
|
|
94
|
+
- A lowercase Jira or Plane project in `projects` is reported once instead of twice.
|
|
95
|
+
- `numeric` with a `minLength` above its `maxLength`, and a `pattern` or `flags` that isn't a valid regex, are
|
|
96
|
+
reported in the list of invalid options. Without `maxLength`, the message names its default of 20.
|
|
97
|
+
- The ticket format of a state that is off, or that the framework doesn't have, such as `states.fixme.ticket`
|
|
98
|
+
with Jest, is checked too, so a mistake in it shows up at once.
|
|
99
|
+
|
|
9
100
|
## [0.13.0] - 2026-11-05
|
|
10
101
|
|
|
11
102
|
### Added
|
|
@@ -507,7 +598,9 @@ First release.
|
|
|
507
598
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
508
599
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
509
600
|
|
|
510
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.
|
|
601
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.2...HEAD
|
|
602
|
+
[0.13.2]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.1...v0.13.2
|
|
603
|
+
[0.13.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.0...v0.13.1
|
|
511
604
|
[0.13.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.12.1...v0.13.0
|
|
512
605
|
[0.12.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.12.0...v0.12.1
|
|
513
606
|
[0.12.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.11.1...v0.12.0
|
package/README.md
CHANGED
|
@@ -242,7 +242,7 @@ and markers. To turn a rule off, set it to `'off'` as usual.
|
|
|
242
242
|
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
243
243
|
| `framework` | `'playwright' \| 'jest' \| 'vitest' \| 'mocha' \| 'cypress' \| 'webdriverio' \| 'node-test' \| 'jasmine'` | required | The test framework of the files the config applies to. Each framework's config, such as `configs.playwright`, sets it, and `configure()` needs it. See [Frameworks](#frameworks). |
|
|
244
244
|
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
|
|
245
|
-
| `placeholders` | `string[]` | see above | Tickets rejected as placeholders. `*` matches any characters. Case-insensitive
|
|
245
|
+
| `placeholders` | `string[]` | see above | Tickets rejected as placeholders. `*` matches any characters. Case-insensitive, and a leading `#` on the ticket or the placeholder is ignored. Your list replaces the default one, so repeat the defaults you want to keep. Tickets numbered 0 are always rejected. |
|
|
246
246
|
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states. |
|
|
247
247
|
| `states` | `{ [state]: boolean \| { enabled?, tag?, marker?, ticket? } }` | `skip`, `fixme` and `todo` on | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `todo`, `retry`, `new`, `quarantined`) on or off, renames their tag or marker, or gives them their own ticket format. `fail`, `slow` and `retry` are off by default; a setting for `new` or `quarantined` here wins over `lifecycleTags`. Only `new` and `quarantined` have a tag to rename; see [Tag names](#tag-names). `fixme` and `slow` exist only in Playwright, `todo` in every framework but Playwright, `fail` in Playwright, Jest, Vitest and node:test, and `retry` in every framework but node:test and Jasmine, so turning one on elsewhere has no effect. |
|
|
248
248
|
| `customStates` | `{ [name]: { when, marker, ticket? } }` | `{}` | Adds a state for a tag, e.g. `{ 'needs-data': { when: '@needs-data', marker: 'NEEDS-DATA' } }`. `when` is a tag and `marker` an uppercase keyword; neither can be shared with another state. |
|
|
@@ -253,7 +253,7 @@ and markers. To turn a rule off, set it to `'off'` as usual.
|
|
|
253
253
|
| `allowNotes` | `boolean` | `false` | Allow free text after the tickets, e.g. `// SKIP: TRADE-123 flaky on CI`. Applies to markers and to `FIXME` / `TODO` comments. |
|
|
254
254
|
| `comments` | `false \| { keywords?: string[] }` | `{ keywords: ['FIXME', 'TODO'] }` | Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: TRADE-123`. Your keywords replace the default ones. `false` allows free-form comments. |
|
|
255
255
|
| `annotations` | `false \| { type?: string, only?: boolean }` | `{ type: 'issue' }` | Accept the ticket from a test annotation as well: Playwright's `annotation` of the given type or Vitest's `meta` key of that name. One valid annotation covers every state the test is in, and a describe's covers its tests. `only: true` makes it the only place for a test's or describe's ticket. See [Tickets from annotations](#tickets-from-annotations). |
|
|
256
|
-
| `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and
|
|
256
|
+
| `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest, Vitest and node:test default to `['test', 'it']`, Mocha, Cypress and WebdriverIO to `['it', 'specify', 'test']`, and Jasmine to `['it']`. Your list replaces the default one, so include those names if you still need them. See below. |
|
|
257
257
|
| `reportUnreadableTags` | `boolean` | `false` | When a tag state is on, report titles that aren't static text, and tags or details that can't be read in the file, such as ones imported from another module, since their tags can't be checked. |
|
|
258
258
|
|
|
259
259
|
### Tag names
|
|
@@ -330,8 +330,9 @@ run on their own servers.
|
|
|
330
330
|
|
|
331
331
|
For a tracker without a preset, use `url` with its host, for example
|
|
332
332
|
`{ preset: 'url', hosts: ['www.wrike.com'] }` for Wrike links such as
|
|
333
|
-
`https://www.wrike.com/open.htm?id=4821
|
|
334
|
-
|
|
333
|
+
`https://www.wrike.com/open.htm?id=4821`; a link with `?id=0`, or a path that ends in `/0`, is a
|
|
334
|
+
placeholder. It works the same for Microsoft Planner (`tasks.office.com`), Airtable (`airtable.com`),
|
|
335
|
+
Smartsheet (`app.smartsheet.com`) and trackers of your own. For another
|
|
335
336
|
tracker's keys, such as `WEB-12`, `any` or `pattern` works. Gitea, Forgejo and Codeberg use GitHub's link layout,
|
|
336
337
|
so the `github` preset with their `host` covers them. Bitbucket has no issue tracker since August 2026,
|
|
337
338
|
so it has no preset.
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,28 @@ const plugin = {
|
|
|
32
32
|
configs: {},
|
|
33
33
|
configure,
|
|
34
34
|
};
|
|
35
|
+
/** `ticket` as a list, so ESLint replaces it whole when it merges two configs' settings. */
|
|
36
|
+
function ticketAsList(options) {
|
|
37
|
+
const { ticket } = options;
|
|
38
|
+
return ticket === undefined || Array.isArray(ticket) ? options : { ...options, ticket: [ticket] };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* ESLint merges the settings of configs that apply to the same file key by key and replaces arrays. Merged
|
|
42
|
+
* key by key, a later config's `{ preset: 'github' }` would keep an earlier `projects` that github doesn't take,
|
|
43
|
+
* so every ticket format goes in a list.
|
|
44
|
+
*/
|
|
45
|
+
function ticketsAsLists(options) {
|
|
46
|
+
const { states, customStates } = options;
|
|
47
|
+
return {
|
|
48
|
+
...ticketAsList(options),
|
|
49
|
+
...(states && {
|
|
50
|
+
states: Object.fromEntries(Object.entries(states).map(([name, state]) => [name, typeof state === 'object' ? ticketAsList(state) : state])),
|
|
51
|
+
}),
|
|
52
|
+
...(customStates && {
|
|
53
|
+
customStates: Object.fromEntries(Object.entries(customStates).map(([name, state]) => [name, ticketAsList(state)])),
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* Returns the config of `options.framework` with shared options in `settings['test-governance']`,
|
|
37
59
|
* so all rules read the same ticket formats and states.
|
|
@@ -42,7 +64,7 @@ export function configure(options) {
|
|
|
42
64
|
name: `${PLUGIN_NAME}/${options.framework}`,
|
|
43
65
|
plugins: { [PLUGIN_NAME]: plugin },
|
|
44
66
|
rules: { ...configRules },
|
|
45
|
-
settings: { [SETTINGS_KEY]: options },
|
|
67
|
+
settings: { [SETTINGS_KEY]: ticketsAsLists(options) },
|
|
46
68
|
};
|
|
47
69
|
}
|
|
48
70
|
// Each sets its framework, so it wins over a `configure()` that applies to all files when listed after it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appliesTo, describeSubject, directiveOf, isStateMarker, missingStates, strayMarkers, } from "../utils/analyze.js";
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
|
-
import { indentOf, keywordRange } from "../utils/fix.js";
|
|
3
|
+
import { indentOf, keywordRange, lineBreakAfter } from "../utils/fix.js";
|
|
4
4
|
export default createRule({
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'problem',
|
|
@@ -39,7 +39,7 @@ export default createRule({
|
|
|
39
39
|
suggest: missing.map((state) => ({
|
|
40
40
|
messageId: 'renameMarker',
|
|
41
41
|
data: { expected: state.marker },
|
|
42
|
-
fix: (fixer) => fixer.replaceTextRange(keywordRange(
|
|
42
|
+
fix: (fixer) => fixer.replaceTextRange(keywordRange(marker), state.marker),
|
|
43
43
|
})),
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -75,9 +75,12 @@ export default createRule({
|
|
|
75
75
|
{
|
|
76
76
|
messageId: 'addMarker',
|
|
77
77
|
data: { expected: state.marker, tickets },
|
|
78
|
-
fix: (fixer) =>
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
fix: (fixer) => {
|
|
79
|
+
const lineBreak = lineBreakAfter(sourceCode, present.comment);
|
|
80
|
+
return directive
|
|
81
|
+
? fixer.insertTextBeforeRange(present.comment.range, `${added}${lineBreak}${indent}`)
|
|
82
|
+
: fixer.insertTextAfterRange(present.comment.range, `${lineBreak}${indent}${added}`);
|
|
83
|
+
},
|
|
81
84
|
},
|
|
82
85
|
]
|
|
83
86
|
: [],
|
|
@@ -24,6 +24,7 @@ export default createRule({
|
|
|
24
24
|
const checkedTags = [...options.states.filter(isTagState).map((s) => s.tag), ...options.knownTags];
|
|
25
25
|
const newTag = options.states.find((s) => s.name === 'new')?.tag;
|
|
26
26
|
const quarantinedTag = options.states.find((s) => s.name === 'quarantined')?.tag;
|
|
27
|
+
const spelled = new Set();
|
|
27
28
|
for (const subject of analysis.subjects) {
|
|
28
29
|
for (const occurrence of subject.tags)
|
|
29
30
|
checkSpelling(occurrence);
|
|
@@ -52,6 +53,11 @@ export default createRule({
|
|
|
52
53
|
}
|
|
53
54
|
function checkSpelling(occurrence) {
|
|
54
55
|
const { tag, range } = occurrence;
|
|
56
|
+
// A constant used in several titles, as `` `buys ${TAG}` ``, is one place to fix, so one report.
|
|
57
|
+
const key = `${occurrence.node.range[0]}:${range?.[0]}:${tag}`;
|
|
58
|
+
if (spelled.has(key))
|
|
59
|
+
return;
|
|
60
|
+
spelled.add(key);
|
|
55
61
|
// A Vitest tag name is written without its `@`, so show it, and fix it, the same way.
|
|
56
62
|
const written = (name) => (occurrence.plain ? name.slice(1) : name);
|
|
57
63
|
if (checkedTags.includes(tag))
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("../utils/create-rule.ts").Rule<"orphaned" | "detached" | "removeMarker">;
|
|
1
|
+
declare const _default: import("../utils/create-rule.ts").Rule<"orphaned" | "orphanedRegion" | "detached" | "removeMarker">;
|
|
2
2
|
export default _default;
|
|
@@ -13,6 +13,7 @@ export default createRule({
|
|
|
13
13
|
schema: [],
|
|
14
14
|
messages: {
|
|
15
15
|
orphaned: "`{{marker}}:` is left over: {{subject}} is not in the '{{state}}' state.",
|
|
16
|
+
orphanedRegion: "`{{marker}}:` is left over: no test up to the `eslint-enable` of this comment is in the '{{state}}' state.",
|
|
16
17
|
detached: '`{{marker}}:` is not directly above a test, describe or skip call, so it has no effect.',
|
|
17
18
|
removeMarker: 'Remove this comment.',
|
|
18
19
|
},
|
|
@@ -29,17 +30,21 @@ export default createRule({
|
|
|
29
30
|
continue;
|
|
30
31
|
markers.push({ marker, messageId: 'detached', subject: '' });
|
|
31
32
|
}
|
|
33
|
+
// TODO is the everyday work-comment keyword, so `// TODO: TRADE-12` above a test that isn't a todo
|
|
34
|
+
// is a tracked note about it far more often than a todo test's forgotten marker.
|
|
35
|
+
const trackedNote = (marker) => marker.state.name === 'todo' && options.workCommentKeywords.has(marker.keyword.toUpperCase());
|
|
36
|
+
for (const marker of analysis.idleRegionMarkers) {
|
|
37
|
+
if (!trackedNote(marker))
|
|
38
|
+
markers.push({ marker, messageId: 'orphanedRegion', subject: '' });
|
|
39
|
+
}
|
|
32
40
|
for (const subject of analysis.subjects) {
|
|
33
41
|
// A stray marker next to a missing one is a mismatch, reported by marker-matches-state.
|
|
34
42
|
if (missingStates(subject, options).length > 0)
|
|
35
43
|
continue;
|
|
36
44
|
const name = describeSubject(subject).replace(/^This/, 'this');
|
|
37
45
|
for (const marker of strayMarkers(subject, options)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (marker.state.name === 'todo' && options.workCommentKeywords.has(marker.keyword.toUpperCase()))
|
|
41
|
-
continue;
|
|
42
|
-
markers.push({ marker, messageId: 'orphaned', subject: name });
|
|
46
|
+
if (!trackedNote(marker))
|
|
47
|
+
markers.push({ marker, messageId: 'orphaned', subject: name });
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
for (const { marker, messageId, subject } of markers) {
|
|
@@ -50,7 +55,7 @@ export default createRule({
|
|
|
50
55
|
loc: marker.comment.loc,
|
|
51
56
|
messageId,
|
|
52
57
|
data: { marker: marker.keyword, state: marker.state.name, subject },
|
|
53
|
-
suggest: [{ messageId: 'removeMarker', fix: () => removeMarkerComment(sourceCode, marker
|
|
58
|
+
suggest: [{ messageId: 'removeMarker', fix: () => removeMarkerComment(sourceCode, marker) }],
|
|
54
59
|
});
|
|
55
60
|
}
|
|
56
61
|
},
|
|
@@ -34,10 +34,10 @@ export default createRule({
|
|
|
34
34
|
// in the wrong case.
|
|
35
35
|
const owned = new Set();
|
|
36
36
|
for (const subject of analysis.subjects) {
|
|
37
|
-
for (const marker of subject.markers) {
|
|
37
|
+
for (const marker of [...subject.markers, ...subject.regionMarkers]) {
|
|
38
38
|
const state = marker.state ?? marker.caseOf;
|
|
39
39
|
if (state && appliesTo(subject, state.name))
|
|
40
|
-
owned.add(marker.
|
|
40
|
+
owned.add(marker.keywordStart);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
// `TODO`/`FIXME` in capitals counts with or without a colon. Other casings need the colon, so a
|
|
@@ -62,13 +62,13 @@ export default createRule({
|
|
|
62
62
|
return { ...parsed, extra: { text: note, range: [start + close, start + line.trimEnd().length] } };
|
|
63
63
|
}
|
|
64
64
|
for (const comment of sourceCode.getAllComments()) {
|
|
65
|
-
if (owned.has(comment))
|
|
66
|
-
continue;
|
|
67
65
|
for (const { text: line, start } of commentLines(comment)) {
|
|
68
66
|
const match = keywordRe.exec(line);
|
|
69
67
|
if (!match)
|
|
70
68
|
continue;
|
|
71
69
|
const [whole, indent, keyword] = match;
|
|
70
|
+
if (owned.has(start + indent.length))
|
|
71
|
+
continue;
|
|
72
72
|
// The parenthesised group is optional, so it can be undefined despite RegExpExecArray's typing.
|
|
73
73
|
const inParens = match[3];
|
|
74
74
|
const upper = keyword.toUpperCase();
|
|
@@ -70,7 +70,7 @@ export default createRule({
|
|
|
70
70
|
const reportReason = (subject, state) => {
|
|
71
71
|
if (evaluate(subject, state).kind === 'ok' || hasAnnotationTicket(subject, state, options))
|
|
72
72
|
return;
|
|
73
|
-
const callee = context.sourceCode.getText(subject.node.callee);
|
|
73
|
+
const callee = context.sourceCode.getText(subject.node.callee).replace(/\s+/g, '');
|
|
74
74
|
const first = subject.node.arguments.at(0);
|
|
75
75
|
const where = options.framework.reasonArgument;
|
|
76
76
|
// Vitest's `context.skip()` takes either a reason alone or a condition and then the reason.
|
package/dist/utils/analyze.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface Marker {
|
|
|
22
22
|
text: string;
|
|
23
23
|
range: TSESTree.Range;
|
|
24
24
|
};
|
|
25
|
+
/** The index of the marker's line in `commentLines(comment)`. */
|
|
26
|
+
line: number;
|
|
27
|
+
/** Where the keyword starts in the source. */
|
|
28
|
+
keywordStart: number;
|
|
25
29
|
}
|
|
26
30
|
/** A marker whose keyword is exactly a state's marker. */
|
|
27
31
|
export type StateMarker = Marker & {
|
|
@@ -87,6 +91,11 @@ export interface Subject {
|
|
|
87
91
|
* Vitest's option states when its options object can't. Includes those of enclosing describes.
|
|
88
92
|
*/
|
|
89
93
|
unknownStates: Set<string>;
|
|
94
|
+
/**
|
|
95
|
+
* States of other subjects whose comment block is this one's, as a test that is the arrow body of a
|
|
96
|
+
* describe, `describe('d', () => test.skip(...))`, so a marker for them isn't left over here.
|
|
97
|
+
*/
|
|
98
|
+
sharedStates: Set<string>;
|
|
90
99
|
/** True when this node is skipped or fixme'd by any means, whether or not those states are on. */
|
|
91
100
|
skipped: boolean;
|
|
92
101
|
ownSkip: boolean;
|
|
@@ -115,6 +124,8 @@ export interface Analysis {
|
|
|
115
124
|
subjects: Subject[];
|
|
116
125
|
/** Markers that are not in the comment block of any test, describe or runtime call. */
|
|
117
126
|
detachedMarkers: StateMarker[];
|
|
127
|
+
/** Markers of `eslint-disable` regions that cover tests, none of them in the marker's state. */
|
|
128
|
+
idleRegionMarkers: StateMarker[];
|
|
118
129
|
/** Commented-out tests; their comments are left out of the comment blocks of the tests around them. */
|
|
119
130
|
commentedOutTests: CommentedOutTest[];
|
|
120
131
|
/** Whether the file uses the test framework at all: an import from it or a call to a test function. */
|