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.
Files changed (43) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/README.md +202 -83
  3. package/dist/index.d.ts +12 -11
  4. package/dist/index.js +28 -14
  5. package/dist/rules/marker-matches-state.d.ts +0 -1
  6. package/dist/rules/marker-matches-state.js +4 -4
  7. package/dist/rules/no-conflicting-states.d.ts +0 -1
  8. package/dist/rules/no-conflicting-states.js +17 -19
  9. package/dist/rules/no-orphaned-marker.d.ts +0 -1
  10. package/dist/rules/no-orphaned-marker.js +12 -7
  11. package/dist/rules/require-ticket-in-comments.d.ts +0 -1
  12. package/dist/rules/require-ticket-in-comments.js +8 -13
  13. package/dist/rules/require-ticket.d.ts +1 -2
  14. package/dist/rules/require-ticket.js +22 -10
  15. package/dist/utils/analyze.d.ts +29 -12
  16. package/dist/utils/analyze.js +526 -181
  17. package/dist/utils/constants.d.ts +5 -3
  18. package/dist/utils/constants.js +4 -3
  19. package/dist/utils/create-rule.d.ts +7 -8
  20. package/dist/utils/create-rule.js +3 -9
  21. package/dist/utils/distance.d.ts +0 -1
  22. package/dist/utils/distance.js +0 -1
  23. package/dist/utils/errors.d.ts +0 -1
  24. package/dist/utils/errors.js +0 -1
  25. package/dist/utils/fix.d.ts +0 -3
  26. package/dist/utils/fix.js +0 -8
  27. package/dist/utils/frameworks.d.ts +68 -0
  28. package/dist/utils/frameworks.js +108 -0
  29. package/dist/utils/options.d.ts +33 -12
  30. package/dist/utils/options.js +9 -5
  31. package/dist/utils/schema.d.ts +0 -1
  32. package/dist/utils/schema.js +9 -3
  33. package/dist/utils/tickets.d.ts +0 -1
  34. package/dist/utils/tickets.js +3 -3
  35. package/dist/utils/validate.d.ts +4 -1
  36. package/dist/utils/validate.js +9 -4
  37. package/docs/rules/marker-matches-state.md +19 -11
  38. package/docs/rules/no-conflicting-states.md +18 -10
  39. package/docs/rules/no-orphaned-marker.md +59 -18
  40. package/docs/rules/require-ticket-in-comments.md +27 -23
  41. package/docs/rules/require-ticket.md +135 -49
  42. package/docs/upgrading.md +131 -0
  43. 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
- const recommendedRules = Object.fromEntries(Object.keys(ruleModules).map((name) => [
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 recommended config with shared options in `settings['test-governance']`,
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}/recommended`,
40
+ name: `${PLUGIN_NAME}/${options.framework}`,
38
41
  plugins: { [PLUGIN_NAME]: plugin },
39
- rules: { ...recommendedRules },
42
+ rules: { ...configRules },
40
43
  settings: { [SETTINGS_KEY]: options },
41
44
  };
42
45
  }
43
- plugin.configs.recommended = {
44
- name: `${PLUGIN_NAME}/recommended`,
45
- plugins: { [PLUGIN_NAME]: plugin },
46
- rules: { ...recommendedRules },
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
- // Makes `require('eslint-plugin-test-governance')` return the plugin itself rather than the module
51
- // namespace. With the namespace, a CommonJS config that registers the plugin next to
52
- // `configs.recommended` fails with "Cannot redefine plugin".
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
@@ -1,3 +1,2 @@
1
1
  declare const _default: import("../utils/create-rule.ts").Rule<"wrongMarker" | "sharedMarker" | "renameMarker" | "addMarker">;
2
2
  export default _default;
3
- //# sourceMappingURL=marker-matches-state.d.ts.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
- check(context, analysis, options) {
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
@@ -1,3 +1,2 @@
1
1
  declare const _default: import("../utils/create-rule.ts").Rule<"newAndUnstable" | "newSkipped" | "tagCase" | "tagTypo">;
2
2
  export default _default;
3
- //# sourceMappingURL=no-conflicting-states.d.ts.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
- check(context, analysis, options) {
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 ownIndex = new Map();
29
- for (const occurrence of subject.tags) {
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, index) {
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({ node: occurrence.node, messageId: 'tagTypo', data: { found: tag, expected: near.tag } });
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
@@ -1,3 +1,2 @@
1
1
  declare const _default: import("../utils/create-rule.ts").Rule<"orphaned" | "detached" | "removeMarker">;
2
2
  export default _default;
3
- //# sourceMappingURL=no-orphaned-marker.d.ts.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
- check(context, analysis, options) {
20
- // Files without Playwright tests, such as application code, have no markers to check.
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: WEB-12` above a helper is a tracked work comment, checked by require-ticket-in-comments.
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,3 +1,2 @@
1
1
  declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText">;
2
2
  export default _default;
3
- //# sourceMappingURL=require-ticket-in-comments.d.ts.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
- check(context, analysis, options) {
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(WEB-123): ...`, a common style that tools also use for
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(WEB-1, WEB-2): text`: the tickets are in the parentheses, and anything after them, from the
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
- // `//` and `/*` are both two characters, so the comment's value starts two characters in.
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
- check(context, analysis, options) {
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 (subject.dynamicTitle && options.reportDynamicTitles && hasTagStates) {
30
- context.report({ node: subject.node.arguments[0], messageId: 'dynamicTitle' });
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: { ...data, subject: describeSubject(subject) },
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({ loc: result.marker.comment.loc, messageId: 'missingTicket', data });
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: { ...data, ticket: result.ticket.text, expected: state.ticket.expected },
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
@@ -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
- export interface MarkerTicket {
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
- node: TSESTree.Literal | TSESTree.TemplateLiteral;
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
- export interface StateSource {
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 Playwright at all: a Playwright import or a call to a test function. */
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
- * 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.
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('pays with PayPal'`), so editors underline the declaration rather than the whole body.
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