Repository: lo1tuma/eslint-plugin-mocha
Pull request #163 (2018-06-16 05:10:03)

Adds mocha-aware prefer-arrow-callback rule
Addresses #141 

---

- Copied core eslint `prefer-arrow-callback` rule code, test code, and markdown doc from: https://github.com/eslint/eslint/blob/master/docs/rules/prefer-arrow-callback.md
- Modified this rule to be mocha-aware by utilizing the exiting utility function `astUtils.isMochaFunctionCall(node, scope)`
- Updated tests to include *rule not triggered* tests for all mocha functions
- Updated documentation to include instructions to disable the core `prefer-arrow-callback` rule before enabling the `mocha/prefer-arrow-callback` rule

The following eslint rules are failing with this code:
```
/Users/mfields/workspace/eslint-plugin-mocha/lib/rules/prefer-arrow-callback.js
   90:1   error  Function 'getCallbackInfo' has a complexity of 12                                     complexity
   90:1   error  Function 'getCallbackInfo' has too many statements (13). Maximum allowed is 10        max-statements
  251:38  error  Method 'FunctionExpression:exit' has a complexity of 16                               complexity
  251:38  error  Method 'FunctionExpression:exit' has too many statements (14). Maximum allowed is 10  max-statements
  290:28  error  Method 'fix' has a complexity of 9                                                    complexity
  290:28  error  Method 'fix' has too many statements (11). Maximum allowed is 10                      max-statements
```
It looks like it will take some substantial work to get this code in line with the complexity limit of 4.  I think it would be preferable to not completely rewrite this rule so that updates from the core eslint `prefer-arrow-callback` rule can be applied more easily here in the future.  Would the maintainers here be open to a rule-specific relaxing of the complexity and max-statements eslint rules (now included in code changes)?


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 1b1a8ad..6d75607 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -16,5 +16,6 @@
 * [no-skipped-tests](no-skipped-tests.md) - disallow skipped mocha tests (fixable)
 * [no-synchronous-tests](no-synchronous-tests.md) - disallow synchronous tests
 * [no-top-level-hooks](no-top-level-hooks.md) - disallow top-level hooks
+* [prefer-arrow-callback](prefer-arrow-callback.md) - prefer arrow function callbacks (mocha-aware)
 * [valid-suite-description](valid-suite-description.md) - match suite descriptions against a pre-configured regular expression
 * [valid-test-description](valid-test-description.md) - match test descriptions against a pre-configured regular expression
