@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,132 @@
1
+ import {findVariable} from '@eslint-community/eslint-utils';
2
+ import {
3
+ resolveImports,
4
+ createContextTracker,
5
+ getCalleeChain,
6
+ } from './utils/node-test.js';
7
+ import {isLoop, isFunction} from './ast/index.js';
8
+
9
+ const MESSAGE_ID = 'no-snapshot-in-loop';
10
+
11
+ const messages = {
12
+ [MESSAGE_ID]: 'Do not use positional snapshots inside loops. Changing the iteration count shifts every following snapshot.',
13
+ };
14
+
15
+ function isInCurrentCallback(node, tracker) {
16
+ const callback = tracker.currentCallback();
17
+ if (!callback) {
18
+ return false;
19
+ }
20
+
21
+ let current = node.parent;
22
+ while (current) {
23
+ if (current === callback) {
24
+ return true;
25
+ }
26
+
27
+ if (isFunction(current)) {
28
+ return false;
29
+ }
30
+
31
+ current = current.parent;
32
+ }
33
+
34
+ return false;
35
+ }
36
+
37
+ function isCurrentContextReference(node, tracker, sourceCode) {
38
+ const parameter = tracker.currentCallback()?.params[0];
39
+ if (
40
+ parameter?.type !== 'Identifier'
41
+ || node.name !== parameter.name
42
+ ) {
43
+ return false;
44
+ }
45
+
46
+ const variable = findVariable(sourceCode.getScope(node), node);
47
+ return variable?.defs.some(definition => definition.name === parameter) ?? false;
48
+ }
49
+
50
+ function isCurrentContextSnapshotCall(node, tracker, sourceCode) {
51
+ const chain = getCalleeChain(node.callee);
52
+ return (
53
+ chain?.members.length === 2
54
+ && chain.members[0].name === 'assert'
55
+ && chain.members[1].name === 'snapshot'
56
+ && isInCurrentCallback(node, tracker)
57
+ && isCurrentContextReference(chain.root, tracker, sourceCode)
58
+ );
59
+ }
60
+
61
+ function isInLoopBody(node) {
62
+ let child = node;
63
+ let current = node.parent;
64
+
65
+ while (current) {
66
+ if (isFunction(current)) {
67
+ return false;
68
+ }
69
+
70
+ if (
71
+ isLoop(current)
72
+ && current.body === child
73
+ ) {
74
+ return true;
75
+ }
76
+
77
+ child = current;
78
+ current = current.parent;
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ /** @param {import('eslint').Rule.RuleContext} context */
85
+ const create = context => {
86
+ const {sourceCode} = context;
87
+ const imports = resolveImports(context);
88
+ if (!imports.isTestFile) {
89
+ return;
90
+ }
91
+
92
+ const tracker = createContextTracker(imports);
93
+
94
+ context.on('CallExpression', node => {
95
+ let problem;
96
+
97
+ if (
98
+ isCurrentContextSnapshotCall(node, tracker, sourceCode)
99
+ && isInLoopBody(node)
100
+ ) {
101
+ problem = {
102
+ node,
103
+ messageId: MESSAGE_ID,
104
+ };
105
+ }
106
+
107
+ tracker.update(node);
108
+
109
+ return problem;
110
+ });
111
+
112
+ context.onExit('CallExpression', node => {
113
+ tracker.leave(node);
114
+ });
115
+ };
116
+
117
+ /** @type {import('eslint').Rule.RuleModule} */
118
+ const config = {
119
+ create,
120
+ meta: {
121
+ type: 'problem',
122
+ docs: {
123
+ description: 'Disallow snapshot assertions inside loop bodies.',
124
+ recommended: false,
125
+ },
126
+ schema: [],
127
+ messages,
128
+ languages: ['js/js'],
129
+ },
130
+ };
131
+
132
+ export default config;
@@ -0,0 +1,51 @@
1
+ import {resolveImports, parseAssertionCall} from './utils/node-test.js';
2
+ import isFunction from './ast/is-function.js';
3
+
4
+ const MESSAGE_ID = 'no-standalone-assert';
5
+
6
+ const messages = {
7
+ [MESSAGE_ID]: 'Assertion runs when the module is loaded, not as part of a test. Move it into a test or hook.',
8
+ };
9
+
10
+ /** @param {import('eslint').Rule.RuleContext} context */
11
+ const create = context => {
12
+ const imports = resolveImports(context);
13
+ if (!imports.isTestFile || !imports.hasAssert) {
14
+ return;
15
+ }
16
+
17
+ context.on('CallExpression', node => {
18
+ if (!parseAssertionCall(node, imports)) {
19
+ return;
20
+ }
21
+
22
+ // Any enclosing function (test/hook callback or a helper) means it is not standalone.
23
+ for (let current = node.parent; current; current = current.parent) {
24
+ if (isFunction(current)) {
25
+ return;
26
+ }
27
+ }
28
+
29
+ return {
30
+ node,
31
+ messageId: MESSAGE_ID,
32
+ };
33
+ });
34
+ };
35
+
36
+ /** @type {import('eslint').Rule.RuleModule} */
37
+ const config = {
38
+ create,
39
+ meta: {
40
+ type: 'problem',
41
+ docs: {
42
+ description: 'Disallow assertions outside of a test.',
43
+ recommended: 'unopinionated',
44
+ },
45
+ schema: [],
46
+ messages,
47
+ languages: ['js/js'],
48
+ },
49
+ };
50
+
51
+ export default config;
@@ -0,0 +1,68 @@
1
+ import {resolveImports, parseTestCall, getHookCallback} from './utils/node-test.js';
2
+
3
+ const MESSAGE_ID = 'no-test-inside-hook';
4
+
5
+ const messages = {
6
+ [MESSAGE_ID]: 'Do not define a test or suite inside a hook. Define it at the top level or inside a `describe`.',
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
+ // Stack of hook callback function nodes we are currently inside.
17
+ const hookCallbackStack = [];
18
+
19
+ context.on('CallExpression', node => {
20
+ const parsed = parseTestCall(node, imports);
21
+ if (!parsed) {
22
+ return;
23
+ }
24
+
25
+ if ((parsed.kind === 'test' || parsed.kind === 'suite') && hookCallbackStack.length > 0) {
26
+ return {
27
+ node,
28
+ messageId: MESSAGE_ID,
29
+ };
30
+ }
31
+
32
+ if (parsed.kind === 'hook') {
33
+ const callback = getHookCallback(node);
34
+ if (callback) {
35
+ hookCallbackStack.push(callback);
36
+ }
37
+ }
38
+ });
39
+
40
+ context.onExit('CallExpression', node => {
41
+ const parsed = parseTestCall(node, imports);
42
+ if (parsed?.kind !== 'hook') {
43
+ return;
44
+ }
45
+
46
+ const callback = getHookCallback(node);
47
+ if (callback && hookCallbackStack.at(-1) === callback) {
48
+ hookCallbackStack.pop();
49
+ }
50
+ });
51
+ };
52
+
53
+ /** @type {import('eslint').Rule.RuleModule} */
54
+ const config = {
55
+ create,
56
+ meta: {
57
+ type: 'problem',
58
+ docs: {
59
+ description: 'Disallow defining tests and suites inside a hook.',
60
+ recommended: 'unopinionated',
61
+ },
62
+ schema: [],
63
+ messages,
64
+ languages: ['js/js'],
65
+ },
66
+ };
67
+
68
+ export default config;
@@ -0,0 +1,217 @@
1
+ import {findVariable} from '@eslint-community/eslint-utils';
2
+ import {
3
+ HOOK_FUNCTIONS,
4
+ MODIFIERS,
5
+ resolveImports,
6
+ parseTestCall,
7
+ getHookCallback,
8
+ getTestCallback,
9
+ getSubtestReceiver,
10
+ isHookMemberTestCall,
11
+ } from './utils/node-test.js';
12
+ import isFunction from './ast/is-function.js';
13
+ import isPromiseType from './utils/is-promise-type.js';
14
+
15
+ const MESSAGE_ID = 'no-test-return-statement';
16
+
17
+ const messages = {
18
+ [MESSAGE_ID]: 'Do not return a concrete value from a test or hook. Return a Promise to signal async completion, or return nothing.',
19
+ };
20
+
21
+ // "Nothing" types and types we cannot pin down, all fine to return.
22
+ const ALLOWED_TYPE_STRINGS = new Set(['void', 'undefined', 'null', 'any', 'never', 'unknown']);
23
+
24
+ /*
25
+ Whether the type is safe to return from a test or hook: a returned Promise (awaited by `node:test`),
26
+ a "nothing" type, or a type we cannot resolve. Anything else is a concrete value.
27
+ */
28
+ function isAllowedReturnType(type, checker) {
29
+ if (type.isUnion()) {
30
+ return type.types.every(member => isAllowedReturnType(member, checker));
31
+ }
32
+
33
+ // `isPromiseType` returns `undefined` when indeterminate; only a definite `false` is a value.
34
+ if (isPromiseType(type, checker) !== false) {
35
+ return true;
36
+ }
37
+
38
+ return ALLOWED_TYPE_STRINGS.has(checker.typeToString(type));
39
+ }
40
+
41
+ function isDisallowedReturnValue(node, parserServices, checker) {
42
+ let type;
43
+ try {
44
+ type = parserServices.getTypeAtLocation(node);
45
+ } catch {
46
+ return false;
47
+ }
48
+
49
+ return !isAllowedReturnType(type, checker);
50
+ }
51
+
52
+ function getCallbackContextVariable(callback, sourceCode) {
53
+ const parameter = callback.params[0];
54
+ if (parameter?.type !== 'Identifier') {
55
+ return undefined;
56
+ }
57
+
58
+ return sourceCode.getDeclaredVariables(callback).find(variable => variable.identifiers.includes(parameter));
59
+ }
60
+
61
+ function isContextReference(node, state) {
62
+ if (node?.type !== 'Identifier') {
63
+ return false;
64
+ }
65
+
66
+ const variable = findVariable(state.sourceCode.getScope(node), node);
67
+ return variable !== undefined && state.contextVariables.includes(variable);
68
+ }
69
+
70
+ function isContextHookCall(callExpression, state) {
71
+ const {callee} = callExpression;
72
+ return callee.type === 'MemberExpression'
73
+ && !callee.computed
74
+ && callee.object.type === 'Identifier'
75
+ && isContextReference(callee.object, state)
76
+ && callee.property.type === 'Identifier'
77
+ && HOOK_FUNCTIONS.has(callee.property.name);
78
+ }
79
+
80
+ function isContextSubtestCall(callExpression, state) {
81
+ const receiver = getSubtestReceiver(callExpression);
82
+ return receiver !== undefined && isContextReference(receiver, state);
83
+ }
84
+
85
+ function getCheckedCallback(callExpression, state) {
86
+ const parsed = parseTestCall(callExpression, state.imports);
87
+ if (isHookMemberTestCall(parsed)) {
88
+ return getHookCallback(callExpression);
89
+ }
90
+
91
+ if (parsed?.kind === 'test') {
92
+ if (parsed.modifiers.some(modifier => !MODIFIERS.has(modifier.name))) {
93
+ return undefined;
94
+ }
95
+
96
+ return getTestCallback(callExpression);
97
+ }
98
+
99
+ if (parsed?.kind === 'hook' && parsed.modifiers.length === 0) {
100
+ return getHookCallback(callExpression);
101
+ }
102
+
103
+ if (isContextSubtestCall(callExpression, state)) {
104
+ return getTestCallback(callExpression);
105
+ }
106
+
107
+ if (isContextHookCall(callExpression, state)) {
108
+ return getHookCallback(callExpression);
109
+ }
110
+
111
+ return undefined;
112
+ }
113
+
114
+ /** @param {import('eslint').Rule.RuleContext} context */
115
+ const create = context => {
116
+ const {sourceCode} = context;
117
+ const imports = resolveImports(context);
118
+ if (!imports.isTestFile) {
119
+ return;
120
+ }
121
+
122
+ // This rule relies on type information to tell a Promise from a plain value. Without it
123
+ // (plain JavaScript, or TypeScript linted without a program), do nothing.
124
+ const {parserServices} = context.sourceCode;
125
+ if (!parserServices?.program) {
126
+ return;
127
+ }
128
+
129
+ const checker = parserServices.program.getTypeChecker();
130
+ const checkedCallbacks = new Set();
131
+ const state = {
132
+ imports,
133
+ sourceCode,
134
+ contextVariables: [],
135
+ };
136
+ const contextCalls = new Set();
137
+
138
+ context.on('CallExpression', node => {
139
+ const callback = getCheckedCallback(node, state);
140
+ if (!callback) {
141
+ return;
142
+ }
143
+
144
+ checkedCallbacks.add(callback);
145
+ state.contextVariables.push(getCallbackContextVariable(callback, sourceCode));
146
+ contextCalls.add(node);
147
+
148
+ if (
149
+ callback.type === 'ArrowFunctionExpression'
150
+ && callback.body.type !== 'BlockStatement'
151
+ && !callback.async
152
+ && isDisallowedReturnValue(callback.body, parserServices, checker)
153
+ ) {
154
+ return {
155
+ node: callback.body,
156
+ messageId: MESSAGE_ID,
157
+ };
158
+ }
159
+ });
160
+
161
+ context.onExit('CallExpression', node => {
162
+ if (!contextCalls.has(node)) {
163
+ return;
164
+ }
165
+
166
+ contextCalls.delete(node);
167
+ state.contextVariables.pop();
168
+ });
169
+
170
+ context.on('ReturnStatement', node => {
171
+ if (!node.argument) {
172
+ return;
173
+ }
174
+
175
+ // The return must belong to the test/hook callback itself, not a nested helper function.
176
+ let enclosing = node.parent;
177
+ while (enclosing && !isFunction(enclosing)) {
178
+ enclosing = enclosing.parent;
179
+ }
180
+
181
+ if (!enclosing || !checkedCallbacks.has(enclosing)) {
182
+ return;
183
+ }
184
+
185
+ // An `async` function always wraps its return value in a Promise, which `node:test`
186
+ // awaits, so returning a plain value there is fine.
187
+ if (enclosing.async) {
188
+ return;
189
+ }
190
+
191
+ if (!isDisallowedReturnValue(node.argument, parserServices, checker)) {
192
+ return;
193
+ }
194
+
195
+ return {
196
+ node,
197
+ messageId: MESSAGE_ID,
198
+ };
199
+ });
200
+ };
201
+
202
+ /** @type {import('eslint').Rule.RuleModule} */
203
+ const config = {
204
+ create,
205
+ meta: {
206
+ type: 'suggestion',
207
+ docs: {
208
+ description: 'Disallow returning a concrete non-Promise value from a test or hook.',
209
+ recommended: true,
210
+ },
211
+ schema: [],
212
+ messages,
213
+ languages: ['js/js'],
214
+ },
215
+ };
216
+
217
+ export default config;
@@ -0,0 +1,11 @@
1
+ import createTestModifierRule from './shared/test-modifier-rule.js';
2
+
3
+ /** @type {import('eslint').Rule.RuleModule} */
4
+ const config = createTestModifierRule({
5
+ modifier: 'todo',
6
+ description: 'Disallow the `.todo` test modifier.',
7
+ errorMessage: 'Do not commit `.todo` tests.',
8
+ recommended: false,
9
+ });
10
+
11
+ export default config;