eslint-node-test 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/core-rule-replacements.js +9 -0
- package/configs/flat-config-base.js +9 -0
- package/index.d.ts +11 -0
- package/index.js +51 -0
- package/license +9 -0
- package/package.json +106 -0
- package/readme.md +143 -0
- package/rules/assertion-arguments.js +134 -0
- package/rules/ast/call-or-new-expression.js +100 -0
- package/rules/ast/function-types.js +7 -0
- package/rules/ast/index.js +17 -0
- package/rules/ast/is-expression-statement.js +7 -0
- package/rules/ast/is-function.js +5 -0
- package/rules/ast/is-loop.js +5 -0
- package/rules/ast/is-member-expression.js +98 -0
- package/rules/ast/is-method-call.js +62 -0
- package/rules/ast/literal.js +32 -0
- package/rules/ast/loop-types.js +9 -0
- package/rules/consistent-modifier-style.js +95 -0
- package/rules/consistent-test-context-name.js +75 -0
- package/rules/consistent-test-filename.js +70 -0
- package/rules/consistent-test-it.js +86 -0
- package/rules/fix/index.js +5 -0
- package/rules/fix/remove-argument.js +58 -0
- package/rules/fix/replace-member-expression-property.js +25 -0
- package/rules/hooks-order.js +132 -0
- package/rules/index.js +66 -0
- package/rules/max-assertions.js +87 -0
- package/rules/max-nested-describe.js +70 -0
- package/rules/no-assert-in-describe.js +51 -0
- package/rules/no-assert-in-hook.js +51 -0
- package/rules/no-assert-throws-async.js +114 -0
- package/rules/no-assert-throws-string.js +65 -0
- package/rules/no-async-describe.js +50 -0
- package/rules/no-async-fn-without-await.js +74 -0
- package/rules/no-callback-and-promise.js +56 -0
- package/rules/no-commented-tests.js +59 -0
- package/rules/no-conditional-assertion.js +101 -0
- package/rules/no-conditional-in-test.js +66 -0
- package/rules/no-conditional-tests.js +75 -0
- package/rules/no-conflicting-modifiers.js +73 -0
- package/rules/no-done-callback.js +58 -0
- package/rules/no-duplicate-hooks.js +75 -0
- package/rules/no-export.js +79 -0
- package/rules/no-identical-assertion-arguments.js +71 -0
- package/rules/no-identical-title.js +101 -0
- package/rules/no-incorrect-deep-equal.js +100 -0
- package/rules/no-incorrect-strict-equal.js +86 -0
- package/rules/no-loop-static-title.js +93 -0
- package/rules/no-misused-concurrency.js +85 -0
- package/rules/no-mock-timers-destructured-import.js +150 -0
- package/rules/no-nested-tests.js +71 -0
- package/rules/no-only-test.js +11 -0
- package/rules/no-skip-test.js +11 -0
- package/rules/no-skip-without-reason.js +88 -0
- package/rules/no-skip-without-return.js +127 -0
- package/rules/no-standalone-assert.js +51 -0
- package/rules/no-test-inside-hook.js +68 -0
- package/rules/no-test-return-statement.js +114 -0
- package/rules/no-todo-test.js +11 -0
- package/rules/no-unawaited-rejects.js +74 -0
- package/rules/no-unawaited-subtest.js +66 -0
- package/rules/no-unknown-test-options.js +77 -0
- package/rules/no-useless-assertion.js +47 -0
- package/rules/prefer-assert-match.js +245 -0
- package/rules/prefer-assert-throws.js +90 -0
- package/rules/prefer-async-await.js +203 -0
- package/rules/prefer-context-mock.js +59 -0
- package/rules/prefer-diagnostic.js +94 -0
- package/rules/prefer-equality-assertion.js +101 -0
- package/rules/prefer-hooks-on-top.js +73 -0
- package/rules/prefer-lowercase-title.js +119 -0
- package/rules/prefer-mock-method.js +115 -0
- package/rules/prefer-strict-assert.js +69 -0
- package/rules/prefer-test-context-assert.js +125 -0
- package/rules/prefer-todo.js +98 -0
- package/rules/require-assertion.js +92 -0
- package/rules/require-await-concurrent-subtests.js +119 -0
- package/rules/require-context-assert-with-plan.js +127 -0
- package/rules/require-hook.js +108 -0
- package/rules/require-throws-expectation.js +52 -0
- package/rules/require-top-level-describe.js +89 -0
- package/rules/rule/index.js +9 -0
- package/rules/rule/to-eslint-create.js +37 -0
- package/rules/rule/to-eslint-listener.js +40 -0
- package/rules/rule/to-eslint-problem.js +38 -0
- package/rules/rule/to-eslint-rule-fixer.js +49 -0
- package/rules/rule/to-eslint-rule.js +38 -0
- package/rules/rule/to-eslint-rules.js +10 -0
- package/rules/rule/unicorn-context.js +36 -0
- package/rules/rule/unicorn-listeners.js +65 -0
- package/rules/rule/utilities.js +26 -0
- package/rules/shared/test-modifier-rule.js +92 -0
- package/rules/test-title-format.js +86 -0
- package/rules/test-title.js +139 -0
- package/rules/utils/contains-suspension-point.js +35 -0
- package/rules/utils/escape-string.js +24 -0
- package/rules/utils/get-comments.js +15 -0
- package/rules/utils/get-documentation-url.js +9 -0
- package/rules/utils/get-enclosing-function.js +18 -0
- package/rules/utils/index.js +16 -0
- package/rules/utils/is-conditional-branch.js +37 -0
- package/rules/utils/is-promise-type.js +28 -0
- package/rules/utils/is-same-reference.js +179 -0
- package/rules/utils/is-value-not-usable.js +5 -0
- package/rules/utils/node-test.js +713 -0
- package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +80 -0
- package/rules/utils/parentheses/iterate-surrounding-parentheses.js +82 -0
- package/rules/utils/parentheses/parentheses.js +69 -0
- package/rules/utils/types.js +5 -0
- package/rules/utils/unwrap-typescript-expression.js +16 -0
- package/rules/valid-describe-callback.js +63 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import toEslintListener from './to-eslint-listener.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import {EslintListers, ListenerType, Listener} from './to-eslint-create.js'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class UnicornListeners {
|
|
8
|
+
#context;
|
|
9
|
+
#listeners = new Map();
|
|
10
|
+
|
|
11
|
+
constructor(context) {
|
|
12
|
+
this.#context = context;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#addEventListener(selectors, listener) {
|
|
16
|
+
const listeners = this.#listeners;
|
|
17
|
+
for (const selector of selectors) {
|
|
18
|
+
if (listeners.has(selector)) {
|
|
19
|
+
listeners.get(selector).push(listener);
|
|
20
|
+
} else {
|
|
21
|
+
listeners.set(selector, [listener]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
@param {ListenerType | ListenerType[]} selectorOrSelectors
|
|
28
|
+
@param {Listener} listener
|
|
29
|
+
*/
|
|
30
|
+
on(selectorOrSelectors, listener) {
|
|
31
|
+
const selectors = Array.isArray(selectorOrSelectors) ? selectorOrSelectors : [selectorOrSelectors];
|
|
32
|
+
this.#addEventListener(selectors, listener);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
@param {ListenerType | ListenerType[]} selectorOrSelectors
|
|
37
|
+
@param {Listener} listener
|
|
38
|
+
*/
|
|
39
|
+
onExit(selectorOrSelectors, listener) {
|
|
40
|
+
const selectors = Array.isArray(selectorOrSelectors) ? selectorOrSelectors : [selectorOrSelectors];
|
|
41
|
+
this.#addEventListener(selectors.map(selector => `${selector}:exit`), listener);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
@returns {EslintListers}
|
|
46
|
+
*/
|
|
47
|
+
toEslintListeners() {
|
|
48
|
+
const eslintListeners = {};
|
|
49
|
+
|
|
50
|
+
for (const [selector, listeners] of this.#listeners) {
|
|
51
|
+
eslintListeners[selector] = toEslintListener(
|
|
52
|
+
this.#context,
|
|
53
|
+
function * (...listenerArguments) {
|
|
54
|
+
for (const listener of listeners) {
|
|
55
|
+
yield listener(...listenerArguments);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return eslintListeners;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default UnicornListeners;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const isIterable = object => typeof object?.[Symbol.iterator] === 'function';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import * as ESLint from 'eslint';
|
|
5
|
+
@import {UnicornReportFixer} from './to-eslint-rule-fixer.js';
|
|
6
|
+
@import {UnicornProblems, UnicornProblem} from './to-eslint-problem.js';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
Iterate ESLint fix or ESLint problem
|
|
11
|
+
|
|
12
|
+
@template {UnicornReportFixer | UnicornProblems} ValueType
|
|
13
|
+
|
|
14
|
+
@param {ValueType} value
|
|
15
|
+
@returns {IterableIterator<ValueType extends UnicornReportFixer ? ESLint.Rule.Fix : UnicornProblem>}
|
|
16
|
+
*/
|
|
17
|
+
export function * iterateFixOrProblems(value) {
|
|
18
|
+
if (!isIterable(value)) {
|
|
19
|
+
yield value;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const element of value) {
|
|
24
|
+
yield * iterateFixOrProblems(element);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
findModifier,
|
|
5
|
+
getTestOptions,
|
|
6
|
+
findEnabledOptionsProperty,
|
|
7
|
+
} from '../utils/node-test.js';
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Shared logic for rules that disallow a single test modifier (`only`/`skip`/`todo`).
|
|
11
|
+
|
|
12
|
+
A modifier can be applied two ways in `node:test`:
|
|
13
|
+
- As a chained property: `test.only(…)`.
|
|
14
|
+
- As an options-object property: `test('title', {only: true}, () => {})`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
@param {{
|
|
19
|
+
modifier: 'only' | 'skip' | 'todo',
|
|
20
|
+
description: string,
|
|
21
|
+
errorMessage: string,
|
|
22
|
+
recommended: 'unopinionated' | boolean,
|
|
23
|
+
}} options
|
|
24
|
+
@returns {import('eslint').Rule.RuleModule}
|
|
25
|
+
*/
|
|
26
|
+
export default function createTestModifierRule({modifier, description, errorMessage, recommended}) {
|
|
27
|
+
const MESSAGE_ID_ERROR = `no-${modifier}-test/error`;
|
|
28
|
+
const MESSAGE_ID_SUGGESTION = `no-${modifier}-test/suggestion`;
|
|
29
|
+
const messages = {
|
|
30
|
+
[MESSAGE_ID_ERROR]: errorMessage,
|
|
31
|
+
[MESSAGE_ID_SUGGESTION]: `Remove \`.${modifier}\`.`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
35
|
+
const create = context => {
|
|
36
|
+
const {sourceCode} = context;
|
|
37
|
+
const imports = resolveImports(context);
|
|
38
|
+
if (!imports.isTestFile) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
context.on('CallExpression', node => {
|
|
43
|
+
const parsed = parseTestCall(node, imports);
|
|
44
|
+
if (!parsed) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const modifierNode = findModifier(parsed.modifiers, modifier);
|
|
49
|
+
if (modifierNode) {
|
|
50
|
+
return {
|
|
51
|
+
node: modifierNode,
|
|
52
|
+
messageId: MESSAGE_ID_ERROR,
|
|
53
|
+
suggest: [
|
|
54
|
+
{
|
|
55
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
56
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
|
57
|
+
fix(fixer) {
|
|
58
|
+
const dotToken = sourceCode.getTokenBefore(modifierNode);
|
|
59
|
+
return fixer.removeRange([sourceCode.getRange(dotToken)[0], sourceCode.getRange(modifierNode)[1]]);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const property = findEnabledOptionsProperty(getTestOptions(node), modifier);
|
|
67
|
+
if (property) {
|
|
68
|
+
return {
|
|
69
|
+
node: property,
|
|
70
|
+
messageId: MESSAGE_ID_ERROR,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
create,
|
|
78
|
+
meta: {
|
|
79
|
+
type: 'problem',
|
|
80
|
+
docs: {
|
|
81
|
+
description,
|
|
82
|
+
recommended,
|
|
83
|
+
},
|
|
84
|
+
hasSuggestions: true,
|
|
85
|
+
schema: [],
|
|
86
|
+
messages,
|
|
87
|
+
languages: [
|
|
88
|
+
'js/js',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestTitle,
|
|
5
|
+
getStaticString,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'test-title-format/mismatch';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'Test title does not match the required format: `{{format}}`.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
15
|
+
const create = context => {
|
|
16
|
+
const imports = resolveImports(context);
|
|
17
|
+
if (!imports.isTestFile) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const formatOption = context.options[0]?.format;
|
|
22
|
+
if (!formatOption) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let titleRegExp;
|
|
27
|
+
try {
|
|
28
|
+
titleRegExp = new RegExp(formatOption, 'v');
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Invalid \`format\` option for \`test-title-format\`: ${error.message}`, {cause: error});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
context.on('CallExpression', node => {
|
|
34
|
+
const parsed = parseTestCall(node, imports);
|
|
35
|
+
if (!parsed || parsed.kind === 'hook') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const titleNode = getTestTitle(node, context);
|
|
40
|
+
if (!titleNode) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const titleValue = getStaticString(titleNode, context);
|
|
45
|
+
if (titleValue === undefined) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!titleRegExp.test(titleValue)) {
|
|
50
|
+
return {
|
|
51
|
+
node,
|
|
52
|
+
messageId: MESSAGE_ID,
|
|
53
|
+
data: {format: String(titleRegExp)},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
60
|
+
const config = {
|
|
61
|
+
create,
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'suggestion',
|
|
64
|
+
docs: {
|
|
65
|
+
description: 'Require test titles to match a configured pattern.',
|
|
66
|
+
recommended: false,
|
|
67
|
+
},
|
|
68
|
+
schema: [
|
|
69
|
+
{
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
format: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'A regular expression pattern that test titles must match.',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
defaultOptions: [{}],
|
|
81
|
+
messages,
|
|
82
|
+
languages: ['js/js'],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default config;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestTitle,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import {escapeString} from './utils/index.js';
|
|
8
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID_MISSING = 'test-title/missing';
|
|
11
|
+
const MESSAGE_ID_NOT_STRING = 'test-title/not-string';
|
|
12
|
+
const MESSAGE_ID_EMPTY = 'test-title/empty';
|
|
13
|
+
const MESSAGE_ID_WHITESPACE = 'test-title/whitespace';
|
|
14
|
+
|
|
15
|
+
const messages = {
|
|
16
|
+
[MESSAGE_ID_MISSING]: 'Test must have a title.',
|
|
17
|
+
[MESSAGE_ID_NOT_STRING]: 'Test title must be a string.',
|
|
18
|
+
[MESSAGE_ID_EMPTY]: 'Test title must not be empty.',
|
|
19
|
+
[MESSAGE_ID_WHITESPACE]: 'Test title must not have leading or trailing whitespace.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Validate the resolved static title string of a `titleNode` (a string `Literal` or `TemplateLiteral`).
|
|
24
|
+
Returns a problem for an empty or untrimmed title, or `undefined` when it is fine or not statically
|
|
25
|
+
resolvable.
|
|
26
|
+
*/
|
|
27
|
+
function getStaticTitleProblem(titleNode) {
|
|
28
|
+
let titleValue;
|
|
29
|
+
if (titleNode.type === 'Literal') {
|
|
30
|
+
titleValue = titleNode.value;
|
|
31
|
+
} else if (titleNode.type === 'TemplateLiteral' && titleNode.expressions.length === 0) {
|
|
32
|
+
titleValue = titleNode.quasis[0].value.cooked;
|
|
33
|
+
} else {
|
|
34
|
+
// A template literal with expressions or some other dynamic node — can't validate.
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (titleValue === null || titleValue === undefined) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (titleValue.trim() === '') {
|
|
43
|
+
return {
|
|
44
|
+
node: titleNode,
|
|
45
|
+
messageId: MESSAGE_ID_EMPTY,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (titleValue !== titleValue.trim()) {
|
|
50
|
+
const trimmed = titleValue.trim();
|
|
51
|
+
// Preserve the original string delimiter; a template literal (no expressions here) becomes
|
|
52
|
+
// a normal single-quoted string.
|
|
53
|
+
const quote = titleNode.type === 'Literal' ? titleNode.raw[0] : '\'';
|
|
54
|
+
return {
|
|
55
|
+
node: titleNode,
|
|
56
|
+
messageId: MESSAGE_ID_WHITESPACE,
|
|
57
|
+
fix: fixer => fixer.replaceText(titleNode, escapeString(trimmed, quote)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
63
|
+
const create = context => {
|
|
64
|
+
const imports = resolveImports(context);
|
|
65
|
+
if (!imports.isTestFile) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
context.on('CallExpression', node => {
|
|
70
|
+
const parsed = parseTestCall(node, imports);
|
|
71
|
+
if (!parsed || parsed.kind === 'hook') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const firstArgument = node.arguments[0];
|
|
76
|
+
|
|
77
|
+
// No arguments at all — truly empty call, skip.
|
|
78
|
+
if (!firstArgument) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If the first argument is the implementation (callback), title is missing.
|
|
83
|
+
const callback = getTestCallback(node);
|
|
84
|
+
if (callback && callback === firstArgument) {
|
|
85
|
+
return {
|
|
86
|
+
node,
|
|
87
|
+
messageId: MESSAGE_ID_MISSING,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// First arg is an options object (possibly TypeScript-wrapped) with no preceding string title.
|
|
92
|
+
if (unwrapTypeScriptExpression(firstArgument).type === 'ObjectExpression') {
|
|
93
|
+
return {
|
|
94
|
+
node,
|
|
95
|
+
messageId: MESSAGE_ID_MISSING,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const titleNode = getTestTitle(node, context);
|
|
100
|
+
|
|
101
|
+
// First argument exists but is not a string (e.g. a number, boolean).
|
|
102
|
+
if (!titleNode) {
|
|
103
|
+
// If first argument is a function or identifier pointing to a fn, that's already handled above.
|
|
104
|
+
// For non-string literals (numbers, booleans, null), report.
|
|
105
|
+
if (
|
|
106
|
+
firstArgument.type === 'Literal'
|
|
107
|
+
&& typeof firstArgument.value !== 'string'
|
|
108
|
+
) {
|
|
109
|
+
return {
|
|
110
|
+
node: firstArgument,
|
|
111
|
+
messageId: MESSAGE_ID_NOT_STRING,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Dynamic/computed title — can't validate statically, skip.
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return getStaticTitleProblem(titleNode);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
124
|
+
const config = {
|
|
125
|
+
create,
|
|
126
|
+
meta: {
|
|
127
|
+
type: 'problem',
|
|
128
|
+
docs: {
|
|
129
|
+
description: 'Require tests to have a title.',
|
|
130
|
+
recommended: true,
|
|
131
|
+
},
|
|
132
|
+
fixable: 'code',
|
|
133
|
+
schema: [],
|
|
134
|
+
messages,
|
|
135
|
+
languages: ['js/js'],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export default config;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {isFunction} from '../ast/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Check whether `node` or any of its descendants, excluding nested functions, is a suspension point (`await`, `for await…of`, or `yield`).
|
|
5
|
+
|
|
6
|
+
Nested functions are not descended into, since their suspension points belong to a different function.
|
|
7
|
+
|
|
8
|
+
@param {import('estree').Node} node
|
|
9
|
+
@param {import('eslint').SourceCode['visitorKeys']} visitorKeys
|
|
10
|
+
@returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export default function containsSuspensionPoint(node, visitorKeys) {
|
|
13
|
+
if (
|
|
14
|
+
node.type === 'AwaitExpression'
|
|
15
|
+
|| node.type === 'YieldExpression'
|
|
16
|
+
|| (node.type === 'ForOfStatement' && node.await)
|
|
17
|
+
) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (isFunction(node)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
26
|
+
const child = node[key];
|
|
27
|
+
for (const childNode of Array.isArray(child) ? child : [child]) {
|
|
28
|
+
if (childNode?.type && containsSuspensionPoint(childNode, visitorKeys)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import jsesc from 'jsesc';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Escape string and wrap the result in quotes.
|
|
5
|
+
|
|
6
|
+
@param {string} string - The string to be quoted.
|
|
7
|
+
@param {string} [quote] - The quote character.
|
|
8
|
+
@returns {string} - The quoted and escaped string.
|
|
9
|
+
*/
|
|
10
|
+
export default function escapeString(string, quote = '\'') {
|
|
11
|
+
/* c8 ignore start */
|
|
12
|
+
if (typeof string !== 'string') {
|
|
13
|
+
throw new TypeError('Unexpected string.');
|
|
14
|
+
}
|
|
15
|
+
/* c8 ignore end */
|
|
16
|
+
|
|
17
|
+
return jsesc(string, {
|
|
18
|
+
quotes: quote === '"' ? 'double' : 'single',
|
|
19
|
+
wrap: true,
|
|
20
|
+
es6: true,
|
|
21
|
+
minimal: true,
|
|
22
|
+
lowercaseHex: false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Get all comments in the file, regardless of language. JavaScript/TypeScript expose them via `sourceCode.getAllComments()`, while non-JavaScript languages (for example CSS via `@eslint/css`) expose them on `sourceCode.comments`. Some plugins like `@html-eslint` have `getAllComments()` but it returns `[]` — in that case we fall back to `sourceCode.comments`.
|
|
3
|
+
|
|
4
|
+
@param {import('eslint').Rule.RuleContext} context
|
|
5
|
+
@returns {Array<object>}
|
|
6
|
+
*/
|
|
7
|
+
export default function getComments(context) {
|
|
8
|
+
const {sourceCode} = context;
|
|
9
|
+
const fromGetAllComments = sourceCode.getAllComments?.();
|
|
10
|
+
if (fromGetAllComments?.length) {
|
|
11
|
+
return fromGetAllComments;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return sourceCode.comments ?? [];
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import packageJson from '../../package.json' with {type: 'json'};
|
|
3
|
+
|
|
4
|
+
const repoUrl = 'https://github.com/sindresorhus/eslint-node-test';
|
|
5
|
+
|
|
6
|
+
export default function getDocumentationUrl(filename) {
|
|
7
|
+
const ruleName = path.basename(filename, '.js');
|
|
8
|
+
return `${repoUrl}/blob/v${packageJson.version}/docs/rules/${ruleName}.md`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import isFunction from '../ast/is-function.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Walk up the ancestor chain to the nearest enclosing function node.
|
|
5
|
+
Returns `undefined` if the program root is reached first.
|
|
6
|
+
|
|
7
|
+
@param {import('estree').Node} node
|
|
8
|
+
@returns {import('estree').Function | undefined}
|
|
9
|
+
*/
|
|
10
|
+
export default function getEnclosingFunction(node) {
|
|
11
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
12
|
+
if (isFunction(current)) {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
isParenthesized,
|
|
3
|
+
getParentheses,
|
|
4
|
+
getParenthesizedRange,
|
|
5
|
+
} from './parentheses/parentheses.js';
|
|
6
|
+
|
|
7
|
+
export {default as containsSuspensionPoint} from './contains-suspension-point.js';
|
|
8
|
+
export {default as escapeString} from './escape-string.js';
|
|
9
|
+
export {default as getComments} from './get-comments.js';
|
|
10
|
+
export {default as isPromiseType} from './is-promise-type.js';
|
|
11
|
+
export {default as isSameReference} from './is-same-reference.js';
|
|
12
|
+
export {default as isValueNotUsable} from './is-value-not-usable.js';
|
|
13
|
+
export {default as unwrapTypeScriptExpression, isTypeScriptExpressionWrapper} from './unwrap-typescript-expression.js';
|
|
14
|
+
export {isUnknownType} from './types.js';
|
|
15
|
+
export {default as isConditionalBranch} from './is-conditional-branch.js';
|
|
16
|
+
export {default as getEnclosingFunction} from './get-enclosing-function.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Whether `child` sits in a conditionally-executed branch of its parent `ancestor`, as opposed to the
|
|
3
|
+
always-evaluated condition/test. Covers `if`/ternary (consequent or alternate), logical `&&`/`||`/`??`
|
|
4
|
+
(the short-circuited right side), and `switch` (a case body). Pass `includeLoops` to also treat a
|
|
5
|
+
loop body as conditional, since it may run zero times.
|
|
6
|
+
*/
|
|
7
|
+
export default function isConditionalBranch(ancestor, child, {includeLoops = false} = {}) {
|
|
8
|
+
switch (ancestor.type) {
|
|
9
|
+
case 'IfStatement':
|
|
10
|
+
case 'ConditionalExpression': {
|
|
11
|
+
// Only the consequent/alternate are conditional; the test always runs.
|
|
12
|
+
return child !== ancestor.test;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
case 'LogicalExpression': {
|
|
16
|
+
// Only the right-hand side is conditional (it may be short-circuited).
|
|
17
|
+
return child === ancestor.right;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
case 'SwitchCase': {
|
|
21
|
+
// The case body is conditional; the case's test expression is not.
|
|
22
|
+
return ancestor.consequent.includes(child);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case 'WhileStatement':
|
|
26
|
+
case 'DoWhileStatement':
|
|
27
|
+
case 'ForStatement':
|
|
28
|
+
case 'ForInStatement':
|
|
29
|
+
case 'ForOfStatement': {
|
|
30
|
+
return includeLoops && child === ancestor.body;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
default: {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {isUnknownType} from './types.js';
|
|
2
|
+
|
|
3
|
+
// Best-effort check whether a TypeScript type is a promise. Returns `true`/`false` when known, `undefined` when indeterminate.
|
|
4
|
+
function isPromiseType(type, checker) {
|
|
5
|
+
type = checker.getNonNullableType(type);
|
|
6
|
+
|
|
7
|
+
if (isUnknownType(type)) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (type.isUnion()) {
|
|
12
|
+
const results = type.types.map(type => isPromiseType(type, checker));
|
|
13
|
+
|
|
14
|
+
if (results.every(Boolean)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (results.every(result => result === false)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return checker.getPromisedTypeOfPromise(type) !== undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default isPromiseType;
|