@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,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
isAssertionCallWithSupportedContext,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'require-throws-expectation';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: '`{{method}}()` accepts any thrown value. Pass an error matcher (error class, `RegExp`, validation object, or function) as the second argument.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const THROWS_METHODS = new Set(['throws', 'rejects']);
|
|
15
|
+
|
|
16
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
17
|
+
const create = context => {
|
|
18
|
+
const imports = resolveImports(context);
|
|
19
|
+
if (!imports.isAssertOrTestFile) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
24
|
+
|
|
25
|
+
context.on('CallExpression', node => {
|
|
26
|
+
tracker.update(node);
|
|
27
|
+
|
|
28
|
+
const parsed = parseAssertionCall(node, imports);
|
|
29
|
+
if (!parsed || !THROWS_METHODS.has(parsed.method) || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Only the single-argument form lacks a matcher. A spread could expand to one.
|
|
34
|
+
if (node.arguments.length !== 1 || node.arguments[0].type === 'SpreadElement') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
node,
|
|
40
|
+
messageId: MESSAGE_ID,
|
|
41
|
+
data: {method: parsed.method},
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
context.onExit('CallExpression', node => {
|
|
46
|
+
tracker.leave(node);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
51
|
+
const config = {
|
|
52
|
+
create,
|
|
53
|
+
meta: {
|
|
54
|
+
type: 'problem',
|
|
55
|
+
docs: {
|
|
56
|
+
description: 'Require an error matcher for `assert.throws()`/`assert.rejects()`.',
|
|
57
|
+
recommended: 'unopinionated',
|
|
58
|
+
},
|
|
59
|
+
schema: [],
|
|
60
|
+
messages,
|
|
61
|
+
languages: ['js/js'],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default config;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
resolveImports,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
createContextTracker,
|
|
6
|
+
isAssertionCallWithSupportedContext,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
import isFunction from './ast/is-function.js';
|
|
9
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
10
|
+
|
|
11
|
+
const MESSAGE_ID = 'require-throws-validator-return-true';
|
|
12
|
+
|
|
13
|
+
const messages = {
|
|
14
|
+
[MESSAGE_ID]: 'Validator function passed to `assert.{{method}}()` must return the boolean value `true`.',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const THROWS_METHODS = new Set(['throws', 'rejects']);
|
|
18
|
+
const NON_TRUE_NODE_TYPES = new Set([
|
|
19
|
+
'ArrayExpression',
|
|
20
|
+
'ArrowFunctionExpression',
|
|
21
|
+
'ClassExpression',
|
|
22
|
+
'FunctionExpression',
|
|
23
|
+
'NewExpression',
|
|
24
|
+
'ObjectExpression',
|
|
25
|
+
'TemplateLiteral',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function isAssertionCall(node, imports) {
|
|
29
|
+
node = unwrapTypeScriptExpression(node);
|
|
30
|
+
return node.type === 'CallExpression' && parseAssertionCall(node, imports) !== undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isPromiseStaticCall(node) {
|
|
34
|
+
return node.type === 'CallExpression'
|
|
35
|
+
&& node.callee.type === 'MemberExpression'
|
|
36
|
+
&& !node.callee.computed
|
|
37
|
+
&& node.callee.object.type === 'Identifier'
|
|
38
|
+
&& node.callee.object.name === 'Promise'
|
|
39
|
+
&& node.callee.property.type === 'Identifier';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canReturnTrue(node, context, imports) {
|
|
43
|
+
if (!node) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
node = unwrapTypeScriptExpression(node);
|
|
48
|
+
|
|
49
|
+
if (isAssertionCall(node, imports)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isPromiseStaticCall(node)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const staticValue = getStaticValue(node, context.sourceCode.getScope(node));
|
|
58
|
+
if (staticValue) {
|
|
59
|
+
return staticValue.value === true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Any other expression (identifier, call, member access, conditional, …) could resolve to `true`.
|
|
63
|
+
return !NON_TRUE_NODE_TYPES.has(node.type);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasReturnTrue(functionBody, context, imports) {
|
|
67
|
+
function walk(node) {
|
|
68
|
+
if (node.type === 'ReturnStatement') {
|
|
69
|
+
return canReturnTrue(node.argument, context, imports);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node !== functionBody && isFunction(node)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const key of context.sourceCode.visitorKeys[node.type] ?? []) {
|
|
77
|
+
const child = node[key];
|
|
78
|
+
for (const childNode of Array.isArray(child) ? child : [child]) {
|
|
79
|
+
if (childNode?.type && walk(childNode)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return walk(functionBody);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function canValidatorReturnTrue(validator, context, imports) {
|
|
92
|
+
if (validator.async || validator.generator) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (validator.body.type !== 'BlockStatement') {
|
|
97
|
+
return canReturnTrue(validator.body, context, imports);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return hasReturnTrue(validator.body, context, imports);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
104
|
+
const create = context => {
|
|
105
|
+
const imports = resolveImports(context);
|
|
106
|
+
if (!imports.isAssertOrTestFile) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
111
|
+
|
|
112
|
+
context.on('CallExpression', node => {
|
|
113
|
+
tracker.update(node);
|
|
114
|
+
|
|
115
|
+
const parsed = parseAssertionCall(node, imports);
|
|
116
|
+
if (!parsed || !THROWS_METHODS.has(parsed.method) || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const validator = unwrapTypeScriptExpression(node.arguments[1]);
|
|
121
|
+
if (!validator || !isFunction(validator)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (canValidatorReturnTrue(validator, context, imports)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
node: validator,
|
|
131
|
+
messageId: MESSAGE_ID,
|
|
132
|
+
data: {method: parsed.method},
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
context.onExit('CallExpression', node => {
|
|
137
|
+
tracker.leave(node);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
142
|
+
const config = {
|
|
143
|
+
create,
|
|
144
|
+
meta: {
|
|
145
|
+
type: 'problem',
|
|
146
|
+
docs: {
|
|
147
|
+
description: 'Require validator functions in `assert.throws()`/`assert.rejects()` to return `true`.',
|
|
148
|
+
recommended: 'unopinionated',
|
|
149
|
+
},
|
|
150
|
+
schema: [],
|
|
151
|
+
messages,
|
|
152
|
+
languages: ['js/js'],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
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,35 @@
|
|
|
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
|
+
export default 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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import toEslintProblem from './to-eslint-problem.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import * as ESLint from 'eslint';
|
|
5
|
+
@import {UnicornContext} from './unicorn-context.js'
|
|
6
|
+
@import {UnicornProblems} from './to-eslint-problem.js'
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
@typedef {ESLint.Rule.RuleListener} EslintListers
|
|
11
|
+
@typedef {keyof EslintListers} ListenerType
|
|
12
|
+
@typedef {EslintListers[ListenerType]} EslintListener
|
|
13
|
+
@typedef {(...listenerArguments: Parameters<EslintListener>) => UnicornProblems} UnicornRuleListen
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
Report a listener's return value: `undefined` (the common case), a single problem object, or an array/iterable of problems (possibly nested). Kept generator-free — this runs for every visited node of every rule, and the overwhelming majority of calls report nothing.
|
|
18
|
+
|
|
19
|
+
@param {ESLint.Rule.RuleContext} context
|
|
20
|
+
@param {UnicornProblems} problems
|
|
21
|
+
*/
|
|
22
|
+
function reportProblems(context, problems) {
|
|
23
|
+
if (!problems) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A single problem object is a plain object (not iterable), so report it directly.
|
|
28
|
+
if (typeof problems[Symbol.iterator] !== 'function') {
|
|
29
|
+
context.report(toEslintProblem(problems));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const problem of problems) {
|
|
34
|
+
reportProblems(context, problem);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
@param {UnicornContext} context
|
|
40
|
+
@param {UnicornRuleListen[]} listeners
|
|
41
|
+
@returns {EslintListener}
|
|
42
|
+
*/
|
|
43
|
+
export default function toEslintListener(context, listeners) {
|
|
44
|
+
// Forward positional arguments explicitly instead of a rest array, since this runs for every
|
|
45
|
+
// visited node and a rest/spread would allocate on each call. ESLint passes at most three
|
|
46
|
+
// arguments to a listener (`onCodePathSegmentLoop`); node listeners get a single `node`.
|
|
47
|
+
return (argument0, argument1, argument2) => {
|
|
48
|
+
for (const listener of listeners) {
|
|
49
|
+
reportProblems(context, listener(argument0, argument1, argument2));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
export default 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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
export default 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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
export default 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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
export default 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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import toEslintListener from './to-eslint-listener.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import {EslintListers, ListenerType, Listener} from './to-eslint-create.js'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export default 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(this.#context, listeners);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return eslintListeners;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|