diff --git a/docs/rules/prefer-arrow-callback.md b/docs/rules/prefer-arrow-callback.md
new file mode 100644
index 0000000..9108ff0
--- /dev/null
+++ b/docs/rules/prefer-arrow-callback.md
@@ -0,0 +1,124 @@
+# Require using arrow functions for callbacks (prefer-arrow-callback)
+
+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.
+
+You will want to disable the original `prefer-arrow-callback` rule and configure the mocha-friendly replacement under the rules section.
+
+```json
+{
+    "rules": {
+        "prefer-arrow-callback": 0,
+        "mocha/prefer-arrow-callback": 2
+    }
+}
+```
+
+## Rule Overview
+
+Arrow functions can be an attractive alternative to function expressions for callbacks or function arguments.
+
+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.
+
+Additionally, arrow functions are:
+
+- less verbose, and easier to reason about.
+
+- bound lexically regardless of where or when they are invoked.
+
+## Rule Details
+
+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.
+
+The following examples **will** be flagged:
+
+```js
+/* eslint mocha/prefer-arrow-callback: "error" */
+
+foo(function(a) { return a; }); // ERROR
+// prefer: foo(a => a)
+
+foo(function() { return this.a; }.bind(this)); // ERROR
+// prefer: foo(() => this.a)
+```
+
+Instances where an arrow function would not produce identical results will be ignored.
+
+The following examples **will not** be flagged:
+
+```js
+/* eslint mocha/prefer-arrow-callback: "error" */
+/* eslint-env es6 */
+
+// arrow function callback
+foo(a => a); // OK
+
+// generator as callback
+foo(function*() { yield; }); // OK
+
+// function expression not used as callback or function argument
+var foo = function foo(a) { return a; }; // OK
+
+// unbound function expression callback
+foo(function() { return this.a; }); // OK
+
+// recursive named function callback
+foo(function bar(n) { return n && n + bar(n - 1); }); // OK
+
+// mocha suite definition callback
+describe('test suite', function() { return Promise.resolve(); }); // OK
+
+// mocha hook callback
+beforeEach('before each test', function() { return Promise.resolve(); }); // OK
+
+// mocha test case callback
+it('should resolve', function() { return Promise.resolve(); }); // OK
+```
+
+## Options
+
+Access further control over this rule's behavior via an options object.
+
+Default: `{ allowNamedFunctions: false, allowUnboundThis: true }`
+
+### allowNamedFunctions
+
+By default `{ "allowNamedFunctions": false }`, this `boolean` option prohibits using named functions as callbacks or function arguments.
+
+Changing this value to `true` will reverse this option's behavior by allowing use of named functions without restriction.
+
+`{ "allowNamedFunctions": true }` **will not** flag the following example:
+
+```js
+/* eslint mocha/prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
+
+foo(function bar() {});
+```
+
+### allowUnboundThis
+
+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.
+
+When set to `false` this option prohibits the use of function expressions as callbacks or function arguments entirely, without exception.
+
+`{ "allowUnboundThis": false }` **will** flag the following examples:
+
+```js
+/* eslint mocha/prefer-arrow-callback: [ "error", { "allowUnboundThis": false } ] */
+/* eslint-env es6 */
+
+foo(function() { this.a; });
+
+foo(function() { (() => this); });
+
+someArray.map(function(itm) { return this.doSomething(itm); }, someObject);
+```
+
+## When Not To Use It
+
+- In environments that have not yet adopted ES6 language features (ES3/5).
+
+- In ES6+ environments that allow the use of function expressions when describing callbacks or function arguments.
+
+## Further Reading
+
+- [More on ES6 arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)
diff --git a/index.js b/index.js
index 67452ad..111aa09 100644
--- a/index.js
+++ b/index.js
@@ -19,7 +19,8 @@ module.exports = {
         'no-identical-title': require('./lib/rules/no-identical-title'),
         'max-top-level-suites': require('./lib/rules/max-top-level-suites'),
         'no-nested-tests': require('./lib/rules/no-nested-tests'),
-        'no-setup-in-describe': require('./lib/rules/no-setup-in-describe')
+        'no-setup-in-describe': require('./lib/rules/no-setup-in-describe'),
+        'prefer-arrow-callback': require('./lib/rules/prefer-arrow-callback')
     },
     configs: {
         recommended: {
diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js
new file mode 100644
index 0000000..08c6a99
--- /dev/null
+++ b/lib/rules/prefer-arrow-callback.js
@@ -0,0 +1,346 @@
+'use strict';
+
+/*
+ * relax complexity and max-statements eslint rules in order to preserve the imported
+ * core eslint `prefer-arrow-callback` rule code so that future updates to that code
+ * can be more easily applied here.
+ */
+/* eslint "complexity": [ "error", 18 ], "max-statements": [ "error", 15 ] */
+
+/**
+ * @fileoverview A rule to suggest using arrow functions as callbacks.
+ * @author Toru Nagashima (core eslint rule)
+ * @author Michael Fields (mocha-aware rule modifications)
+ */
+const astUtils = require('../util/ast');
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Checks whether or not a given variable is a function name.
+ * @param {eslint-scope.Variable} variable - A variable to check.
+ * @returns {boolean} `true` if the variable is a function name.
+ */
+function isFunctionName(variable) {
+    return variable && variable.defs[0].type === 'FunctionName';
+}
+
+/**
+ * Checks whether or not a given MetaProperty node equals to a given value.
+ * @param {ASTNode} node - A MetaProperty node to check.
+ * @param {string} metaName - The name of `MetaProperty.meta`.
+ * @param {string} propertyName - The name of `MetaProperty.property`.
+ * @returns {boolean} `true` if the node is the specific value.
+ */
+function checkMetaProperty(node, metaName, propertyName) {
+    return node.meta.name === metaName && node.property.name === propertyName;
+}
+
+/**
+ * Gets the variable object of `arguments` which is defined implicitly.
+ * @param {eslint-scope.Scope} scope - A scope to get.
+ * @returns {eslint-scope.Variable} The found variable object.
+ */
+function getVariableOfArguments(scope) {
+    const variables = scope.variables;
+    let variableObject = null;
+
+    for (let i = 0; i < variables.length; i += 1) {
+        const variable = variables[i];
+
+        /*
+         * If there was a parameter which is named "arguments", the
+         * implicit "arguments" is not defined.
+         * So does fast return with null.
+         */
+        if (variable.name === 'arguments' && variable.identifiers.length === 0) {
+            variableObject = variable;
+            break;
+        }
+    }
+    return variableObject;
+}
+
+/**
+ * Does the node property indicate a `bind` identifier
+ * @param {object} property - the node property.
+ * @returns {boolean}
+ */
+function propertyIndicatesBind(property) {
+    return !property.computed &&
+    property.type === 'Identifier' &&
+    property.name === 'bind';
+}
+
+/**
+ * Is the node a `.bind(this)` node?
+ * @param {ASTNode} node - the node property.
+ * @returns {boolean}
+ */
+function isBindThis(node, currentNode) {
+    return node.object === currentNode &&
+        propertyIndicatesBind(node.property) &&
+        node.parent.type === 'CallExpression' &&
+        node.parent.callee === node;
+}
+
+/**
+ * Checkes whether or not a given node is a callback.
+ * @param {ASTNode} node - A node to check.
+ * @param {object} context - The eslint context.
+ * @returns {Object}
+ *   {boolean} retv.isCallback - `true` if the node is a callback.
+ *   {boolean} retv.isMochaCallback - `true` if the node is an argument to a mocha function.
+ *   {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`.
+ */
+function getCallbackInfo(node, context) {
+    const retv = { isCallback: false, isLexicalThis: false };
+    let searchComplete = false;
+    let currentNode = node;
+    let parent = node.parent;
+
+    while (currentNode && !searchComplete) {
+        switch (parent.type) {
+        // Checks parents recursively.
+
+        case 'LogicalExpression':
+        case 'ConditionalExpression':
+            break;
+
+            // Checks whether the parent node is `.bind(this)` call.
+        case 'MemberExpression':
+            if (isBindThis(parent, currentNode)) {
+                retv.isLexicalThis =
+                        parent.parent.arguments.length === 1 &&
+                        parent.parent.arguments[0].type === 'ThisExpression';
+                parent = parent.parent;
+            } else {
+                searchComplete = true;
+            }
+            break;
+
+            // Checks whether the node is a callback.
+        case 'CallExpression':
+        case 'NewExpression':
+            if (parent.callee !== currentNode) {
+                retv.isCallback = true;
+            }
+            // Checks whether the node is a mocha function callback.
+            if (retv.isCallback && astUtils.isMochaFunctionCall(parent, context.getScope())) {
+                retv.isMochaCallback = true;
+            }
+            searchComplete = true;
+            break;
+
+        default:
+            searchComplete = true;
+        }
+
+        if (!searchComplete) {
+            currentNode = parent;
+            parent = parent.parent;
+        }
+    }
+    return retv;
+}
+
+/**
+ * Checks whether a simple list of parameters contains any duplicates. This does not handle complex
+ * parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate
+ * parameter names anyway. Instead, it always returns `false` for complex parameter lists.
+ * @param {ASTNode[]} paramsList The list of parameters for a function
+ * @returns {boolean} `true` if the list of parameters contains any duplicates
+ */
+function hasDuplicateParams(paramsList) {
+    return paramsList.every((param) => param.type === 'Identifier') &&
+          paramsList.length !== new Set(paramsList.map((param) => param.name)).size;
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: 'require using arrow functions for callbacks',
+            category: 'ECMAScript 6',
+            recommended: false,
+            url: 'https://eslint.org/docs/rules/prefer-arrow-callback'
+        },
+
+        schema: [
+            {
+                type: 'object',
+                properties: {
+                    allowNamedFunctions: {
+                        type: 'boolean'
+                    },
+                    allowUnboundThis: {
+                        type: 'boolean'
+                    }
+                },
+                additionalProperties: false
+            }
+        ],
+
+        fixable: 'code'
+    },
+
+    create(context) {
+        const options = context.options[0] || {};
+
+        // allowUnboundThis defaults to true
+        const allowUnboundThis = options.allowUnboundThis !== false;
+        const allowNamedFunctions = options.allowNamedFunctions;
+        const sourceCode = context.getSourceCode();
+
+        /*
+         * {Array<{this: boolean, super: boolean, meta: boolean}>}
+         * - this - A flag which shows there are one or more ThisExpression.
+         * - super - A flag which shows there are one or more Super.
+         * - meta - A flag which shows there are one or more MetaProperty.
+         */
+        let stack = [];
+
+        /**
+         * Pushes new function scope with all `false` flags.
+         * @returns {void}
+         */
+        function enterScope() {
+            stack.push({ this: false, super: false, meta: false });
+        }
+
+        /**
+         * Pops a function scope from the stack.
+         * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
+         */
+        function exitScope() {
+            return stack.pop();
+        }
+
+        return {
+
+            // Reset internal state.
+            Program() {
+                stack = [];
+            },
+
+            // If there are below, it cannot replace with arrow functions merely.
+            ThisExpression() {
+                const info = stack[stack.length - 1];
+
+                if (info) {
+                    info.this = true;
+                }
+            },
+
+            Super() {
+                const info = stack[stack.length - 1];
+
+                if (info) {
+                    info.super = true;
+                }
+            },
+
+            MetaProperty(node) {
+                const info = stack[stack.length - 1];
+
+                if (info && checkMetaProperty(node, 'new', 'target')) {
+                    info.meta = true;
+                }
+            },
+
+            // To skip nested scopes.
+            FunctionDeclaration: enterScope,
+            'FunctionDeclaration:exit': exitScope,
+
+            // Main.
+            FunctionExpression: enterScope,
+            'FunctionExpression:exit'(node) {
+                const scopeInfo = exitScope();
+
+                // Skip named function expressions
+                if (allowNamedFunctions && node.id && node.id.name) {
+                    return;
+                }
+
+                // Skip generators.
+                if (node.generator) {
+                    return;
+                }
+
+                // Skip recursive functions.
+                const nameVar = context.getDeclaredVariables(node)[0];
+
+                if (isFunctionName(nameVar) && nameVar.references.length > 0) {
+                    return;
+                }
+
+                // Skip if it's using arguments.
+                const variable = getVariableOfArguments(context.getScope());
+
+                if (variable && variable.references.length > 0) {
+                    return;
+                }
+
+                // Reports if it's a callback which can replace with arrows.
+                const callbackInfo = getCallbackInfo(node, context);
+
+                if (callbackInfo.isCallback &&
+                    (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) &&
+                    !scopeInfo.super &&
+                    !scopeInfo.meta &&
+                    !callbackInfo.isMochaCallback
+                ) {
+                    context.report({
+                        node,
+                        message: 'Unexpected function expression.',
+                        fix(fixer) {
+                            if (!callbackInfo.isLexicalThis && scopeInfo.this || hasDuplicateParams(node.params)) {
+                                /*
+                                 * 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. If the callback function has duplicates in its list of parameters (possible
+                                 * in sloppy mode), don't replace it with an arrow function, because this is a
+                                 * SyntaxError with arrow functions.
+                                 */
+                                return null;
+                            }
+
+                            const paramsLeftParen = node.params.length ?
+                                sourceCode.getTokenBefore(node.params[0]) :
+                                sourceCode.getTokenBefore(node.body, 1);
+                            const paramsRightParen = sourceCode.getTokenBefore(node.body);
+                            const asyncKeyword = node.async ? 'async ' : '';
+                            const paramsFullText = sourceCode.text.slice(
+                                paramsLeftParen.range[0], paramsRightParen.range[1]
+                            );
+                            const arrowFunctionText =
+                                `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
+
+                            /*
+                             * If the callback function has `.bind(this)`, replace it with an arrow function and remove
+                             * the binding. Otherwise, just replace the arrow function itself.
+                             */
+                            const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
+
+                            /*
+                             * If the replaced node is part of a BinaryExpression, LogicalExpression, or
+                             * MemberExpression, then the arrow function needs to be parenthesized, because
+                             *  `foo || () => {}` is invalid syntax even though `foo || function() {}` is valid.
+                             */
+                            const needsParens = replacedNode.parent.type !== 'CallExpression' &&
+                              replacedNode.parent.type !== 'ConditionalExpression';
+                            const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
+
+                            return fixer.replaceText(replacedNode, replacementText);
+                        }
+                    });
+                }
+            }
+        };
+    }
+};


Test patch:
diff --git a/test/rules/prefer-arrow-callback.js b/test/rules/prefer-arrow-callback.js
new file mode 100644
index 0000000..7aea6da
--- /dev/null
+++ b/test/rules/prefer-arrow-callback.js
@@ -0,0 +1,215 @@
+/**
+ * @fileoverview Tests for prefer-arrow-callback rule.
+ * @author Toru Nagashima (core eslint rule tests)
+ * @author Michael Fields (mocha-aware additional tests)
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester;
+const rules = require('../../').rules;
+const ruleTester = new RuleTester({
+    parserOptions: { ecmaVersion: 2017 }
+});
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const errors = [ {
+    message: 'Unexpected function expression.',
+    type: 'FunctionExpression'
+} ];
+
+ruleTester.run('prefer-arrow-callback', rules['prefer-arrow-callback'], {
+    valid: [
+        'foo(a => a);',
+        'foo(function*() {});',
+        'foo(function() { this; });',
+        { code: 'foo(function bar() {});', options: [ { allowNamedFunctions: true } ] },
+        'foo(function() { (() => this); });',
+        'foo(function() { this; }.bind(obj));',
+        'foo(function() { this; }.call(this));',
+        'foo(a => { (function() {}); });',
+        'var foo = function foo() {};',
+        '(function foo() {})();',
+        'foo(function bar() { bar; });',
+        'foo(function bar() { arguments; });',
+        'foo(function bar() { arguments; }.bind(this));',
+        'foo(function bar() { super.a; });',
+        'foo(function bar() { super.a; }.bind(this));',
+        '() => super()',
+        'foo(function bar() { new.target; });',
+        'foo(function bar() { new.target; }.bind(this));',
+        '() => new.target',
+        'foo(function bar() { this; }.bind(this, somethingElse));',
+        // mocha-specific valid test cases
+        'before(function bar() {});',
+        'after(function bar() {});',
+        'beforeEach(function bar() {});',
+        'afterEach(function bar() {});',
+        'describe("name", function bar() {});',
+        'xdescribe("name", function bar() {});',
+        'describe.only("name", function bar() {});',
+        'describe.skip("name", function bar() {});',
+        'context("name", function bar() {});',
+        'xcontext("name", function bar() {});',
+        'context.only("name", function bar() {});',
+        'context.skip("name", function bar() {});',
+        'suite("name", function bar() {});',
+        'xsuite("name", function bar() {});',
+        'suite.only("name", function bar() {});',
+        'suite.skip("name", function bar() {});',
+        'it("name", function bar() {});',
+        'it.only("name", function bar() {});',
+        'it.skip("name", function bar() {});',
+        'xit("name", function bar() {});',
+        'test("name", function bar() {});',
+        'test.only("name", function bar() {});',
+        'test.skip("name", function bar() {});',
+        'specify("name", function bar() {});',
+        'specify.only("name", function bar() {});',
+        'specify.skip("name", function bar() {});',
+        'xspecify("name", function bar() {});'
+    ],
+    invalid: [
+        {
+            code: 'foo(function bar() {});',
+            output: 'foo(() => {});',
+            errors
+        },
+        {
+            code: 'foo(function() {});',
+            output: 'foo(() => {});',
+            options: [ { allowNamedFunctions: true } ],
+            errors
+        },
+        {
+            code: 'foo(function bar() {});',
+            output: 'foo(() => {});',
+            options: [ { allowNamedFunctions: false } ],
+            errors
+        },
+        {
+            code: 'foo(function() {});',
+            output: 'foo(() => {});',
+            errors
+        },
+        {
+            code: 'foo(nativeCb || function() {});',
+            output: 'foo(nativeCb || (() => {}));',
+            errors
+        },
+        {
+            code: 'foo(bar ? function() {} : function() {});',
+            output: 'foo(bar ? () => {} : () => {});',
+            errors: [ errors[0], errors[0] ]
+        },
+        {
+            code: 'foo(function() { (function() { this; }); });',
+            output: 'foo(() => { (function() { this; }); });',
+            errors
+        },
+        {
+            code: 'foo(function() { this; }.bind(this));',
+            output: 'foo(() => { this; });',
+            errors
+        },
+        {
+            code: 'foo(bar || function() { this; }.bind(this));',
+            output: 'foo(bar || (() => { this; }));',
+            errors
+        },
+        {
+            code: 'foo(function() { (() => this); }.bind(this));',
+            output: 'foo(() => { (() => this); });',
+            errors
+        },
+        {
+            code: 'foo(function bar(a) { a; });',
+            output: 'foo((a) => { a; });',
+            errors
+        },
+        {
+            code: 'foo(function(a) { a; });',
+            output: 'foo((a) => { a; });',
+            errors
+        },
+        {
+            code: 'foo(function(arguments) { arguments; });',
+            output: 'foo((arguments) => { arguments; });',
+            errors
+        },
+        {
+            code: 'foo(function() { this; });',
+            // No fix applied
+            output: null,
+            options: [ { allowUnboundThis: false } ],
+            errors
+        },
+        {
+            code: 'foo(function() { (() => this); });',
+            // No fix applied
+            output: null,
+            options: [ { allowUnboundThis: false } ],
+            errors
+        },
+        {
+            code: 'qux(function(foo, bar, baz) { return foo * 2; })',
+            output: 'qux((foo, bar, baz) => { return foo * 2; })',
+            errors
+        },
+        {
+            code: 'qux(function(foo, bar, baz) { return foo * bar; }.bind(this))',
+            output: 'qux((foo, bar, baz) => { return foo * bar; })',
+            errors
+        },
+        {
+            code: 'qux(function(foo, bar, baz) { return foo * this.qux; }.bind(this))',
+            output: 'qux((foo, bar, baz) => { return foo * this.qux; })',
+            errors
+        },
+        {
+            code: 'foo(function() {}.bind(this, somethingElse))',
+            output: 'foo((() => {}).bind(this, somethingElse))',
+            errors
+        },
+        {
+            code: 'qux(function(foo = 1, [bar = 2] = [], {qux: baz = 3} = {foo: \'bar\'}) { return foo + bar; });',
+            output: 'qux((foo = 1, [bar = 2] = [], {qux: baz = 3} = {foo: \'bar\'}) => { return foo + bar; });',
+            errors
+        },
+        {
+            code: 'qux(function(baz, baz) { })',
+            // Duplicate parameter names are a SyntaxError in arrow functions
+            output: null,
+            errors
+        },
+        {
+            code: 'qux(function( /* no params */ ) { })',
+            output: 'qux(( /* no params */ ) => { })',
+            errors
+        },
+        {
+            code: 'qux(function( /* a */ foo /* b */ , /* c */ bar /* d */ , /* e */ baz /* f */ ) { return foo; })',
+            output: 'qux(( /* a */ foo /* b */ , /* c */ bar /* d */ , /* e */ baz /* f */ ) => { return foo; })',
+            errors
+        },
+        {
+            code: 'qux(async function (foo = 1, bar = 2, baz = 3) { return baz; })',
+            output: 'qux(async (foo = 1, bar = 2, baz = 3) => { return baz; })',
+            parserOptions: { ecmaVersion: 8 },
+            errors
+        },
+        {
+            code: 'qux(async function (foo = 1, bar = 2, baz = 3) { return this; }.bind(this))',
+            output: 'qux(async (foo = 1, bar = 2, baz = 3) => { return this; })',
+            parserOptions: { ecmaVersion: 8 },
+            errors
+        }
+    ]
+});