Skip to content

Commit

Permalink
Add prefer-reflect-apply rule (#239)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Alex Zherdev and sindresorhus committed Sep 16, 2019
1 parent 850b8a1 commit 0b9c0fe
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 0 deletions.
30 changes: 30 additions & 0 deletions docs/rules/prefer-reflect-apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Prefer `Reflect.apply()` over `Function#apply()`

[`Reflect.apply)()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply) is arguably less verbose and easier to understand. In addition, when you accept arbitrary methods, it's not safe to assume `.apply()` exists or is not overridden.

This rule is fixable.


## Fail

```js
function foo() {}

foo.apply(null, [42]);
Function.prototype.apply.call(foo, null, [42]);
foo.apply(this, [42]);
Function.prototype.apply.call(foo, this, [42]);
foo.apply(null, arguments);
Function.prototype.apply.call(foo, null, arguments);
foo.apply(this, arguments);
Function.prototype.apply.call(foo, this, arguments);
```


## Pass

```js
function foo() {}

Reflect.apply(foo, null, [42]);
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ module.exports = {
'unicorn/prefer-node-append': 'error',
'unicorn/prefer-node-remove': 'error',
'unicorn/prefer-query-selector': 'error',
'unicorn/prefer-reflect-apply': 'error',
'unicorn/prefer-spread': 'error',
'unicorn/prefer-starts-ends-with': 'error',
'unicorn/prefer-text-content': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Configure it in `package.json`.
"unicorn/prefer-node-append": "error",
"unicorn/prefer-node-remove": "error",
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-reflect-apply": "error",
"unicorn/prefer-spread": "error",
"unicorn/prefer-starts-ends-with": "error",
"unicorn/prefer-text-content": "error",
Expand Down Expand Up @@ -116,6 +117,7 @@ Configure it in `package.json`.
- [prefer-node-append](docs/rules/prefer-node-append.md) - Prefer `Node#append()` over `Node#appendChild()`. *(fixable)*
- [prefer-node-remove](docs/rules/prefer-node-remove.md) - Prefer `node.remove()` over `parentNode.removeChild(node)` and `parentElement.removeChild(node)`. *(fixable)*
- [prefer-query-selector](docs/rules/prefer-query-selector.md) - Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. *(partly fixable)*
- [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) - Prefer `Reflect.apply()` over `Function#apply()`. *(fixable)*
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
- [prefer-starts-ends-with](docs/rules/prefer-starts-ends-with.md) - Prefer `String#startsWith()` & `String#endsWith()` over more complex alternatives.
- [prefer-text-content](docs/rules/prefer-text-content.md) - Prefer `.textContent` over `.innerText`. *(fixable)*
Expand Down
86 changes: 86 additions & 0 deletions rules/prefer-reflect-apply.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';
const astUtils = require('eslint-ast-utils');
const getDocsUrl = require('./utils/get-docs-url');

const isApplySignature = (argument1, argument2) => (
((argument1.type === 'Literal' && argument1.raw === 'null') ||
argument1.type === 'ThisExpression') &&
(argument2.type === 'ArrayExpression' ||
(argument2.type === 'Identifier' &&
argument2.name === 'arguments'))
);

const getReflectApplyCall = (sourceCode, func, receiver, args) => (
`Reflect.apply(${sourceCode.getText(func)}, ${sourceCode.getText(receiver)}, ${sourceCode.getText(args)})`
);

const fixDirectApplyCall = (node, sourceCode) => {
if (
astUtils.getPropertyName(node.callee) === 'apply' &&
node.arguments.length === 2 &&
isApplySignature(node.arguments[0], node.arguments[1])
) {
return fixer => (
fixer.replaceText(
node,
getReflectApplyCall(sourceCode, node.callee.object, node.arguments[0], node.arguments[1])
)
);
}
};

const fixFunctionPrototypeCall = (node, sourceCode) => {
if (
astUtils.getPropertyName(node.callee) === 'call' &&
astUtils.getPropertyName(node.callee.object) === 'apply' &&
astUtils.getPropertyName(node.callee.object.object) === 'prototype' &&
node.callee.object.object.object &&
node.callee.object.object.object.type === 'Identifier' &&
node.callee.object.object.object.name === 'Function' &&
node.arguments.length === 3 &&
isApplySignature(node.arguments[1], node.arguments[2])
) {
return fixer => (
fixer.replaceText(
node,
getReflectApplyCall(sourceCode, node.arguments[0], node.arguments[1], node.arguments[2])
)
);
}
};

const create = context => {
return {
CallExpression: node => {
if (
!(
node.callee.type === 'MemberExpression' &&
!['Literal', 'ArrayExpression', 'ObjectExpression'].includes(node.callee.object.type)
)
) {
return;
}

const sourceCode = context.getSourceCode();
const fix = fixDirectApplyCall(node, sourceCode) || fixFunctionPrototypeCall(node, sourceCode);
if (fix) {
context.report({
node,
message: 'Prefer `Reflect.apply()` over `Function#apply()`.',
fix
});
}
}
};
};

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocsUrl(__filename)
},
fixable: 'code'
}
};
85 changes: 85 additions & 0 deletions test/prefer-reflect-apply.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import test from 'ava';
import avaRuleTester from 'eslint-ava-rule-tester';
import rule from '../rules/prefer-reflect-apply';

const ruleTester = avaRuleTester(test, {
env: {
es6: true
}
});

const errors = [
{
ruleId: 'prefer-reflect-apply'
}
];

ruleTester.run('prefer-reflect-apply', rule, {
valid: [
'foo.apply();',
'foo.apply(null);',
'foo.apply(this);',
'foo.apply(null, 42);',
'foo.apply(this, 42);',
'foo.apply(bar, arguments);',
'[].apply(null, [42]);',
'foo.apply(bar);',
'foo.apply(bar, []);',
'foo.apply;',
'apply;',
'Reflect.apply(foo, null);',
'Reflect.apply(foo, null, [bar]);'
],
invalid: [
{
code: 'foo.apply(null, [42]);',
output: 'Reflect.apply(foo, null, [42]);',
errors
},
{
code: 'foo.bar.apply(null, [42]);',
output: 'Reflect.apply(foo.bar, null, [42]);',
errors
},
{
code: 'Function.prototype.apply.call(foo, null, [42]);',
output: 'Reflect.apply(foo, null, [42]);',
errors
},
{
code: 'Function.prototype.apply.call(foo.bar, null, [42]);',
output: 'Reflect.apply(foo.bar, null, [42]);',
errors
},
{
code: 'foo.apply(null, arguments);',
output: 'Reflect.apply(foo, null, arguments);',
errors
},
{
code: 'Function.prototype.apply.call(foo, null, arguments);',
output: 'Reflect.apply(foo, null, arguments);',
errors
},
{
code: 'foo.apply(this, [42]);',
output: 'Reflect.apply(foo, this, [42]);',
errors
},
{
code: 'Function.prototype.apply.call(foo, this, [42]);',
output: 'Reflect.apply(foo, this, [42]);',
errors
},
{
code: 'foo.apply(this, arguments);',
output: 'Reflect.apply(foo, this, arguments);',
errors
},
{
code: 'Function.prototype.apply.call(foo, this, arguments);',
output: 'Reflect.apply(foo, this, arguments);',
errors
}
]
});

0 comments on commit 0b9c0fe

Please sign in to comment.