@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,113 @@
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 = 'no-deep-equal-with-primitive';
10
+
11
+ const DEEP_EQUAL_METHODS = new Map([
12
+ ['deepEqual', 'equal'],
13
+ ['deepStrictEqual', 'strictEqual'],
14
+ ['notDeepEqual', 'notEqual'],
15
+ ['notDeepStrictEqual', 'notStrictEqual'],
16
+ ]);
17
+
18
+ /**
19
+ Check if a node represents a primitive value.
20
+ Covers: literals, `undefined`/`NaN`/`Infinity` identifiers, template literals (always a string,
21
+ regardless of interpolation), `void` expressions, and negated numeric/Infinity/NaN literals.
22
+ */
23
+ function isPrimitive(node) {
24
+ node = unwrapTypeScriptExpression(node);
25
+ return (
26
+ (node.type === 'Literal' && !node.regex)
27
+ || (node.type === 'Identifier' && ['undefined', 'NaN', 'Infinity'].includes(node.name))
28
+ || node.type === 'TemplateLiteral'
29
+ || (node.type === 'UnaryExpression' && node.operator === 'void')
30
+ || (
31
+ node.type === 'UnaryExpression'
32
+ && node.operator === '-'
33
+ && (
34
+ (node.argument.type === 'Literal' && !node.argument.regex)
35
+ || (node.argument.type === 'Identifier' && (node.argument.name === 'Infinity' || node.argument.name === 'NaN'))
36
+ )
37
+ )
38
+ );
39
+ }
40
+
41
+ /** @param {import('eslint').Rule.RuleContext} context */
42
+ const create = context => {
43
+ const imports = resolveImports(context);
44
+ if (!imports.isAssertOrTestFile) {
45
+ return;
46
+ }
47
+
48
+ const tracker = createContextTracker(imports, {trackHooks: true});
49
+
50
+ context.on('CallExpression', node => {
51
+ tracker.update(node);
52
+
53
+ const assertion = parseAssertionCall(node, imports);
54
+ if (!assertion || !isAssertionCallWithSupportedContext(node, tracker)) {
55
+ return;
56
+ }
57
+
58
+ const {method} = assertion;
59
+ const replacement = DEEP_EQUAL_METHODS.get(method);
60
+ if (!replacement) {
61
+ return;
62
+ }
63
+
64
+ const [actual, expected] = node.arguments;
65
+ if (!actual || !expected) {
66
+ return;
67
+ }
68
+
69
+ if (!isPrimitive(actual) && !isPrimitive(expected)) {
70
+ return;
71
+ }
72
+
73
+ const {callee} = node;
74
+ const problem = {
75
+ node,
76
+ messageId: MESSAGE_ID,
77
+ data: {method},
78
+ };
79
+
80
+ // Autofix only the member forms (`assert.deepEqual`, `t.assert.deepEqual`). A bare named
81
+ // import (`deepEqual`) cannot be rewritten to `equal` without also importing it, so leave
82
+ // it reported but unfixed.
83
+ if (callee.type === 'MemberExpression') {
84
+ problem.fix = fixer => fixer.replaceText(callee.property, replacement);
85
+ }
86
+
87
+ return problem;
88
+ });
89
+
90
+ context.onExit('CallExpression', node => {
91
+ tracker.leave(node);
92
+ });
93
+ };
94
+
95
+ /** @type {import('eslint').Rule.RuleModule} */
96
+ const config = {
97
+ create,
98
+ meta: {
99
+ type: 'suggestion',
100
+ docs: {
101
+ description: 'Disallow `deepEqual`/`deepStrictEqual` (and their `notDeep*` variants) when comparing with primitives.',
102
+ recommended: 'unopinionated',
103
+ },
104
+ fixable: 'code',
105
+ schema: [],
106
+ messages: {
107
+ [MESSAGE_ID]: 'Avoid using `{{method}}` with a primitive. Use the non-deep equality equivalent instead.',
108
+ },
109
+ languages: ['js/js'],
110
+ },
111
+ };
112
+
113
+ export default config;
@@ -0,0 +1,99 @@
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 = 'no-incorrect-strict-equal';
10
+
11
+ // Strict/loose equality methods and their deep equivalents.
12
+ const STRICT_TO_DEEP = new Map([
13
+ ['equal', 'deepEqual'],
14
+ ['strictEqual', 'deepStrictEqual'],
15
+ ['notEqual', 'notDeepEqual'],
16
+ ['notStrictEqual', 'notDeepStrictEqual'],
17
+ ]);
18
+
19
+ /**
20
+ Check if a node is an object or array literal. These are freshly allocated, so a strict/loose
21
+ equality comparison against them is decided purely by reference identity, never by structure.
22
+ */
23
+ function isObjectOrArrayLiteral(node) {
24
+ const unwrapped = unwrapTypeScriptExpression(node);
25
+ return unwrapped.type === 'ObjectExpression' || unwrapped.type === 'ArrayExpression';
26
+ }
27
+
28
+ /** @param {import('eslint').Rule.RuleContext} context */
29
+ const create = context => {
30
+ const imports = resolveImports(context);
31
+ if (!imports.isAssertOrTestFile) {
32
+ return;
33
+ }
34
+
35
+ const tracker = createContextTracker(imports, {trackHooks: true});
36
+
37
+ context.on('CallExpression', node => {
38
+ tracker.update(node);
39
+
40
+ const assertion = parseAssertionCall(node, imports);
41
+ if (!assertion || !isAssertionCallWithSupportedContext(node, tracker)) {
42
+ return;
43
+ }
44
+
45
+ const replacement = STRICT_TO_DEEP.get(assertion.method);
46
+ if (!replacement) {
47
+ return;
48
+ }
49
+
50
+ const [actual, expected] = node.arguments;
51
+ if (!actual || !expected) {
52
+ return;
53
+ }
54
+
55
+ if (!isObjectOrArrayLiteral(actual) && !isObjectOrArrayLiteral(expected)) {
56
+ return;
57
+ }
58
+
59
+ const {callee} = node;
60
+ const problem = {
61
+ node,
62
+ messageId: MESSAGE_ID,
63
+ data: {method: assertion.method, replacement},
64
+ };
65
+
66
+ // Autofix only the member forms (`assert.strictEqual`, `t.assert.strictEqual`). A bare named
67
+ // import (`strictEqual`) cannot be rewritten to `deepStrictEqual` without also importing it,
68
+ // so leave it reported but unfixed.
69
+ if (callee.type === 'MemberExpression') {
70
+ problem.fix = fixer => fixer.replaceText(callee.property, replacement);
71
+ }
72
+
73
+ return problem;
74
+ });
75
+
76
+ context.onExit('CallExpression', node => {
77
+ tracker.leave(node);
78
+ });
79
+ };
80
+
81
+ /** @type {import('eslint').Rule.RuleModule} */
82
+ const config = {
83
+ create,
84
+ meta: {
85
+ type: 'suggestion',
86
+ docs: {
87
+ description: 'Disallow `strictEqual`/`equal` (and their `not*` variants) when comparing with an object or array literal.',
88
+ recommended: 'unopinionated',
89
+ },
90
+ fixable: 'code',
91
+ schema: [],
92
+ messages: {
93
+ [MESSAGE_ID]: 'Avoid using `{{method}}` with an object or array literal, which compares by reference rather than structure. Use `{{replacement}}` instead.',
94
+ },
95
+ languages: ['js/js'],
96
+ },
97
+ };
98
+
99
+ export default config;
@@ -0,0 +1,93 @@
1
+ import {resolveImports, parseTestCall, getStaticString} from './utils/node-test.js';
2
+ import {isLoop, isFunction} from './ast/index.js';
3
+
4
+ const MESSAGE_ID = 'no-loop-static-title';
5
+
6
+ const messages = {
7
+ [MESSAGE_ID]: 'This title is static but generated in a loop, so every iteration registers the same title. Include the loop variable so each title is unique.',
8
+ };
9
+
10
+ // Array methods whose callback is commonly used to generate one test per element.
11
+ const ITERATION_METHODS = new Set(['map', 'forEach', 'flatMap']);
12
+
13
+ /** Whether `callExpression` is an iteration method call (`xs.map(fn)`) whose callback is `callback`. */
14
+ function isIterationCall(callExpression, callback) {
15
+ const {callee} = callExpression;
16
+ return (
17
+ callee.type === 'MemberExpression'
18
+ && !callee.computed
19
+ && callee.property.type === 'Identifier'
20
+ && ITERATION_METHODS.has(callee.property.name)
21
+ && callExpression.arguments.includes(callback)
22
+ );
23
+ }
24
+
25
+ /*
26
+ Whether the test/suite call repeats across a loop without an intervening test/suite scope.
27
+ Walking up from the call: a loop means it repeats; the first enclosing function that is an
28
+ iteration callback (`xs.map(…)`) also means it repeats; any other function is a scope boundary
29
+ (a `describe`/test callback or helper) under which the static title is no longer a duplicate.
30
+ */
31
+ function isInRepeatingScope(node) {
32
+ let current = node.parent;
33
+ while (current) {
34
+ if (isLoop(current)) {
35
+ return true;
36
+ }
37
+
38
+ if (isFunction(current)) {
39
+ return current.parent?.type === 'CallExpression' && isIterationCall(current.parent, current);
40
+ }
41
+
42
+ current = current.parent;
43
+ }
44
+
45
+ return false;
46
+ }
47
+
48
+ /** @param {import('eslint').Rule.RuleContext} context */
49
+ const create = context => {
50
+ const imports = resolveImports(context);
51
+ if (!imports.isTestFile) {
52
+ return;
53
+ }
54
+
55
+ context.on('CallExpression', node => {
56
+ const parsed = parseTestCall(node, imports);
57
+ if (parsed?.kind !== 'test' && parsed?.kind !== 'suite') {
58
+ return;
59
+ }
60
+
61
+ // A static title resolves to a constant string; a title that interpolates the loop variable
62
+ // does not, so it is correctly left alone.
63
+ if (getStaticString(node.arguments[0], context) === undefined) {
64
+ return;
65
+ }
66
+
67
+ if (!isInRepeatingScope(node)) {
68
+ return;
69
+ }
70
+
71
+ return {
72
+ node: node.arguments[0],
73
+ messageId: MESSAGE_ID,
74
+ };
75
+ });
76
+ };
77
+
78
+ /** @type {import('eslint').Rule.RuleModule} */
79
+ const config = {
80
+ create,
81
+ meta: {
82
+ type: 'problem',
83
+ docs: {
84
+ description: 'Disallow a static test or suite title inside a loop.',
85
+ recommended: 'unopinionated',
86
+ },
87
+ schema: [],
88
+ messages,
89
+ languages: ['js/js'],
90
+ },
91
+ };
92
+
93
+ export default config;
@@ -0,0 +1,85 @@
1
+ import {
2
+ resolveImports,
3
+ parseTestCall,
4
+ createContextTracker,
5
+ getSubtestReceiver,
6
+ getTestOptions,
7
+ findOptionsProperty,
8
+ } from './utils/node-test.js';
9
+
10
+ const MESSAGE_ID = 'no-misused-concurrency';
11
+
12
+ const messages = {
13
+ [MESSAGE_ID]: 'The `concurrency` option has no effect on a test without subtests. It only controls how a suite or a test\'s subtests run concurrently.',
14
+ };
15
+
16
+ /** @param {import('eslint').Rule.RuleContext} context */
17
+ const create = context => {
18
+ const imports = resolveImports(context);
19
+ if (!imports.isTestFile) {
20
+ return;
21
+ }
22
+
23
+ const tracker = createContextTracker(imports);
24
+
25
+ // One frame per test/subtest. A frame with a `concurrency` option but no subtests is misused.
26
+ const frames = [];
27
+
28
+ context.on('CallExpression', node => {
29
+ const isSubtest = tracker.isSubtestCall(node);
30
+
31
+ // Attribute this subtest to the frame that owns its receiver context, before it pushes its own.
32
+ if (isSubtest) {
33
+ const receiver = getSubtestReceiver(node).name;
34
+ const ownerFrame = frames.findLast(frame => frame.contextName === receiver);
35
+ if (ownerFrame) {
36
+ ownerFrame.hasSubtest = true;
37
+ }
38
+ }
39
+
40
+ const isTest = parseTestCall(node, imports)?.kind === 'test';
41
+ tracker.update(node);
42
+
43
+ if (isTest || isSubtest) {
44
+ frames.push({
45
+ node,
46
+ contextName: tracker.current(),
47
+ concurrencyProperty: findOptionsProperty(getTestOptions(node), 'concurrency'),
48
+ hasSubtest: false,
49
+ });
50
+ }
51
+ });
52
+
53
+ context.onExit('CallExpression', node => {
54
+ tracker.leave(node);
55
+
56
+ if (frames.at(-1)?.node !== node) {
57
+ return;
58
+ }
59
+
60
+ const frame = frames.pop();
61
+ if (frame.concurrencyProperty && !frame.hasSubtest) {
62
+ return {
63
+ node: frame.concurrencyProperty,
64
+ messageId: MESSAGE_ID,
65
+ };
66
+ }
67
+ });
68
+ };
69
+
70
+ /** @type {import('eslint').Rule.RuleModule} */
71
+ const config = {
72
+ create,
73
+ meta: {
74
+ type: 'problem',
75
+ docs: {
76
+ description: 'Disallow the `concurrency` option on a test without subtests.',
77
+ recommended: 'unopinionated',
78
+ },
79
+ schema: [],
80
+ messages,
81
+ languages: ['js/js'],
82
+ },
83
+ };
84
+
85
+ export default config;
@@ -0,0 +1,150 @@
1
+ import {resolveImports} from './utils/node-test.js';
2
+
3
+ const MESSAGE_ID = 'no-mock-timers-destructured-import';
4
+
5
+ const messages = {
6
+ [MESSAGE_ID]: '`{{name}}` is imported directly, so `mock.timers` cannot intercept it. Call the global `{{name}}` instead.',
7
+ };
8
+
9
+ const TIMER_MODULES = new Set(['node:timers', 'timers']);
10
+
11
+ // Imported timer function -> the `mock.timers` API name that mocks it.
12
+ const FUNCTION_TO_API = new Map([
13
+ ['setTimeout', 'setTimeout'],
14
+ ['clearTimeout', 'setTimeout'],
15
+ ['setInterval', 'setInterval'],
16
+ ['clearInterval', 'setInterval'],
17
+ ['setImmediate', 'setImmediate'],
18
+ ['clearImmediate', 'setImmediate'],
19
+ ]);
20
+
21
+ /*
22
+ The enabled timer APIs, or `null` when `enable()` was called without an `apis` list (all APIs).
23
+ */
24
+ function getEnabledApis(callExpression) {
25
+ const [argument] = callExpression.arguments;
26
+ if (argument?.type !== 'ObjectExpression') {
27
+ return null;
28
+ }
29
+
30
+ const apisProperty = argument.properties.find(property =>
31
+ property.type === 'Property'
32
+ && !property.computed
33
+ && (
34
+ (property.key.type === 'Identifier' && property.key.name === 'apis')
35
+ || (property.key.type === 'Literal' && property.key.value === 'apis')
36
+ ));
37
+
38
+ if (apisProperty?.value.type !== 'ArrayExpression') {
39
+ return null;
40
+ }
41
+
42
+ return apisProperty.value.elements
43
+ .filter(element => element?.type === 'Literal' && typeof element.value === 'string')
44
+ .map(element => element.value);
45
+ }
46
+
47
+ /** @param {import('eslint').Rule.RuleContext} context */
48
+ const create = context => {
49
+ const {sourceCode} = context;
50
+ const imports = resolveImports(context);
51
+ if (!imports.isTestFile) {
52
+ return;
53
+ }
54
+
55
+ // Named timer-function imports from `node:timers`.
56
+ const timerImports = [];
57
+ for (const node of sourceCode.ast.body) {
58
+ if (node.type !== 'ImportDeclaration' || !TIMER_MODULES.has(node.source.value)) {
59
+ continue;
60
+ }
61
+
62
+ for (const specifier of node.specifiers) {
63
+ if (
64
+ specifier.type === 'ImportSpecifier'
65
+ && specifier.imported.type === 'Identifier'
66
+ && FUNCTION_TO_API.has(specifier.imported.name)
67
+ ) {
68
+ timerImports.push(specifier);
69
+ }
70
+ }
71
+ }
72
+
73
+ if (timerImports.length === 0) {
74
+ return;
75
+ }
76
+
77
+ const {mockLocals} = imports;
78
+
79
+ // `mock.timers` (global) or `t.mock.timers` (context).
80
+ const isMockTimers = node =>
81
+ node.type === 'MemberExpression'
82
+ && !node.computed
83
+ && node.property.type === 'Identifier'
84
+ && node.property.name === 'timers'
85
+ && (
86
+ // `mock.timers` (global, named/renamed import).
87
+ (node.object.type === 'Identifier' && mockLocals.has(node.object.name))
88
+ // `t.mock.timers` (context) or `namespace.mock.timers`.
89
+ || (
90
+ node.object.type === 'MemberExpression'
91
+ && !node.object.computed
92
+ && node.object.property.type === 'Identifier'
93
+ && node.object.property.name === 'mock'
94
+ )
95
+ );
96
+
97
+ const enabledApis = new Set();
98
+ let isAllEnabled = false;
99
+
100
+ context.on('CallExpression', node => {
101
+ const {callee} = node;
102
+ if (
103
+ callee.type === 'MemberExpression'
104
+ && !callee.computed
105
+ && callee.property.type === 'Identifier'
106
+ && callee.property.name === 'enable'
107
+ && isMockTimers(callee.object)
108
+ ) {
109
+ const apis = getEnabledApis(node);
110
+ if (apis === null) {
111
+ isAllEnabled = true;
112
+ } else {
113
+ for (const api of apis) {
114
+ enabledApis.add(api);
115
+ }
116
+ }
117
+ }
118
+ });
119
+
120
+ context.onExit('Program', () => {
121
+ if (!isAllEnabled && enabledApis.size === 0) {
122
+ return;
123
+ }
124
+
125
+ return timerImports
126
+ .filter(specifier => isAllEnabled || enabledApis.has(FUNCTION_TO_API.get(specifier.imported.name)))
127
+ .map(specifier => ({
128
+ node: specifier,
129
+ messageId: MESSAGE_ID,
130
+ data: {name: specifier.imported.name},
131
+ }));
132
+ });
133
+ };
134
+
135
+ /** @type {import('eslint').Rule.RuleModule} */
136
+ const config = {
137
+ create,
138
+ meta: {
139
+ type: 'problem',
140
+ docs: {
141
+ description: 'Disallow destructured timer imports when using `mock.timers`.',
142
+ recommended: 'unopinionated',
143
+ },
144
+ schema: [],
145
+ messages,
146
+ languages: ['js/js'],
147
+ },
148
+ };
149
+
150
+ export default config;
@@ -0,0 +1,71 @@
1
+ import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
2
+
3
+ const MESSAGE_ID = 'no-nested-tests/error';
4
+
5
+ const messages = {
6
+ [MESSAGE_ID]: 'Do not define a test or suite inside a test body. Use `t.test()` for subtests instead.',
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 callback function nodes of the `test`/`it` calls we are currently inside.
17
+ // Suites (`describe`/`suite`) legitimately contain tests and nested suites, so they
18
+ // do not open a scope here — only a test/it body does.
19
+ const testCallbackStack = [];
20
+
21
+ context.on('CallExpression', node => {
22
+ const parsed = parseTestCall(node, imports);
23
+ if (!parsed) {
24
+ return;
25
+ }
26
+
27
+ // A test or suite defined inside a test body should be a subtest (`t.test()`).
28
+ if ((parsed.kind === 'test' || parsed.kind === 'suite') && testCallbackStack.length > 0) {
29
+ return {
30
+ node,
31
+ messageId: MESSAGE_ID,
32
+ };
33
+ }
34
+
35
+ if (parsed.kind === 'test') {
36
+ const callback = getTestCallback(node);
37
+ if (callback) {
38
+ testCallbackStack.push(callback);
39
+ }
40
+ }
41
+ });
42
+
43
+ context.onExit('CallExpression', node => {
44
+ const parsed = parseTestCall(node, imports);
45
+ if (!parsed || parsed.kind !== 'test') {
46
+ return;
47
+ }
48
+
49
+ const callback = getTestCallback(node);
50
+ if (callback && testCallbackStack.at(-1) === callback) {
51
+ testCallbackStack.pop();
52
+ }
53
+ });
54
+ };
55
+
56
+ /** @type {import('eslint').Rule.RuleModule} */
57
+ const config = {
58
+ create,
59
+ meta: {
60
+ type: 'problem',
61
+ docs: {
62
+ description: 'Disallow tests and suites nested inside a test body.',
63
+ recommended: 'unopinionated',
64
+ },
65
+ schema: [],
66
+ messages,
67
+ languages: ['js/js'],
68
+ },
69
+ };
70
+
71
+ 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: 'only',
6
+ description: 'Disallow the `.only` test modifier.',
7
+ errorMessage: 'Do not use the `.only` test modifier as it prevents the other tests from running.',
8
+ recommended: 'unopinionated',
9
+ });
10
+
11
+ export default config;