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,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
createContextTracker,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'require-context-assert-with-plan';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'This assertion is not counted by `{{context}}.plan()`. Use `{{context}}.assert` so the runner counts it toward the plan.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Get the context name of a `<context>.plan(…)` call, or `undefined`. */
|
|
15
|
+
function getPlanContextName(node) {
|
|
16
|
+
const {callee} = node;
|
|
17
|
+
if (
|
|
18
|
+
callee.type === 'MemberExpression'
|
|
19
|
+
&& !callee.computed
|
|
20
|
+
&& callee.property.type === 'Identifier'
|
|
21
|
+
&& callee.property.name === 'plan'
|
|
22
|
+
&& callee.object.type === 'Identifier'
|
|
23
|
+
) {
|
|
24
|
+
return callee.object.name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a call is the `<context>.assert.method(…)` form, which the plan does count. */
|
|
31
|
+
function isContextAssertCall(node, tracker) {
|
|
32
|
+
const {callee} = node;
|
|
33
|
+
return (
|
|
34
|
+
callee.type === 'MemberExpression'
|
|
35
|
+
&& callee.object.type === 'MemberExpression'
|
|
36
|
+
&& !callee.object.computed
|
|
37
|
+
&& callee.object.property.type === 'Identifier'
|
|
38
|
+
&& callee.object.property.name === 'assert'
|
|
39
|
+
&& callee.object.object.type === 'Identifier'
|
|
40
|
+
&& tracker.isContextName(callee.object.object.name)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
45
|
+
const create = context => {
|
|
46
|
+
const imports = resolveImports(context);
|
|
47
|
+
// Without a `node:assert` import the only assertions are `t.assert.*` (which count toward the
|
|
48
|
+
// plan and are excluded below), so there is nothing to report.
|
|
49
|
+
if (!imports.isTestFile || !imports.hasAssert) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tracker = createContextTracker(imports);
|
|
54
|
+
|
|
55
|
+
// One frame per enclosing test/subtest. Assertions attach to the innermost; the frame is
|
|
56
|
+
// reported only if its test called `plan()`.
|
|
57
|
+
const frames = [];
|
|
58
|
+
|
|
59
|
+
context.on('CallExpression', node => {
|
|
60
|
+
const isTest = parseTestCall(node, imports)?.kind === 'test' || tracker.isSubtestCall(node);
|
|
61
|
+
tracker.update(node);
|
|
62
|
+
|
|
63
|
+
if (isTest) {
|
|
64
|
+
frames.push({
|
|
65
|
+
node, contextName: tracker.current(), hasPlan: false, assertions: [],
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (frames.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const planContextName = getPlanContextName(node);
|
|
75
|
+
if (planContextName !== undefined) {
|
|
76
|
+
// Mark the innermost frame whose test owns this context.
|
|
77
|
+
for (let index = frames.length - 1; index >= 0; index -= 1) {
|
|
78
|
+
if (frames[index].contextName === planContextName) {
|
|
79
|
+
frames[index].hasPlan = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (parseAssertionCall(node, imports) && !isContextAssertCall(node, tracker)) {
|
|
88
|
+
frames.at(-1).assertions.push(node);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
context.onExit('CallExpression', node => {
|
|
93
|
+
tracker.leave(node);
|
|
94
|
+
|
|
95
|
+
if (frames.at(-1)?.node !== node) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const frame = frames.pop();
|
|
100
|
+
if (!frame.hasPlan || frame.contextName === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return frame.assertions.map(assertion => ({
|
|
105
|
+
node: assertion,
|
|
106
|
+
messageId: MESSAGE_ID,
|
|
107
|
+
data: {context: frame.contextName},
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
113
|
+
const config = {
|
|
114
|
+
create,
|
|
115
|
+
meta: {
|
|
116
|
+
type: 'problem',
|
|
117
|
+
docs: {
|
|
118
|
+
description: 'Require assertions to use the test context when the test sets a plan.',
|
|
119
|
+
recommended: 'unopinionated',
|
|
120
|
+
},
|
|
121
|
+
schema: [],
|
|
122
|
+
messages,
|
|
123
|
+
languages: ['js/js'],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default config;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import isFunction from './ast/is-function.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID = 'require-hook';
|
|
10
|
+
|
|
11
|
+
const messages = {
|
|
12
|
+
[MESSAGE_ID]: 'This runs when the file is loaded, not as part of a test. Move it into a `before`, `beforeEach`, `after`, or `afterEach` hook.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
Whether the statement sits directly in a registration-time scope: the module top level or a
|
|
17
|
+
`describe`/`suite` body. Statements inside a test/hook callback or a helper function are fine.
|
|
18
|
+
*/
|
|
19
|
+
function isInRegistrationScope(statement, imports) {
|
|
20
|
+
const {parent} = statement;
|
|
21
|
+
if (parent.type === 'Program') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (parent.type !== 'BlockStatement') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const callback = parent.parent;
|
|
30
|
+
if (
|
|
31
|
+
!isFunction(callback)
|
|
32
|
+
|| callback.parent?.type !== 'CallExpression'
|
|
33
|
+
|| getTestCallback(callback.parent) !== callback
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parseTestCall(callback.parent, imports)?.kind === 'suite';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
42
|
+
const create = context => {
|
|
43
|
+
const {sourceCode} = context;
|
|
44
|
+
const imports = resolveImports(context);
|
|
45
|
+
if (!imports.isTestFile) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const allow = new Set(context.options[0].allow);
|
|
50
|
+
|
|
51
|
+
context.on('ExpressionStatement', node => {
|
|
52
|
+
const call = node.expression;
|
|
53
|
+
if (call.type !== 'CallExpression') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The test/suite/hook registration calls themselves belong here.
|
|
58
|
+
if (parseTestCall(call, imports)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Misplaced assertions are reported by `no-assert-in-describe`.
|
|
63
|
+
if (parseAssertionCall(call, imports)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (allow.has(sourceCode.getText(call.callee))) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isInRegistrationScope(node, imports)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {node, messageId: MESSAGE_ID};
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
80
|
+
const config = {
|
|
81
|
+
create,
|
|
82
|
+
meta: {
|
|
83
|
+
type: 'suggestion',
|
|
84
|
+
docs: {
|
|
85
|
+
description: 'Require setup and teardown code to be inside a hook.',
|
|
86
|
+
recommended: false,
|
|
87
|
+
},
|
|
88
|
+
schema: [
|
|
89
|
+
{
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
allow: {
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: {type: 'string'},
|
|
95
|
+
uniqueItems: true,
|
|
96
|
+
description: 'Callee expressions allowed at the top level (for example, `["console.log"]`).',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
defaultOptions: [{allow: []}],
|
|
103
|
+
messages,
|
|
104
|
+
languages: ['js/js'],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default config;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'require-throws-expectation';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: '`{{method}}()` accepts any thrown value. Pass an error matcher (error class, `RegExp`, validation object, or function) as the second argument.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const THROWS_METHODS = new Set(['throws', 'rejects']);
|
|
10
|
+
|
|
11
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
12
|
+
const create = context => {
|
|
13
|
+
const imports = resolveImports(context);
|
|
14
|
+
if (!imports.isAssertOrTestFile) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
context.on('CallExpression', node => {
|
|
19
|
+
const parsed = parseAssertionCall(node, imports);
|
|
20
|
+
if (!parsed || !THROWS_METHODS.has(parsed.method)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Only the single-argument form lacks a matcher. A spread could expand to one.
|
|
25
|
+
if (node.arguments.length !== 1 || node.arguments[0].type === 'SpreadElement') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
node,
|
|
31
|
+
messageId: MESSAGE_ID,
|
|
32
|
+
data: {method: parsed.method},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
38
|
+
const config = {
|
|
39
|
+
create,
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'problem',
|
|
42
|
+
docs: {
|
|
43
|
+
description: 'Require an error matcher for `assert.throws()`/`assert.rejects()`.',
|
|
44
|
+
recommended: 'unopinionated',
|
|
45
|
+
},
|
|
46
|
+
schema: [],
|
|
47
|
+
messages,
|
|
48
|
+
languages: ['js/js'],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default config;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, createSuiteDepthTracker} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID_NOT_WRAPPED = 'require-top-level-describe/not-wrapped';
|
|
4
|
+
const MESSAGE_ID_TOO_MANY = 'require-top-level-describe/too-many';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID_NOT_WRAPPED]: 'A {{kind}} must be placed inside a top-level `describe`.',
|
|
8
|
+
[MESSAGE_ID_TOO_MANY]: 'There should be no more than {{max}} top-level `describe` blocks in a file.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
12
|
+
const create = context => {
|
|
13
|
+
const imports = resolveImports(context);
|
|
14
|
+
if (!imports.isTestFile) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const maxTopLevelDescribes = context.options[0]?.maxTopLevelDescribes;
|
|
19
|
+
|
|
20
|
+
const tracker = createSuiteDepthTracker();
|
|
21
|
+
let topLevelDescribeCount = 0;
|
|
22
|
+
|
|
23
|
+
context.on('CallExpression', node => {
|
|
24
|
+
const parsed = parseTestCall(node, imports);
|
|
25
|
+
if (!parsed) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let problem;
|
|
30
|
+
if (tracker.depth === 0) {
|
|
31
|
+
if (parsed.kind === 'test' || parsed.kind === 'hook') {
|
|
32
|
+
problem = {
|
|
33
|
+
node,
|
|
34
|
+
messageId: MESSAGE_ID_NOT_WRAPPED,
|
|
35
|
+
data: {kind: parsed.kind === 'hook' ? 'hook' : 'test'},
|
|
36
|
+
};
|
|
37
|
+
} else if (parsed.kind === 'suite') {
|
|
38
|
+
topLevelDescribeCount += 1;
|
|
39
|
+
if (maxTopLevelDescribes !== undefined && topLevelDescribeCount > maxTopLevelDescribes) {
|
|
40
|
+
problem = {
|
|
41
|
+
node,
|
|
42
|
+
messageId: MESSAGE_ID_TOO_MANY,
|
|
43
|
+
data: {max: maxTopLevelDescribes},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (parsed.kind === 'suite') {
|
|
50
|
+
tracker.enterSuite(node);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return problem;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
context.onExit('CallExpression', node => {
|
|
57
|
+
tracker.exitSuite(node);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
62
|
+
const config = {
|
|
63
|
+
create,
|
|
64
|
+
meta: {
|
|
65
|
+
type: 'suggestion',
|
|
66
|
+
docs: {
|
|
67
|
+
description: 'Require tests and hooks to be inside a top-level `describe`.',
|
|
68
|
+
recommended: false,
|
|
69
|
+
},
|
|
70
|
+
schema: [
|
|
71
|
+
{
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
maxTopLevelDescribes: {
|
|
75
|
+
type: 'integer',
|
|
76
|
+
minimum: 1,
|
|
77
|
+
description: 'The maximum number of top-level `describe` blocks allowed in a file.',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
defaultOptions: [{}],
|
|
84
|
+
messages,
|
|
85
|
+
languages: ['js/js'],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default config;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import * as ESLint from 'eslint';
|
|
3
|
+
@import {UnicornCreate} from './to-eslint-create.js';
|
|
4
|
+
@import {UnicornRule} from './to-eslint-rule.js';
|
|
5
|
+
@import {UnicornContext} from './unicorn-context.js';
|
|
6
|
+
@import {TSESTree as Estree} from '@typescript-eslint/types';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {default as toEslintRules} from './to-eslint-rules.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import createUnicornContext from './unicorn-context.js';
|
|
3
|
+
import UnicornListeners from './unicorn-listeners.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
@import * as ESLint from 'eslint';
|
|
7
|
+
@import {UnicornContext} from './unicorn-context.js';
|
|
8
|
+
@import {EslintListers, ListenerType, EslintListener} from './to-eslint-listener.js'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
@typedef {ESLint.Rule.RuleModule['create']} EslintCreate
|
|
13
|
+
@typedef {(context: UnicornContext) => void} UnicornCreate
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
Convert Unicorn style of `create` to ESLint style
|
|
18
|
+
|
|
19
|
+
@param {UnicornCreate} unicornCreate
|
|
20
|
+
@returns {EslintCreate}
|
|
21
|
+
*/
|
|
22
|
+
function toEslintCreate(unicornCreate) {
|
|
23
|
+
return eslintContext => {
|
|
24
|
+
const unicornListeners = new UnicornListeners(eslintContext);
|
|
25
|
+
const unicornContext = createUnicornContext(eslintContext, unicornListeners);
|
|
26
|
+
|
|
27
|
+
const result = unicornCreate(unicornContext);
|
|
28
|
+
|
|
29
|
+
assert.equal(result, undefined, `[${eslintContext.id}] Rule \`create\` function should return \`undefined\`, please use \`context.on()\` instead of return listeners.`);
|
|
30
|
+
|
|
31
|
+
const eslintListeners = unicornListeners.toEslintListeners();
|
|
32
|
+
|
|
33
|
+
return eslintListeners;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default toEslintCreate;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {iterateFixOrProblems} from './utilities.js';
|
|
2
|
+
import toEslintProblem from './to-eslint-problem.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
@import * as ESLint from 'eslint';
|
|
6
|
+
@import {UnicornContext} from './unicorn-context.js'
|
|
7
|
+
@import {UnicornProblems} from './to-eslint-problem.js'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
@typedef {ESLint.Rule.RuleListener} EslintListers
|
|
12
|
+
@typedef {keyof EslintListers} ListenerType
|
|
13
|
+
@typedef {EslintListers[ListenerType]} EslintListener
|
|
14
|
+
@typedef {(...listenerArguments: Parameters<EslintListener>) => UnicornProblems} UnicornRuleListen
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
@param {UnicornContext} context
|
|
19
|
+
@param {UnicornRuleListen} listener
|
|
20
|
+
@returns {Listener}
|
|
21
|
+
*/
|
|
22
|
+
function toEslintListener(context, listener) {
|
|
23
|
+
// Listener arguments can be `codePath, node` or `node`
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
@type {UnicornRuleListen}
|
|
27
|
+
*/
|
|
28
|
+
return (...listenerArguments) => {
|
|
29
|
+
const unicornProblems = listener(...listenerArguments);
|
|
30
|
+
|
|
31
|
+
for (const unicornProblem of iterateFixOrProblems(unicornProblems)) {
|
|
32
|
+
if (unicornProblem) {
|
|
33
|
+
const eslintProblem = toEslintProblem(unicornProblem);
|
|
34
|
+
context.report(eslintProblem);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default toEslintListener;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import toEslintFixer from './to-eslint-rule-fixer.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import * as ESLint from 'eslint';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
@typedef {Parameters<ESLint.Rule.RuleContext['report']>[0]} EslintProblem
|
|
9
|
+
@typedef {EslintProblem} UnicornProblem
|
|
10
|
+
@typedef {EslintProblem | undefined | EslintProblem[] | IterableIterator<EslintProblem>} UnicornProblems
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
@param {UnicornProblem} unicornProblem
|
|
15
|
+
@returns {EslintProblem}
|
|
16
|
+
*/
|
|
17
|
+
function toEslintProblem(unicornProblem) {
|
|
18
|
+
const eslintProblem = {...unicornProblem};
|
|
19
|
+
|
|
20
|
+
if (unicornProblem.fix) {
|
|
21
|
+
eslintProblem.fix = toEslintFixer(unicornProblem.fix);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(unicornProblem.suggest)) {
|
|
25
|
+
eslintProblem.suggest = unicornProblem.suggest.map(unicornSuggest => ({
|
|
26
|
+
...unicornSuggest,
|
|
27
|
+
fix: toEslintFixer(unicornSuggest.fix),
|
|
28
|
+
data: {
|
|
29
|
+
...unicornProblem.data,
|
|
30
|
+
...unicornSuggest.data,
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return eslintProblem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default toEslintProblem;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {iterateFixOrProblems} from './utilities.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import * as ESLint from 'eslint';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class FixAbortError extends Error {
|
|
8
|
+
name = 'FixAbortError';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fixOptions = {
|
|
12
|
+
abort() {
|
|
13
|
+
throw new FixAbortError('Fix aborted.');
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
@typedef {ESLint.Rule.ReportFixer | undefined} EslintReportFixer
|
|
19
|
+
@typedef {EslintReportFixer | IterableIterator<EslintReportFixer>} UnicornReportFixer
|
|
20
|
+
@typedef {(fixer: ESLint.Rule.RuleFixer, options: typeof fixOptions) => UnicornReportFixer} UnicornRuleFixer
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
Convert Unicorn style fix function to ESLint style fix function
|
|
25
|
+
|
|
26
|
+
@param {UnicornRuleFixer} fix
|
|
27
|
+
@returns {ESLint.Rule.RuleFixer}
|
|
28
|
+
*/
|
|
29
|
+
function toEslintRuleFixer(fix) {
|
|
30
|
+
/** @param {UnicornReportFixer} fixer */
|
|
31
|
+
return fixer => {
|
|
32
|
+
const unicornReport = fix(fixer, fixOptions);
|
|
33
|
+
|
|
34
|
+
const eslintReport = iterateFixOrProblems(unicornReport);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return [...eslintReport];
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof FixAbortError) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* c8 ignore next */
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default toEslintRuleFixer;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import getDocumentationUrl from '../utils/get-documentation-url.js';
|
|
2
|
+
import toEslintCreate from './to-eslint-create.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
@import * as ESLint from 'eslint';
|
|
6
|
+
@import {UnicornCreate} from './to-eslint-create.js';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
@typedef {ESLint.Rule.RuleModule & {
|
|
11
|
+
create: UnicornCreate
|
|
12
|
+
}} UnicornRule
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
Convert Unicorn rule to ESLint rule
|
|
17
|
+
|
|
18
|
+
@param {string} ruleId
|
|
19
|
+
@param {UnicornRule} unicornRule
|
|
20
|
+
@returns {ESLint.Rule.RuleModule}
|
|
21
|
+
*/
|
|
22
|
+
function toEslintRule(ruleId, unicornRule) {
|
|
23
|
+
return {
|
|
24
|
+
meta: {
|
|
25
|
+
// If there are no options, add `[]` so ESLint can validate that no data is passed to the rule.
|
|
26
|
+
// https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md
|
|
27
|
+
schema: [],
|
|
28
|
+
...unicornRule.meta,
|
|
29
|
+
docs: {
|
|
30
|
+
...unicornRule.meta.docs,
|
|
31
|
+
url: getDocumentationUrl(ruleId),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
create: toEslintCreate(unicornRule.create),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default toEslintRule;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import * as ESLint from 'eslint';
|
|
3
|
+
@import {UnicornListeners, ListenerType, Listener} from './to-eslint-create.js'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
@typedef {(type: ListenerType | ListenerType[], listener: Listener) => ReturnType<Listener>} UnicornRuleListen
|
|
8
|
+
@typedef {ESLint.Rule.RuleContext & {
|
|
9
|
+
on: UnicornRuleListen
|
|
10
|
+
onExit: UnicornRuleListen
|
|
11
|
+
}} UnicornContext
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
Create a better `Context` object with `on` and `onExit` method to add listeners
|
|
16
|
+
|
|
17
|
+
@param {ESLint.Rule.RuleContext} eslintContext
|
|
18
|
+
@param {UnicornListeners} listeners
|
|
19
|
+
@returns {UnicornContext}
|
|
20
|
+
*/
|
|
21
|
+
function createUnicornContext(eslintContext, listeners) {
|
|
22
|
+
/** @type {UnicornContext} */
|
|
23
|
+
const context = new Proxy(eslintContext, {
|
|
24
|
+
get(target, property, receiver) {
|
|
25
|
+
if (property === 'on' || property === 'onExit') {
|
|
26
|
+
return listeners[property].bind(listeners);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Reflect.get(target, property, receiver);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default createUnicornContext;
|