@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.
- package/dist-cjser/index.cjs +8561 -0
- package/index.d.ts +11 -0
- package/index.js +49 -0
- package/license +9 -0
- package/package.json +141 -0
- package/readme.md +172 -0
- package/rules/assertion-arguments.js +147 -0
- package/rules/ast/call-or-new-expression.js +94 -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 +92 -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-assert-style.js +169 -0
- package/rules/consistent-assert-throws-callback-style.js +241 -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 +84 -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 +147 -0
- package/rules/index.js +84 -0
- package/rules/max-assertions.js +88 -0
- package/rules/max-nested-describe.js +70 -0
- package/rules/no-assert-in-describe.js +61 -0
- package/rules/no-assert-in-hook.js +61 -0
- package/rules/no-assert-match-string.js +166 -0
- package/rules/no-assert-throws-async.js +113 -0
- package/rules/no-assert-throws-call.js +106 -0
- package/rules/no-assert-throws-multiple-statements.js +194 -0
- package/rules/no-assert-throws-string.js +78 -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 +64 -0
- package/rules/no-compound-assertion.js +131 -0
- package/rules/no-conditional-assertion.js +108 -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-constant-assertion.js +218 -0
- package/rules/no-done-callback.js +58 -0
- package/rules/no-duplicate-assertions.js +172 -0
- package/rules/no-duplicate-hooks.js +75 -0
- package/rules/no-duplicate-plan.js +209 -0
- package/rules/no-export.js +45 -0
- package/rules/no-identical-assertion-arguments.js +84 -0
- package/rules/no-identical-title.js +101 -0
- package/rules/no-incorrect-deep-equal.js +113 -0
- package/rules/no-incorrect-strict-equal.js +99 -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-parent-test-context.js +244 -0
- package/rules/no-process-env-mutation.js +495 -0
- package/rules/no-process-exit-in-test.js +96 -0
- package/rules/no-skip-test.js +11 -0
- package/rules/no-skip-without-reason.js +90 -0
- package/rules/no-skip-without-return.js +123 -0
- package/rules/no-sleep-in-test.js +482 -0
- package/rules/no-snapshot-in-loop.js +132 -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 +217 -0
- package/rules/no-todo-test.js +11 -0
- package/rules/no-unawaited-promise-assertion.js +366 -0
- package/rules/no-unawaited-rejects.js +87 -0
- package/rules/no-unawaited-subtest.js +66 -0
- package/rules/no-unknown-test-options.js +77 -0
- package/rules/no-useless-assertion.js +60 -0
- package/rules/prefer-assert-match.js +253 -0
- package/rules/prefer-assert-throws.js +97 -0
- package/rules/prefer-async-await.js +203 -0
- package/rules/prefer-context-mock.js +55 -0
- package/rules/prefer-diagnostic.js +94 -0
- package/rules/prefer-equality-assertion.js +124 -0
- package/rules/prefer-hooks-on-top.js +75 -0
- package/rules/prefer-lowercase-title.js +119 -0
- package/rules/prefer-mock-method.js +115 -0
- package/rules/prefer-strict-assert.js +82 -0
- package/rules/prefer-test-context-assert.js +162 -0
- package/rules/prefer-todo.js +98 -0
- package/rules/require-assertion.js +99 -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-mock-timers-advance.js +313 -0
- package/rules/require-mock-timers-apis.js +383 -0
- package/rules/require-throws-expectation.js +65 -0
- package/rules/require-throws-validator-return-true.js +156 -0
- package/rules/require-top-level-describe.js +89 -0
- package/rules/rule/index.js +9 -0
- package/rules/rule/to-eslint-create.js +35 -0
- package/rules/rule/to-eslint-listener.js +52 -0
- package/rules/rule/to-eslint-problem.js +36 -0
- package/rules/rule/to-eslint-rule-fixer.js +47 -0
- package/rules/rule/to-eslint-rule.js +36 -0
- package/rules/rule/to-eslint-rules.js +8 -0
- package/rules/rule/unicorn-context.js +34 -0
- package/rules/rule/unicorn-listeners.js +56 -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/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 +15 -0
- package/rules/utils/is-conditional-branch.js +37 -0
- package/rules/utils/is-promise-type.js +26 -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 +883 -0
- package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +78 -0
- package/rules/utils/parentheses/iterate-surrounding-parentheses.js +80 -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,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 quoteJsString from '@cjser/quote-js-string';
|
|
2
|
+
import {
|
|
3
|
+
resolveImports,
|
|
4
|
+
parseTestCall,
|
|
5
|
+
getTestTitle,
|
|
6
|
+
getTestCallback,
|
|
7
|
+
} from './utils/node-test.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, quoteJsString(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,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,15 @@
|
|
|
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 getComments} from './get-comments.js';
|
|
9
|
+
export {default as isPromiseType} from './is-promise-type.js';
|
|
10
|
+
export {default as isSameReference} from './is-same-reference.js';
|
|
11
|
+
export {default as isValueNotUsable} from './is-value-not-usable.js';
|
|
12
|
+
export {default as unwrapTypeScriptExpression, isTypeScriptExpressionWrapper} from './unwrap-typescript-expression.js';
|
|
13
|
+
export {isUnknownType} from './types.js';
|
|
14
|
+
export {default as isConditionalBranch} from './is-conditional-branch.js';
|
|
15
|
+
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,26 @@
|
|
|
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
|
+
export default 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
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
|
|
3
|
+
// Copied from https://github.com/eslint/eslint/blob/94ba68d76a6940f68ff82eea7332c6505f93df76/lib/rules/utils/ast-utils.js#L392
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Gets the property name of a given node.
|
|
7
|
+
The node can be a MemberExpression, a Property, or a MethodDefinition.
|
|
8
|
+
|
|
9
|
+
If the name is dynamic, this returns `null`.
|
|
10
|
+
|
|
11
|
+
For examples:
|
|
12
|
+
|
|
13
|
+
a.b // => "b"
|
|
14
|
+
a["b"] // => "b"
|
|
15
|
+
a['b'] // => "b"
|
|
16
|
+
a[`b`] // => "b"
|
|
17
|
+
a[100] // => "100"
|
|
18
|
+
a[b] // => null
|
|
19
|
+
a["a" + "b"] // => null
|
|
20
|
+
a[tag`b`] // => null
|
|
21
|
+
a[`${b}`] // => null
|
|
22
|
+
|
|
23
|
+
let a = {b: 1} // => "b"
|
|
24
|
+
let a = {["b"]: 1} // => "b"
|
|
25
|
+
let a = {['b']: 1} // => "b"
|
|
26
|
+
let a = {[`b`]: 1} // => "b"
|
|
27
|
+
let a = {[100]: 1} // => "100"
|
|
28
|
+
let a = {[b]: 1} // => null
|
|
29
|
+
let a = {["a" + "b"]: 1} // => null
|
|
30
|
+
let a = {[tag`b`]: 1} // => null
|
|
31
|
+
let a = {[`${b}`]: 1} // => null
|
|
32
|
+
@param {ASTNode} node The node to get.
|
|
33
|
+
@returns {string|undefined} The property name if static. Otherwise, undefined.
|
|
34
|
+
*/
|
|
35
|
+
function getStaticPropertyName(node) {
|
|
36
|
+
/* c8 ignore next 3 */
|
|
37
|
+
while (node?.type === 'ChainExpression') {
|
|
38
|
+
node = node.expression;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let property;
|
|
42
|
+
|
|
43
|
+
switch (node?.type) {
|
|
44
|
+
case 'MemberExpression': {
|
|
45
|
+
property = node.property;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Only reachable when use this to get class/object member key
|
|
50
|
+
/* c8 ignore next */
|
|
51
|
+
case 'Property':
|
|
52
|
+
case 'MethodDefinition': {
|
|
53
|
+
/* c8 ignore next 2 */
|
|
54
|
+
property = node.key;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// No default
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (property) {
|
|
62
|
+
if (property.type === 'Identifier' && !node.computed) {
|
|
63
|
+
return property.name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const staticResult = getStaticValue(property);
|
|
67
|
+
if (!staticResult) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return String(staticResult.value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
Check if two literal nodes are the same value.
|
|
77
|
+
@param {ASTNode} left The Literal node to compare.
|
|
78
|
+
@param {ASTNode} right The other Literal node to compare.
|
|
79
|
+
@returns {boolean} `true` if the two literal nodes are the same value.
|
|
80
|
+
*/
|
|
81
|
+
function equalLiteralValue(left, right) {
|
|
82
|
+
// RegExp literal.
|
|
83
|
+
if (left.regex || right.regex) {
|
|
84
|
+
return Boolean(left.regex
|
|
85
|
+
&& right.regex
|
|
86
|
+
&& left.regex.pattern === right.regex.pattern
|
|
87
|
+
&& left.regex.flags === right.regex.flags);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// BigInt literal.
|
|
91
|
+
if (left.bigint || right.bigint) {
|
|
92
|
+
return left.bigint === right.bigint;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return left.value === right.value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
Unwrap ChainExpression (`?.`) and TypeScript expression wrapper (`as`, `satisfies`, `<Type>`, or `!`) nodes.
|
|
100
|
+
@param {ASTNode} node The node to unwrap.
|
|
101
|
+
@returns {ASTNode} The unwrapped node.
|
|
102
|
+
*/
|
|
103
|
+
function unwrapNode(node) {
|
|
104
|
+
while (
|
|
105
|
+
[
|
|
106
|
+
'ChainExpression',
|
|
107
|
+
'TSAsExpression',
|
|
108
|
+
'TSSatisfiesExpression',
|
|
109
|
+
'TSTypeAssertion',
|
|
110
|
+
'TSNonNullExpression',
|
|
111
|
+
].includes(node.type)
|
|
112
|
+
) {
|
|
113
|
+
node = node.expression;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return node;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
Check if two expressions reference the same value. For example:
|
|
121
|
+
a = a
|
|
122
|
+
a.b = a.b
|
|
123
|
+
a[0] = a[0]
|
|
124
|
+
a['b'] = a['b']
|
|
125
|
+
@param {ASTNode} left The left side of the comparison.
|
|
126
|
+
@param {ASTNode} right The right side of the comparison.
|
|
127
|
+
@returns {boolean} `true` if both sides match and reference the same value.
|
|
128
|
+
*/
|
|
129
|
+
export default function isSameReference(left, right) {
|
|
130
|
+
left = unwrapNode(left);
|
|
131
|
+
right = unwrapNode(right);
|
|
132
|
+
|
|
133
|
+
if (left.type !== right.type) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
switch (left.type) {
|
|
138
|
+
case 'Super':
|
|
139
|
+
case 'ThisExpression': {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case 'Identifier':
|
|
144
|
+
case 'PrivateIdentifier': {
|
|
145
|
+
return left.name === right.name;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'Literal': {
|
|
149
|
+
return equalLiteralValue(left, right);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'MemberExpression': {
|
|
153
|
+
const nameA = getStaticPropertyName(left);
|
|
154
|
+
|
|
155
|
+
// `x.y = x["y"]`
|
|
156
|
+
if (nameA !== undefined) {
|
|
157
|
+
return (
|
|
158
|
+
isSameReference(left.object, right.object)
|
|
159
|
+
&& nameA === getStaticPropertyName(right)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
`x[0] = x[0]`
|
|
165
|
+
`x[y] = x[y]`
|
|
166
|
+
`x.y = x.y`
|
|
167
|
+
*/
|
|
168
|
+
return (
|
|
169
|
+
left.computed === right.computed
|
|
170
|
+
&& isSameReference(left.object, right.object)
|
|
171
|
+
&& isSameReference(left.property, right.property)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
default: {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|