@cjser/eslint-node-test 0.2.0-cjser.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.
Files changed (128) hide show
  1. package/dist-cjser/index.cjs +8561 -0
  2. package/index.d.ts +11 -0
  3. package/index.js +49 -0
  4. package/license +9 -0
  5. package/package.json +141 -0
  6. package/readme.md +172 -0
  7. package/rules/assertion-arguments.js +147 -0
  8. package/rules/ast/call-or-new-expression.js +94 -0
  9. package/rules/ast/function-types.js +7 -0
  10. package/rules/ast/index.js +17 -0
  11. package/rules/ast/is-expression-statement.js +7 -0
  12. package/rules/ast/is-function.js +5 -0
  13. package/rules/ast/is-loop.js +5 -0
  14. package/rules/ast/is-member-expression.js +92 -0
  15. package/rules/ast/is-method-call.js +62 -0
  16. package/rules/ast/literal.js +32 -0
  17. package/rules/ast/loop-types.js +9 -0
  18. package/rules/consistent-assert-style.js +169 -0
  19. package/rules/consistent-assert-throws-callback-style.js +241 -0
  20. package/rules/consistent-modifier-style.js +95 -0
  21. package/rules/consistent-test-context-name.js +75 -0
  22. package/rules/consistent-test-filename.js +70 -0
  23. package/rules/consistent-test-it.js +84 -0
  24. package/rules/fix/index.js +5 -0
  25. package/rules/fix/remove-argument.js +58 -0
  26. package/rules/fix/replace-member-expression-property.js +25 -0
  27. package/rules/hooks-order.js +147 -0
  28. package/rules/index.js +84 -0
  29. package/rules/max-assertions.js +88 -0
  30. package/rules/max-nested-describe.js +70 -0
  31. package/rules/no-assert-in-describe.js +61 -0
  32. package/rules/no-assert-in-hook.js +61 -0
  33. package/rules/no-assert-match-string.js +166 -0
  34. package/rules/no-assert-throws-async.js +113 -0
  35. package/rules/no-assert-throws-call.js +106 -0
  36. package/rules/no-assert-throws-multiple-statements.js +194 -0
  37. package/rules/no-assert-throws-string.js +78 -0
  38. package/rules/no-async-describe.js +50 -0
  39. package/rules/no-async-fn-without-await.js +74 -0
  40. package/rules/no-callback-and-promise.js +56 -0
  41. package/rules/no-commented-tests.js +64 -0
  42. package/rules/no-compound-assertion.js +131 -0
  43. package/rules/no-conditional-assertion.js +108 -0
  44. package/rules/no-conditional-in-test.js +66 -0
  45. package/rules/no-conditional-tests.js +75 -0
  46. package/rules/no-conflicting-modifiers.js +73 -0
  47. package/rules/no-constant-assertion.js +218 -0
  48. package/rules/no-done-callback.js +58 -0
  49. package/rules/no-duplicate-assertions.js +172 -0
  50. package/rules/no-duplicate-hooks.js +75 -0
  51. package/rules/no-duplicate-plan.js +209 -0
  52. package/rules/no-export.js +45 -0
  53. package/rules/no-identical-assertion-arguments.js +84 -0
  54. package/rules/no-identical-title.js +101 -0
  55. package/rules/no-incorrect-deep-equal.js +113 -0
  56. package/rules/no-incorrect-strict-equal.js +99 -0
  57. package/rules/no-loop-static-title.js +93 -0
  58. package/rules/no-misused-concurrency.js +85 -0
  59. package/rules/no-mock-timers-destructured-import.js +150 -0
  60. package/rules/no-nested-tests.js +71 -0
  61. package/rules/no-only-test.js +11 -0
  62. package/rules/no-parent-test-context.js +244 -0
  63. package/rules/no-process-env-mutation.js +495 -0
  64. package/rules/no-process-exit-in-test.js +96 -0
  65. package/rules/no-skip-test.js +11 -0
  66. package/rules/no-skip-without-reason.js +90 -0
  67. package/rules/no-skip-without-return.js +123 -0
  68. package/rules/no-sleep-in-test.js +482 -0
  69. package/rules/no-snapshot-in-loop.js +132 -0
  70. package/rules/no-standalone-assert.js +51 -0
  71. package/rules/no-test-inside-hook.js +68 -0
  72. package/rules/no-test-return-statement.js +217 -0
  73. package/rules/no-todo-test.js +11 -0
  74. package/rules/no-unawaited-promise-assertion.js +366 -0
  75. package/rules/no-unawaited-rejects.js +87 -0
  76. package/rules/no-unawaited-subtest.js +66 -0
  77. package/rules/no-unknown-test-options.js +77 -0
  78. package/rules/no-useless-assertion.js +60 -0
  79. package/rules/prefer-assert-match.js +253 -0
  80. package/rules/prefer-assert-throws.js +97 -0
  81. package/rules/prefer-async-await.js +203 -0
  82. package/rules/prefer-context-mock.js +55 -0
  83. package/rules/prefer-diagnostic.js +94 -0
  84. package/rules/prefer-equality-assertion.js +124 -0
  85. package/rules/prefer-hooks-on-top.js +75 -0
  86. package/rules/prefer-lowercase-title.js +119 -0
  87. package/rules/prefer-mock-method.js +115 -0
  88. package/rules/prefer-strict-assert.js +82 -0
  89. package/rules/prefer-test-context-assert.js +162 -0
  90. package/rules/prefer-todo.js +98 -0
  91. package/rules/require-assertion.js +99 -0
  92. package/rules/require-await-concurrent-subtests.js +119 -0
  93. package/rules/require-context-assert-with-plan.js +127 -0
  94. package/rules/require-hook.js +108 -0
  95. package/rules/require-mock-timers-advance.js +313 -0
  96. package/rules/require-mock-timers-apis.js +383 -0
  97. package/rules/require-throws-expectation.js +65 -0
  98. package/rules/require-throws-validator-return-true.js +156 -0
  99. package/rules/require-top-level-describe.js +89 -0
  100. package/rules/rule/index.js +9 -0
  101. package/rules/rule/to-eslint-create.js +35 -0
  102. package/rules/rule/to-eslint-listener.js +52 -0
  103. package/rules/rule/to-eslint-problem.js +36 -0
  104. package/rules/rule/to-eslint-rule-fixer.js +47 -0
  105. package/rules/rule/to-eslint-rule.js +36 -0
  106. package/rules/rule/to-eslint-rules.js +8 -0
  107. package/rules/rule/unicorn-context.js +34 -0
  108. package/rules/rule/unicorn-listeners.js +56 -0
  109. package/rules/rule/utilities.js +26 -0
  110. package/rules/shared/test-modifier-rule.js +92 -0
  111. package/rules/test-title-format.js +86 -0
  112. package/rules/test-title.js +139 -0
  113. package/rules/utils/contains-suspension-point.js +35 -0
  114. package/rules/utils/get-comments.js +15 -0
  115. package/rules/utils/get-documentation-url.js +9 -0
  116. package/rules/utils/get-enclosing-function.js +18 -0
  117. package/rules/utils/index.js +15 -0
  118. package/rules/utils/is-conditional-branch.js +37 -0
  119. package/rules/utils/is-promise-type.js +26 -0
  120. package/rules/utils/is-same-reference.js +179 -0
  121. package/rules/utils/is-value-not-usable.js +5 -0
  122. package/rules/utils/node-test.js +883 -0
  123. package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +78 -0
  124. package/rules/utils/parentheses/iterate-surrounding-parentheses.js +80 -0
  125. package/rules/utils/parentheses/parentheses.js +69 -0
  126. package/rules/utils/types.js +5 -0
  127. package/rules/utils/unwrap-typescript-expression.js +16 -0
  128. package/rules/valid-describe-callback.js +63 -0
