Skip to content

Commit a4a8e86

Browse files
authored
Merge pull request #415 from lo1tuma/reuse-core-prefer-arrow-callback
Reuse core prefer-arrow-callback
2 parents d2e88bf + 79e908a commit a4a8e86

4 files changed

Lines changed: 323 additions & 669 deletions

File tree

documentation/rules/prefer-arrow-callback.md

Lines changed: 31 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
<!-- end auto-generated rule header -->
88

9-
This rule is a variation of the core eslint `prefer-arrow-callback` rule that is mocha-aware and does not flag non-arrow callbacks within mocha functions.
9+
This rule is a Mocha-aware drop-in replacement for ESLint's core
10+
[`prefer-arrow-callback`](https://eslint.org/docs/latest/rules/prefer-arrow-callback) rule.
1011

11-
You will want to disable the original `prefer-arrow-callback` rule and configure the mocha-friendly replacement under the rules section.
12+
Use it instead of the core rule when linting Mocha tests. It keeps the core rule's behavior, options,
13+
and fixes, but does not report the callback functions passed directly to Mocha suites, tests, and hooks.
1214

1315
```json
1416
{
@@ -19,136 +21,55 @@ You will want to disable the original `prefer-arrow-callback` rule and configure
1921
}
2022
```
2123

22-
## Rule Overview
23-
24-
Arrow functions can be an attractive alternative to function expressions for callbacks or function arguments.
25-
26-
For example, arrow functions are automatically bound to their surrounding scope/context. This provides an alternative to the pre-ES6 standard of explicitly binding function expressions to achieve similar behavior.
27-
28-
Additionally, arrow functions are:
29-
30-
- less verbose, and easier to reason about.
31-
32-
- bound lexically regardless of where or when they are invoked.
33-
3424
## Rule Details
3525

36-
This rule locates function expressions used as callbacks or function arguments. An error will be produced for any that could be replaced by an arrow function without changing the result.
26+
This rule behaves like ESLint's core `prefer-arrow-callback`, except that direct callbacks for Mocha
27+
functions are allowed.
3728

38-
The following examples **will** be flagged:
29+
These patterns are considered correct:
3930

4031
```js
4132
/* eslint mocha/prefer-arrow-callback: "error" */
4233

43-
foo(function (a) {
44-
return a;
45-
}); // ERROR
46-
// prefer: foo(a => a)
34+
describe("suite", function () {
35+
beforeEach(function () {
36+
setup();
37+
});
4738

48-
foo(function () {
49-
return this.a;
50-
}
51-
.bind(this)); // ERROR
52-
// prefer: foo(() => this.a)
39+
it("works", function () {
40+
runAssertion();
41+
});
42+
});
5343
```
5444

55-
Instances where an arrow function would not produce identical results will be ignored.
56-
57-
The following examples **will not** be flagged:
45+
Non-Mocha callbacks are still checked, even when they appear inside Mocha callbacks:
5846

5947
```js
6048
/* eslint mocha/prefer-arrow-callback: "error" */
61-
/* eslint-env es6 */
62-
63-
// arrow function callback
64-
foo((a) => a); // OK
65-
66-
// generator as callback
67-
foo(function* () {
68-
yield;
69-
}); // OK
70-
71-
// function expression not used as callback or function argument
72-
var foo = function foo(a) {
73-
return a;
74-
}; // OK
75-
76-
// unbound function expression callback
77-
foo(function () {
78-
return this.a;
79-
}); // OK
80-
81-
// recursive named function callback
82-
foo(function bar(n) {
83-
return n && n + bar(n - 1);
84-
}); // OK
85-
86-
// mocha suite definition callback
87-
describe('test suite', function () {
88-
return Promise.resolve();
89-
}); // OK
90-
91-
// mocha hook callback
92-
beforeEach('before each test', function () {
93-
return Promise.resolve();
94-
}); // OK
95-
96-
// mocha test case callback
97-
it('should resolve', function () {
98-
return Promise.resolve();
99-
}); // OK
100-
```
101-
102-
## Options
103-
104-
Access further control over this rule's behavior via an options object.
105-
106-
Default: `{ allowNamedFunctions: false, allowUnboundThis: true }`
107-
108-
### allowNamedFunctions
10949

110-
By default `{ "allowNamedFunctions": false }`, this `boolean` option prohibits using named functions as callbacks or function arguments.
111-
112-
Changing this value to `true` will reverse this option's behavior by allowing use of named functions without restriction.
113-
114-
`{ "allowNamedFunctions": true }` **will not** flag the following example:
115-
116-
```js
117-
/* eslint mocha/prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
118-
119-
foo(function bar() {});
50+
it("works", function () {
51+
foo(function () {
52+
bar();
53+
}); // ERROR
54+
});
12055
```
12156

122-
### allowUnboundThis
123-
124-
By default `{ "allowUnboundThis": true }`, this `boolean` option allows function expressions containing `this` to be used as callbacks, as long as the function in question has not been explicitly bound.
125-
126-
When set to `false` this option prohibits the use of function expressions as callbacks or function arguments entirely, without exception.
127-
128-
`{ "allowUnboundThis": false }` **will** flag the following examples:
57+
## Options
12958

130-
```js
131-
/* eslint mocha/prefer-arrow-callback: [ "error", { "allowUnboundThis": false } ] */
132-
/* eslint-env es6 */
59+
This rule supports the same options as ESLint's core
60+
[`prefer-arrow-callback`](https://eslint.org/docs/latest/rules/prefer-arrow-callback#options) rule:
13361

134-
foo(function () {
135-
this.a;
136-
});
62+
- `allowNamedFunctions`
63+
- `allowUnboundThis`
13764

138-
foo(function () {
139-
(() => this);
140-
});
141-
142-
someArray.map(function (itm) {
143-
return this.doSomething(itm);
144-
}, someObject);
145-
```
65+
The only behavior change is the Mocha-specific exemption described above.
14666

14767
## When Not To Use It
14868

149-
- In environments that have not yet adopted ES6 language features (ES3/5).
150-
- In ES6+ environments that allow the use of function expressions when describing callbacks or function arguments.
69+
- If you are not linting Mocha code.
70+
- If you want the core rule to report Mocha suite, test, and hook callbacks as well.
15171

15272
## Further Reading
15373

154-
- [More on ES6 arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)
74+
- [ESLint core `prefer-arrow-callback`](https://eslint.org/docs/latest/rules/prefer-arrow-callback)
75+
- [Mocha and arrow functions](https://mochajs.org/#arrow-functions)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { Linter, type Rule } from 'eslint';
2+
import { builtinRules } from 'eslint/use-at-your-own-risk';
3+
import assert from 'node:assert';
4+
import type * as preferArrowCallbackModule from './prefer-arrow-callback.js';
5+
6+
const builtinRuleName = 'prefer-arrow-callback';
7+
const originalGet = builtinRules.get.bind(builtinRules);
8+
9+
type PreferArrowCallbackModule = typeof preferArrowCallbackModule;
10+
11+
function isPreferArrowCallbackModule(value: unknown): value is PreferArrowCallbackModule {
12+
return typeof value === 'object' &&
13+
value !== null &&
14+
'preferArrowCallbackRule' in value;
15+
}
16+
17+
async function importPreferArrowCallbackRule(uniqueKey: string): Promise<PreferArrowCallbackModule> {
18+
const importedModule: unknown = await import(`./prefer-arrow-callback.js?${uniqueKey}=${Date.now()}`);
19+
20+
assert.ok(isPreferArrowCallbackModule(importedModule));
21+
22+
return importedModule;
23+
}
24+
25+
describe('prefer-arrow-callback rule wrapper', function () {
26+
it('throws when the ESLint core prefer-arrow-callback rule cannot be loaded', async function () {
27+
try {
28+
builtinRules.get = function (name) {
29+
return name === builtinRuleName ? undefined : originalGet(name);
30+
};
31+
32+
await assert.rejects(
33+
async function () {
34+
await importPreferArrowCallbackRule('missing-core-rule');
35+
},
36+
function (error: unknown) {
37+
return error instanceof Error &&
38+
error.message === 'Unable to load the ESLint core "prefer-arrow-callback" rule.';
39+
}
40+
);
41+
} finally {
42+
builtinRules.get = originalGet;
43+
}
44+
});
45+
46+
it('falls back to default metadata when the core rule metadata is incomplete', async function () {
47+
try {
48+
builtinRules.get = function (name) {
49+
if (name === builtinRuleName) {
50+
return {
51+
meta: {},
52+
create() {
53+
return {};
54+
}
55+
};
56+
}
57+
58+
return originalGet(name);
59+
};
60+
61+
const { preferArrowCallbackRule } = await importPreferArrowCallbackRule('fallback-metadata');
62+
63+
assert.deepStrictEqual(preferArrowCallbackRule.meta, {
64+
type: 'suggestion',
65+
docs: {
66+
description: 'Require using arrow functions for callbacks',
67+
recommended: false,
68+
url: 'https://github.com/lo1tuma/eslint-plugin-mocha/blob/main/documentation/rules/prefer-arrow-callback.md'
69+
},
70+
defaultOptions: [],
71+
schema: [],
72+
fixable: undefined,
73+
hasSuggestions: undefined,
74+
messages: {
75+
preferArrowCallback: 'Unexpected function expression.'
76+
}
77+
});
78+
} finally {
79+
builtinRules.get = originalGet;
80+
}
81+
});
82+
83+
it('forwards rule context helper methods to the wrapped core rule', async function () {
84+
const observedCalls: string[] = [];
85+
const reportedMessages: string[] = [];
86+
87+
try {
88+
builtinRules.get = function (name) {
89+
if (name === builtinRuleName) {
90+
return {
91+
meta: {
92+
type: 'problem',
93+
docs: {
94+
description: 'stub rule'
95+
},
96+
defaultOptions: [],
97+
schema: [],
98+
fixable: 'code',
99+
hasSuggestions: true,
100+
messages: {
101+
preferArrowCallback: 'stub message'
102+
}
103+
},
104+
create(ruleContext: Rule.RuleContext) {
105+
const [node] = ruleContext.sourceCode.ast.body as [Rule.Node?];
106+
const reportNode = node ?? ruleContext.sourceCode.ast;
107+
108+
observedCalls.push('getAncestors');
109+
ruleContext.getAncestors();
110+
observedCalls.push('getDeclaredVariables');
111+
ruleContext.getDeclaredVariables(reportNode);
112+
observedCalls.push('getFilename');
113+
ruleContext.getFilename();
114+
observedCalls.push('getPhysicalFilename');
115+
ruleContext.getPhysicalFilename();
116+
observedCalls.push('getCwd');
117+
ruleContext.getCwd();
118+
observedCalls.push('getScope');
119+
ruleContext.getScope();
120+
observedCalls.push('getSourceCode');
121+
ruleContext.getSourceCode();
122+
observedCalls.push('markVariableAsUsed');
123+
ruleContext.markVariableAsUsed('foo');
124+
observedCalls.push('report');
125+
ruleContext.report({
126+
node: reportNode,
127+
message: 'wrapped report'
128+
});
129+
130+
return {};
131+
}
132+
};
133+
}
134+
135+
return originalGet(name);
136+
};
137+
138+
const { preferArrowCallbackRule } = await importPreferArrowCallbackRule('forwarded-context');
139+
const linter = new Linter({ configType: 'flat' });
140+
const text = 'foo();';
141+
linter.verify(text, [{
142+
languageOptions: { ecmaVersion: 2022, sourceType: 'script' },
143+
rules: {}
144+
}]);
145+
const sourceCode = linter.getSourceCode();
146+
const ruleContext: Rule.RuleContext = {
147+
id: 'prefer-arrow-callback',
148+
options: [],
149+
settings: {},
150+
parserPath: '<text>',
151+
languageOptions: { ecmaVersion: 2022, sourceType: 'script' },
152+
parserOptions: {},
153+
cwd: process.cwd(),
154+
filename: '<text>',
155+
physicalFilename: '<text>',
156+
sourceCode,
157+
getAncestors() {
158+
return [];
159+
},
160+
getDeclaredVariables() {
161+
return [];
162+
},
163+
getFilename() {
164+
return '<text>';
165+
},
166+
getPhysicalFilename() {
167+
return '<text>';
168+
},
169+
getCwd() {
170+
return process.cwd();
171+
},
172+
getScope() {
173+
const [scope] = sourceCode.scopeManager.scopes;
174+
175+
assert.ok(scope !== undefined);
176+
177+
return scope;
178+
},
179+
getSourceCode() {
180+
return sourceCode;
181+
},
182+
markVariableAsUsed() {
183+
return false;
184+
},
185+
report(descriptor) {
186+
if ('message' in descriptor) {
187+
reportedMessages.push(descriptor.message);
188+
}
189+
}
190+
};
191+
192+
preferArrowCallbackRule.create(ruleContext);
193+
} finally {
194+
builtinRules.get = originalGet;
195+
}
196+
197+
assert.deepStrictEqual(observedCalls, [
198+
'getAncestors',
199+
'getDeclaredVariables',
200+
'getFilename',
201+
'getPhysicalFilename',
202+
'getCwd',
203+
'getScope',
204+
'getSourceCode',
205+
'markVariableAsUsed',
206+
'report'
207+
]);
208+
assert.deepStrictEqual(reportedMessages, ['wrapped report']);
209+
});
210+
});

0 commit comments

Comments
 (0)