Skip to content

Commit

Permalink
Update: add fixer for prefer-arrow-callback (fixes #7002)
Browse files Browse the repository at this point in the history
  • Loading branch information
not-an-aardvark committed Aug 29, 2016
1 parent 6869c60 commit 75affc6
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 19 deletions.
2 changes: 2 additions & 0 deletions docs/rules/prefer-arrow-callback.md
@@ -1,5 +1,7 @@
# Suggest using arrow functions as callbacks. (prefer-arrow-callback)

(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule.

Arrow functions are suited to callbacks, because:

- `this` keywords in arrow functions bind to the upper scope's.
Expand Down
37 changes: 35 additions & 2 deletions lib/rules/prefer-arrow-callback.js
Expand Up @@ -140,14 +140,17 @@ module.exports = {
},
additionalProperties: false
}
]
],

fixable: "code"
},

create(context) {
const options = context.options[0] || {};

const allowUnboundThis = options.allowUnboundThis !== false; // default to true
const allowNamedFunctions = options.allowNamedFunctions;
const sourceCode = context.getSourceCode();

/*
* {Array<{this: boolean, super: boolean, meta: boolean}>}
Expand Down Expand Up @@ -246,7 +249,37 @@ module.exports = {
!scopeInfo.super &&
!scopeInfo.meta
) {
context.report(node, "Unexpected function expression.");
context.report({
node,
message: "Unexpected function expression.",
fix(fixer) {

// FIXME: Is there a better way to detect parser support for arrow functions?
if (!context.parserOptions || !context.parserOptions.ecmaVersion || context.parserOptions.ecmaVersion < 6) {

// If ES6 parsing is not enabled, creating arrow functions will cause syntax errors, so don't do that.
return null;
}

if (!callbackInfo.isLexicalThis && scopeInfo.this) {

// If the callback function does not have .bind(this) and contains a reference to `this`, there
// is no way to determine what `this` should be, so don't perform any fixes.
return null;
}

const params = "(" + node.params.map(param => sourceCode.getText(param)).join(", ") + ")";

if (callbackInfo.isLexicalThis) {

// If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
return fixer.replaceText(node.parent.parent, params + " => " + sourceCode.getText(node.body));
}

// Otherwise, only replace the `function` keyword and parameters with the arrow function parameters.
return fixer.replaceTextRange([node.start, node.body.start], params + " => ");
}
});
}
}
};
Expand Down
138 changes: 121 additions & 17 deletions tests/lib/rules/prefer-arrow-callback.js
Expand Up @@ -23,6 +23,26 @@ const errors = [{

const ruleTester = new RuleTester();

/**
* Returns a new list of invalid cases, asserting that no fixes are applied in ES5.
* @param {Array} invalidCases An array of invalid cases, containing output properties even when parsing ES5
* @returns {Array} A new array of invalid cases, asserting that no fixes are applied when parsing ES5, and that
the given fixes are applied when parsing ES6.
*/
function skipFixesIfEs5(invalidCases) {
const allInvalidCases = [];

invalidCases.forEach(invalidCase => {
if (invalidCase.parserOptions && invalidCase.parserOptions.ecmaVersion >= 6) {
allInvalidCases.push(invalidCase);
} else {
allInvalidCases.push(Object.assign({}, invalidCase, {output: invalidCase.code}));
allInvalidCases.push(Object.assign({}, invalidCase, {parserOptions: {ecmaVersion: 6}}));
}
});
return allInvalidCases;
}

ruleTester.run("prefer-arrow-callback", rule, {
valid: [
{code: "foo(a => a);", parserOptions: { ecmaVersion: 6 }},
Expand All @@ -41,22 +61,106 @@ ruleTester.run("prefer-arrow-callback", rule, {
{code: "foo(function bar() { super.a; });", parserOptions: { ecmaVersion: 6 }},
{code: "foo(function bar() { super.a; }.bind(this));", parserOptions: { ecmaVersion: 6 }},
{code: "foo(function bar() { new.target; });", parserOptions: { ecmaVersion: 6 }},
{code: "foo(function bar() { new.target; }.bind(this));", parserOptions: { ecmaVersion: 6 }}
{code: "foo(function bar() { new.target; }.bind(this));", parserOptions: { ecmaVersion: 6 }},
{code: "foo(function bar() { this; }.bind(this, somethingElse));"}
],
invalid: [
{code: "foo(function bar() {});", errors},
{code: "foo(function() {});", options: [{ allowNamedFunctions: true }], errors},
{code: "foo(function bar() {});", options: [{ allowNamedFunctions: false }], errors},
{code: "foo(function() {});", errors},
{code: "foo(nativeCb || function() {});", errors},
{code: "foo(bar ? function() {} : function() {});", errors: [errors[0], errors[0]]},
{code: "foo(function() { (function() { this; }); });", errors},
{code: "foo(function() { this; }.bind(this));", errors},
{code: "foo(function() { (() => this); }.bind(this));", parserOptions: { ecmaVersion: 6 }, errors},
{code: "foo(function bar(a) { a; });", errors},
{code: "foo(function(a) { a; });", errors},
{code: "foo(function(arguments) { arguments; });", errors},
{code: "foo(function() { this; });", options: [{ allowUnboundThis: false }], errors},
{code: "foo(function() { (() => this); });", parserOptions: { ecmaVersion: 6 }, options: [{ allowUnboundThis: false }], errors}
]
invalid: skipFixesIfEs5([
{
code: "foo(function bar() {});",
errors,
output: "foo(() => {});"
},
{
code: "foo(function() {});",
options: [{ allowNamedFunctions: true }],
errors,
output: "foo(() => {});"
},
{
code: "foo(function bar() {});",
options: [{ allowNamedFunctions: false }],
errors,
output: "foo(() => {});"
},
{
code: "foo(function() {});",
errors,
output: "foo(() => {});"
},
{
code: "foo(nativeCb || function() {});",
errors,
output: "foo(nativeCb || () => {});"
},
{
code: "foo(bar ? function() {} : function() {});",
errors: [errors[0], errors[0]],
output: "foo(bar ? () => {} : () => {});"
},
{
code: "foo(function() { (function() { this; }); });",
errors,
output: "foo(() => { (function() { this; }); });"
},
{
code: "foo(function() { this; }.bind(this));",
errors,
output: "foo(() => { this; });"
},
{
code: "foo(function() { (() => this); }.bind(this));",
parserOptions: { ecmaVersion: 6 },
errors,
output: "foo(() => { (() => this); });"
},
{
code: "foo(function bar(a) { a; });",
errors,
output: "foo((a) => { a; });"
},
{
code: "foo(function(a) { a; });",
errors,
output: "foo((a) => { a; });"
},
{
code: "foo(function(arguments) { arguments; });",
errors,
output: "foo((arguments) => { arguments; });"
},
{
code: "foo(function() { this; });",
options: [{ allowUnboundThis: false }],
errors,
output: "foo(function() { this; });" // No fix applied
},
{
code: "foo(function() { (() => this); });",
parserOptions: { ecmaVersion: 6 },
options: [{ allowUnboundThis: false }],
errors,
output: "foo(function() { (() => this); });" // No fix applied
},
{
code: "qux(function(foo, bar, baz) { return foo * 2; })",
errors,
output: "qux((foo, bar, baz) => { return foo * 2; })"
},
{
code: "qux(function(foo, bar, baz) { return foo * bar; }.bind(this))",
errors,
output: "qux((foo, bar, baz) => { return foo * bar; })"
},
{
code: "qux(function(foo, bar, baz) { return foo * this.qux; }.bind(this))",
errors,
output: "qux((foo, bar, baz) => { return foo * this.qux; })"
},
{
code: "qux(function(foo = 1, [bar = 2] = [], {qux: baz = 3} = {foo: 'bar'}) { return foo + bar; });",
parserOptions: { ecmaVersion: 6 },
errors,
output: "qux((foo = 1, [bar = 2] = [], {qux: baz = 3} = {foo: 'bar'}) => { return foo + bar; });"
}
])
});

0 comments on commit 75affc6

Please sign in to comment.