@@ -0,0 +1,88 @@
1
+ import {
2
+ resolveImports,
3
+ parseTestCall,
4
+ parseAssertionCall,
5
+ createContextTracker,
6
+ isAssertionCallWithSupportedContext,
7
+ } from './utils/node-test.js';
8
+
9
+ const MESSAGE_ID = 'max-assertions';
10
+
11
+ const messages = {
12
+ [MESSAGE_ID]: 'Too many assertions ({{count}}). Maximum allowed is {{max}}.',
13
+ };
14
+
15
+ /** @param {import('eslint').Rule.RuleContext} context */
16
+ const create = context => {
17
+ const imports = resolveImports(context);
18
+ if (!imports.isTestFile) {
19
+ return;
20
+ }
21
+
22
+ const {max} = context.options[0];
23
+ const tracker = createContextTracker(imports);
24
+
25
+ // One frame per enclosing test/subtest; assertions count toward the innermost.
26
+ const frames = [];
27
+
28
+ context.on('CallExpression', node => {
29
+ const isTest = parseTestCall(node, imports)?.kind === 'test' || tracker.isSubtestCall(node);
30
+ tracker.update(node);
31
+
32
+ if (isTest) {
33
+ frames.push({node, count: 0});
34
+ return;
35
+ }
36
+
37
+ if (frames.length > 0 && parseAssertionCall(node, imports) && isAssertionCallWithSupportedContext(node, tracker)) {
38
+ frames.at(-1).count += 1;
39
+ }
40
+ });
41
+
42
+ context.onExit('CallExpression', node => {
43
+ tracker.leave(node);
44
+
45
+ if (frames.at(-1)?.node !== node) {
46
+ return;
47
+ }
48
+
49
+ const {count} = frames.pop();
50
+ if (count > max) {
51
+ return {
52
+ node,
53
+ messageId: MESSAGE_ID,
54
+ data: {count, max},
55
+ };
56
+ }
57
+ });
58
+ };
59
+
60
+ /** @type {import('eslint').Rule.RuleModule} */
61
+ const config = {
62
+ create,
63
+ meta: {
64
+ type: 'suggestion',
65
+ docs: {
66
+ description: 'Enforce a maximum number of assertions in a test.',
67
+ recommended: false,
68
+ },
69
+ schema: [
70
+ {
71
+ type: 'object',
72
+ properties: {
73
+ max: {
74
+ type: 'integer',
75
+ minimum: 1,
76
+ description: 'The maximum number of assertions allowed in a test.',
77
+ },
78
+ },
79
+ additionalProperties: false,
80
+ },
81
+ ],
82
+ defaultOptions: [{max: 5}],
83
+ messages,
84
+ languages: ['js/js'],
85
+ },
86
+ };
87
+
88
+ export default config;
@@ -0,0 +1,70 @@
1
+ import {resolveImports, parseTestCall, createSuiteDepthTracker} from './utils/node-test.js';
2
+
3
+ const MESSAGE_ID = 'max-nested-describe';
4
+
5
+ const messages = {
6
+ [MESSAGE_ID]: 'Describe blocks are nested too deeply ({{depth}}). Maximum allowed is {{max}}.',
7
+ };
8
+
9
+ /** @param {import('eslint').Rule.RuleContext} context */
10
+ const create = context => {
11
+ const imports = resolveImports(context);
12
+ if (!imports.isTestFile) {
13
+ return;
14
+ }
15
+
16
+ const {max} = context.options[0];
17
+
18
+ const tracker = createSuiteDepthTracker();
19
+
20
+ context.on('CallExpression', node => {
21
+ const parsed = parseTestCall(node, imports);
22
+ if (parsed?.kind !== 'suite') {
23
+ return;
24
+ }
25
+
26
+ tracker.enterSuite(node);
27
+
28
+ if (tracker.depth > max) {
29
+ return {
30
+ node,
31
+ messageId: MESSAGE_ID,
32
+ data: {depth: tracker.depth, max},
33
+ };
34
+ }
35
+ });
36
+
37
+ context.onExit('CallExpression', node => {
38
+ tracker.exitSuite(node);
39
+ });
40
+ };
41
+
42
+ /** @type {import('eslint').Rule.RuleModule} */
43
+ const config = {
44
+ create,
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: {
48
+ description: 'Enforce a maximum depth for nested `describe` blocks.',
49
+ recommended: true,
50
+ },
51
+ schema: [
52
+ {
53
+ type: 'object',
54
+ properties: {
55
+ max: {
56
+ type: 'integer',
57
+ minimum: 1,
58
+ description: 'The maximum allowed depth of nested `describe` blocks.',
59
+ },
60
+ },
61
+ additionalProperties: false,
62
+ },
63
+ ],
64
+ defaultOptions: [{max: 5}],
65
+ messages,
66
+ languages: ['js/js'],
67
+ },
68
+ };
69
+
70
+ export default config;
@@ -0,0 +1,61 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ nearestTestCallbackKind,
5
+ createContextTracker,
6
+ isAssertionCallWithSupportedContext,
7
+ } from './utils/node-test.js';
8
+
9
+ const MESSAGE_ID = 'no-assert-in-describe';
10
+
11
+ const messages = {
12
+ [MESSAGE_ID]: 'Assertion runs when the suite is built, not when a test runs. Move it into a test or hook.',
13
+ };
14
+
15
+ /** @param {import('eslint').Rule.RuleContext} context */
16
+ const create = context => {
17
+ const imports = resolveImports(context);
18
+ if (!imports.isTestFile) {
19
+ return;
20
+ }
21
+
22
+ const tracker = createContextTracker(imports);
23
+
24
+ context.on('CallExpression', node => {
25
+ tracker.update(node);
26
+
27
+ if (!parseAssertionCall(node, imports) || !isAssertionCallWithSupportedContext(node, tracker)) {
28
+ return;
29
+ }
30
+
31
+ if (nearestTestCallbackKind(node, imports) !== 'suite') {
32
+ return;
33
+ }
34
+
35
+ return {
36
+ node,
37
+ messageId: MESSAGE_ID,
38
+ };
39
+ });
40
+
41
+ context.onExit('CallExpression', node => {
42
+ tracker.leave(node);
43
+ });
44
+ };
45
+
46
+ /** @type {import('eslint').Rule.RuleModule} */
47
+ const config = {
48
+ create,
49
+ meta: {
50
+ type: 'problem',
51
+ docs: {
52
+ description: 'Disallow assertions directly inside a `describe` body.',
53
+ recommended: true,
54
+ },
55
+ schema: [],
56
+ messages,
57
+ languages: ['js/js'],
58
+ },
59
+ };
60
+
61
+ export default config;
@@ -0,0 +1,61 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ nearestTestCallbackKind,
5
+ createContextTracker,
6
+ isAssertionCallWithSupportedContext,
7
+ } from './utils/node-test.js';
8
+
9
+ const MESSAGE_ID = 'no-assert-in-hook';
10
+
11
+ const messages = {
12
+ [MESSAGE_ID]: 'Avoid assertions inside a hook. A hook failure is attributed to every affected test rather than reported as a focused failure. Assert inside a test instead.',
13
+ };
14
+
15
+ /** @param {import('eslint').Rule.RuleContext} context */
16
+ const create = context => {
17
+ const imports = resolveImports(context);
18
+ if (!imports.isTestFile) {
19
+ return;
20
+ }
21
+
22
+ const tracker = createContextTracker(imports, {trackHooks: true});
23
+
24
+ context.on('CallExpression', node => {
25
+ tracker.update(node);
26
+
27
+ if (!parseAssertionCall(node, imports) || !isAssertionCallWithSupportedContext(node, tracker)) {
28
+ return;
29
+ }
30
+
31
+ if (nearestTestCallbackKind(node, imports) !== 'hook') {
32
+ return;
33
+ }
34
+
35
+ return {
36
+ node,
37
+ messageId: MESSAGE_ID,
38
+ };
39
+ });
40
+
41
+ context.onExit('CallExpression', node => {
42
+ tracker.leave(node);
43
+ });
44
+ };
45
+
46
+ /** @type {import('eslint').Rule.RuleModule} */
47
+ const config = {
48
+ create,
49
+ meta: {
50
+ type: 'suggestion',
51
+ docs: {
52
+ description: 'Disallow assertions inside hooks.',
53
+ recommended: false,
54
+ },
55
+ schema: [],
56
+ messages,
57
+ languages: ['js/js'],
58
+ },
59
+ };
60
+
61
+ export default config;
@@ -0,0 +1,166 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ createContextTracker,
5
+ isAssertionCallWithSupportedContext,
6
+ } from './utils/node-test.js';
7
+ import {isStringExpression, getStaticStringValue} from './ast/index.js';
8
+ import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
9
+
10
+ const MESSAGE_ID_ERROR = 'no-assert-match-string/error';
11
+ const MESSAGE_ID_REGEXP_SUGGESTION = 'no-assert-match-string/regexp-suggestion';
12
+ const MESSAGE_ID_ASSERTION_SUGGESTION = 'no-assert-match-string/assertion-suggestion';
13
+
14
+ const messages = {
15
+ [MESSAGE_ID_ERROR]: 'The second argument to `{{method}}()` must be a `RegExp`, not a string.',
16
+ [MESSAGE_ID_REGEXP_SUGGESTION]: 'Wrap the string in `new RegExp()`.',
17
+ [MESSAGE_ID_ASSERTION_SUGGESTION]: 'Compare with `{{replacementMethod}}()`.',
18
+ };
19
+
20
+ const MATCH_METHODS = new Set(['match', 'doesNotMatch']);
21
+ const EQUALITY_METHODS = new Map([
22
+ ['match', 'strictEqual'],
23
+ ['doesNotMatch', 'notStrictEqual'],
24
+ ]);
25
+
26
+ function canConvertToRegExp(node) {
27
+ const value = getStaticStringValue(unwrapTypeScriptExpression(node));
28
+ if (value === undefined) {
29
+ return false;
30
+ }
31
+
32
+ try {
33
+ const regexp = new RegExp(value);
34
+ return regexp instanceof RegExp;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ function getSuggestions({node, parsed, regexpArgument, sourceCode}) {
41
+ const suggestions = [];
42
+
43
+ if (canConvertToRegExp(regexpArgument)) {
44
+ suggestions.push({
45
+ messageId: MESSAGE_ID_REGEXP_SUGGESTION,
46
+ fix: fixer => fixer.replaceText(regexpArgument, `new RegExp(${sourceCode.getText(regexpArgument)})`),
47
+ });
48
+ }
49
+
50
+ const replacementMethod = EQUALITY_METHODS.get(parsed.method);
51
+ if (
52
+ replacementMethod
53
+ && parsed.methodNode
54
+ && node.callee.type !== 'Identifier'
55
+ ) {
56
+ const methodReplacementRange = getMethodReplacementRange(node, parsed, sourceCode);
57
+ if (methodReplacementRange) {
58
+ suggestions.push({
59
+ messageId: MESSAGE_ID_ASSERTION_SUGGESTION,
60
+ data: {replacementMethod},
61
+ fix: fixer => fixer.replaceTextRange(methodReplacementRange, replacementMethod),
62
+ });
63
+ }
64
+ }
65
+
66
+ return suggestions;
67
+ }
68
+
69
+ function getMethodReplacementRange(node, parsed, sourceCode) {
70
+ const {callee} = node;
71
+ if (
72
+ callee.type === 'MemberExpression'
73
+ && callee.object.type === 'MemberExpression'
74
+ && !callee.object.computed
75
+ && callee.object.property.type === 'Identifier'
76
+ && callee.object.property.name === 'strict'
77
+ ) {
78
+ const range = [
79
+ sourceCode.getRange(callee.object.property)[0],
80
+ sourceCode.getRange(parsed.methodNode)[1],
81
+ ];
82
+ if (hasCommentInRange(sourceCode, callee, range)) {
83
+ return undefined;
84
+ }
85
+
86
+ return range;
87
+ }
88
+
89
+ return sourceCode.getRange(parsed.methodNode);
90
+ }
91
+
92
+ function hasCommentInRange(sourceCode, node, range) {
93
+ return sourceCode.getCommentsInside(node).some(comment => {
94
+ const commentRange = sourceCode.getRange(comment);
95
+ return commentRange[0] >= range[0] && commentRange[1] <= range[1];
96
+ });
97
+ }
98
+
99
+ /** @param {import('eslint').Rule.RuleContext} context */
100
+ const create = context => {
101
+ const {sourceCode} = context;
102
+ const imports = resolveImports(context);
103
+ if (!imports.isAssertOrTestFile) {
104
+ return;
105
+ }
106
+
107
+ const tracker = createContextTracker(imports, {trackHooks: true});
108
+
109
+ context.on('CallExpression', node => {
110
+ tracker.update(node);
111
+
112
+ const parsed = parseAssertionCall(node, imports);
113
+ if (!parsed || !MATCH_METHODS.has(parsed.method) || !isAssertionCallWithSupportedContext(node, tracker)) {
114
+ return;
115
+ }
116
+
117
+ const regexpArgument = node.arguments[1];
118
+ if (
119
+ !regexpArgument
120
+ || regexpArgument.type === 'SpreadElement'
121
+ || !isStringExpression(unwrapTypeScriptExpression(regexpArgument))
122
+ ) {
123
+ return;
124
+ }
125
+
126
+ const problem = {
127
+ node: regexpArgument,
128
+ messageId: MESSAGE_ID_ERROR,
129
+ data: {method: parsed.method},
130
+ };
131
+
132
+ const suggestions = getSuggestions({
133
+ node,
134
+ parsed,
135
+ regexpArgument,
136
+ sourceCode,
137
+ });
138
+ if (suggestions.length > 0) {
139
+ problem.suggest = suggestions;
140
+ }
141
+
142
+ return problem;
143
+ });
144
+
145
+ context.onExit('CallExpression', node => {
146
+ tracker.leave(node);
147
+ });
148
+ };
149
+
150
+ /** @type {import('eslint').Rule.RuleModule} */
151
+ const config = {
152
+ create,
153
+ meta: {
154
+ type: 'problem',
155
+ docs: {
156
+ description: 'Disallow strings as the regexp argument of `assert.match()`/`assert.doesNotMatch()`.',
157
+ recommended: 'unopinionated',
158
+ },
159
+ hasSuggestions: true,
160
+ schema: [],
161
+ messages,
162
+ languages: ['js/js'],
163
+ },
164
+ };
165
+
166
+ export default config;
@@ -0,0 +1,113 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ createContextTracker,
5
+ isAssertionCallWithSupportedContext,
6
+ } from './utils/node-test.js';
7
+ import isFunction from './ast/is-function.js';
8
+ import {getEnclosingFunction} from './utils/index.js';
9
+ import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
10
+
11
+ const MESSAGE_ID_ERROR = 'no-assert-throws-async/error';
12
+ const MESSAGE_ID_SUGGESTION = 'no-assert-throws-async/suggestion';
13
+
14
+ const messages = {
15
+ [MESSAGE_ID_ERROR]: '`{{method}}` does not catch an async function, which never throws synchronously. Use `{{replacement}}` instead.',
16
+ [MESSAGE_ID_SUGGESTION]: 'Replace with `{{replacement}}`.',
17
+ };
18
+
19
+ const SYNC_TO_ASYNC = new Map([
20
+ ['throws', 'rejects'],
21
+ ['doesNotThrow', 'doesNotReject'],
22
+ ]);
23
+
24
+ /** @param {import('eslint').Rule.RuleContext} context */
25
+ const create = context => {
26
+ const imports = resolveImports(context);
27
+ if (!imports.isAssertOrTestFile) {
28
+ return;
29
+ }
30
+
31
+ const tracker = createContextTracker(imports, {trackHooks: true});
32
+
33
+ context.on('CallExpression', node => {
34
+ tracker.update(node);
35
+
36
+ const assertion = parseAssertionCall(node, imports);
37
+ if (!assertion || !isAssertionCallWithSupportedContext(node, tracker)) {
38
+ return;
39
+ }
40
+
41
+ const replacement = SYNC_TO_ASYNC.get(assertion.method);
42
+ if (!replacement) {
43
+ return;
44
+ }
45
+
46
+ const [firstArgument] = node.arguments;
47
+ if (!firstArgument) {
48
+ return;
49
+ }
50
+
51
+ const callback = unwrapTypeScriptExpression(firstArgument);
52
+ if (!isFunction(callback) || !callback.async) {
53
+ return;
54
+ }
55
+
56
+ const {callee} = node;
57
+ const data = {method: assertion.method, replacement};
58
+ const problem = {
59
+ node: callee,
60
+ messageId: MESSAGE_ID_ERROR,
61
+ data,
62
+ };
63
+
64
+ // Only the member forms (`assert.throws`, `t.assert.throws`) can be rewritten. A bare named
65
+ // import (`throws`) would reference an unimported `rejects`, so leave it reported but unfixed.
66
+ if (callee.type === 'MemberExpression') {
67
+ const isAwaited = node.parent?.type === 'AwaitExpression';
68
+ const enclosingFunction = getEnclosingFunction(node);
69
+ // Prepend `await` only where it is both valid and needed: a bare call statement inside an
70
+ // async function. Otherwise just switch the method and let `no-unawaited-rejects` guide the await.
71
+ const shouldAwait = !isAwaited
72
+ && node.parent?.type === 'ExpressionStatement'
73
+ && enclosingFunction?.async === true;
74
+
75
+ problem.suggest = [
76
+ {
77
+ messageId: MESSAGE_ID_SUGGESTION,
78
+ data,
79
+ * fix(fixer) {
80
+ yield fixer.replaceText(callee.property, replacement);
81
+ if (shouldAwait) {
82
+ yield fixer.insertTextBefore(node, 'await ');
83
+ }
84
+ },
85
+ },
86
+ ];
87
+ }
88
+
89
+ return problem;
90
+ });
91
+
92
+ context.onExit('CallExpression', node => {
93
+ tracker.leave(node);
94
+ });
95
+ };
96
+
97
+ /** @type {import('eslint').Rule.RuleModule} */
98
+ const config = {
99
+ create,
100
+ meta: {
101
+ type: 'problem',
102
+ docs: {
103
+ description: 'Disallow passing an async function to `assert.throws()`/`assert.doesNotThrow()`.',
104
+ recommended: 'unopinionated',
105
+ },
106
+ hasSuggestions: true,
107
+ schema: [],
108
+ messages,
109
+ languages: ['js/js'],
110
+ },
111
+ };
112
+
113
+ export default config;
@@ -0,0 +1,106 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ createContextTracker,
5
+ isAssertionCallWithSupportedContext,
6
+ } from './utils/node-test.js';
7
+ import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
8
+
9
+ const MESSAGE_ID_ERROR = 'no-assert-throws-call/error';
10
+ const MESSAGE_ID_SUGGESTION = 'no-assert-throws-call/suggestion';
11
+
12
+ const messages = {
13
+ [MESSAGE_ID_ERROR]: '`{{method}}()` expects a function callback. This call runs before the assertion can catch it.',
14
+ [MESSAGE_ID_SUGGESTION]: 'Wrap the call in an arrow function.',
15
+ };
16
+
17
+ function unwrapExpression(node) {
18
+ node = unwrapTypeScriptExpression(node);
19
+
20
+ while (node.type === 'ChainExpression') {
21
+ node = unwrapTypeScriptExpression(node.expression);
22
+ }
23
+
24
+ return node;
25
+ }
26
+
27
+ function isBindCall(node) {
28
+ const {callee} = node;
29
+
30
+ return callee.type === 'MemberExpression'
31
+ && !callee.computed
32
+ && callee.property.type === 'Identifier'
33
+ && callee.property.name === 'bind';
34
+ }
35
+
36
+ function isFunctionConstructorCall(node) {
37
+ return node.callee.type === 'Identifier' && node.callee.name === 'Function';
38
+ }
39
+
40
+ function isFunctionProducingCall(node) {
41
+ return isBindCall(node) || isFunctionConstructorCall(node);
42
+ }
43
+
44
+ /** @param {import('eslint').Rule.RuleContext} context */
45
+ const create = context => {
46
+ const {sourceCode} = context;
47
+ const imports = resolveImports(context);
48
+ if (!imports.isAssertOrTestFile) {
49
+ return;
50
+ }
51
+
52
+ const tracker = createContextTracker(imports, {trackHooks: true});
53
+
54
+ context.on('CallExpression', node => {
55
+ tracker.update(node);
56
+
57
+ const assertion = parseAssertionCall(node, imports);
58
+ if (!assertion || assertion.method !== 'throws' || !isAssertionCallWithSupportedContext(node, tracker)) {
59
+ return;
60
+ }
61
+
62
+ const [firstArgument] = node.arguments;
63
+ if (!firstArgument || firstArgument.type === 'SpreadElement') {
64
+ return;
65
+ }
66
+
67
+ const call = unwrapExpression(firstArgument);
68
+ if (call.type !== 'CallExpression' || isFunctionProducingCall(call)) {
69
+ return;
70
+ }
71
+
72
+ return {
73
+ node: firstArgument,
74
+ messageId: MESSAGE_ID_ERROR,
75
+ data: {method: assertion.method},
76
+ suggest: [
77
+ {
78
+ messageId: MESSAGE_ID_SUGGESTION,
79
+ fix: fixer => fixer.replaceText(firstArgument, `() => ${sourceCode.getText(firstArgument)}`),
80
+ },
81
+ ],
82
+ };
83
+ });
84
+
85
+ context.onExit('CallExpression', node => {
86
+ tracker.leave(node);
87
+ });
88
+ };
89
+
90
+ /** @type {import('eslint').Rule.RuleModule} */
91
+ const config = {
92
+ create,
93
+ meta: {
94
+ type: 'problem',
95
+ docs: {
96
+ description: 'Disallow calling the function passed to `assert.throws()`.',
97
+ recommended: 'unopinionated',
98
+ },
99
+ hasSuggestions: true,
100
+ schema: [],
101
+ messages,
102
+ languages: ['js/js'],
103
+ },
104
+ };
105
+
106
+ export default config;