Repository: eslint/eslint (js)

Issue:
indent handles variable declarations incorrectly when the first declarator is on a new line
**Tell us about your environment**

* **ESLint Version: 4.0.0**
* **Node Version: 6.9.2**
* **npm Version: 5.0.3**

**What parser (default, Babel-ESLint, etc.) are you using?**

**Please show your full configuration:**

<!-- Paste your configuration below: -->
```
"rules": {
  "indent": [2, 2, {"SwitchCase": 1}],
}
```

**What did you do? Please include the actual source code causing the issue.**

<!-- Paste the source code below: -->
```js
const 
  o = {
    foo: 'bar'
  },
  p = true;

console.log(o, p);
```

**What did you expect to happen?**

**What actually happened? Please include the actual, raw output from ESLint.**

```
test.js
  3:1  error  Expected indentation of 6 spaces but found 4  indent
  4:1  error  Expected indentation of 4 spaces but found 2  indent

✖ 2 problems (2 errors, 0 warnings)
  2 errors, 0 warnings potentially fixable with the `--fix` option
```

Works fine with eslint 3.x and if only one variable is declared at a time.

Pull request:
Update: simplify variable declarator indent handling (fixes #8785)
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[x] Bug fix (https://github.com/eslint/eslint/issues/8785)
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**

This fixes the `indent` rule's VariableDeclarator logic to correctly handle the case where a declaration has more than one declarator, but neither is on the same line as the start of the declaration.

This also updates the variable declarator listener to be slightly more similar to the logic for other nodes. Previously, variable declarators were treated as a special case and handling them involved overwriting some of the previously-declared offsets while exiting the node. Unfortunately, declarators are still a special case, but their logic isn't quite as different now -- the correct behavior is applied when entering the node, like it is for other node types.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e41be9d15..5e017bec8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+v4.1.1 - June 25, 2017
+
+* f307aa0 Fix: ensure configs from a plugin are cached separately (fixes #8792) (#8798) (Teddy Katz)
+* 8b48ae8 Docs: Add doc on parser services (fixes #8390) (#8795) (Victor Hom)
+* 0d041e7 Fix: avoid crashing when using baseConfig with extends (fixes #8791) (#8797) (Teddy Katz)
+* 03213bb Chore: improve comment explanation of `indent` internal functions (#8800) (Teddy Katz)
+* d2e88ed Chore: Fix misleading comment in ConfigCache.js (#8799) (Teddy Katz)
+
 v4.1.0 - June 23, 2017
 
 * e8f1362 Docs: Remove wrong descriptions in `padded-block` rule (#8783) (Plusb Preco)
diff --git a/conf/eslint-recommended.js b/conf/eslint-recommended.js
index 0d8d19e44..1a2e2f8e7 100755
--- a/conf/eslint-recommended.js
+++ b/conf/eslint-recommended.js
@@ -48,6 +48,7 @@ module.exports = {
         "func-names": "off",
         "func-style": "off",
         "generator-star-spacing": "off",
+        "getter-return": "off",
         "global-require": "off",
         "guard-for-in": "off",
         "handle-callback-err": "off",
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index f48670896..0f006d781 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -159,7 +159,7 @@ var messages = linter.verifyAndFix("var foo", {
     rules: {
         semi: 2
     }
-}, { filename: "foo.js" });
+});
 ```
 
 Output object from this method:
diff --git a/docs/developer-guide/working-with-plugins.md b/docs/developer-guide/working-with-plugins.md
index b8667d33a..0c8bfad22 100644
--- a/docs/developer-guide/working-with-plugins.md
+++ b/docs/developer-guide/working-with-plugins.md
@@ -218,3 +218,32 @@ Add these keywords into your `package.json` file to make it easy for others to f
 ## Further Reading
 
 * [npm Developer Guide](https://docs.npmjs.com/misc/developers)
+
+### Working with Custom Parsers
+
+If you want to use your own parser and provide additional capabilities for your rules, you can specify your own custom parser. If a `parseForESLint` method is exposed on the parser, this method will be used to parse the code. Otherwise, the `parse` method will be used. Both methods should take in the the source code as the first argument, and an optional configuration object as the second argument (provided as `parserOptions` in a config file). The `parse` method should simply return the AST. The `parseForESLint` method should return an object that contains the required property `ast` and an optional `services` property. `ast` should contain the AST. The `services` property can contain any parser-dependent services (such as type checkers for nodes). The value of the `services` property is available to rules as `context.parserServices`.
+
+You can find an ESLint parser project [here](https://github.com/eslint/typescript-eslint-parser).
+
+    {
+
+        "parser": './path/to/awesome-custom-parser.js'
+    }
+
+```javascript
+var espree = require("espree");
+// awesome-custom-parser.js
+exports.parseForESLint = function(code, options) {
+    return {
+        ast: espree.parse(code, options),
+        services: {
+            foo: function() {
+                console.log("foo");
+            }
+        }
+    };
+};
+
+```
+
+
diff --git a/docs/rules/getter-return.md b/docs/rules/getter-return.md
new file mode 100644
index 000000000..57f92065d
--- /dev/null
+++ b/docs/rules/getter-return.md
@@ -0,0 +1,97 @@
+# Enforces that a return statement is present in property getters (getter-return)
+
+The get syntax binds an object property to a function that will be called when that property is looked up. It was first introduced in ECMAScript 5:
+
+```js
+    var p = {
+        get name(){
+            return "nicholas";
+        }
+    };
+
+    Object.defineProperty(p, "age", {
+        get: function (){
+            return 17;
+        }
+    });
+```
+
+Note that every `getter` is expected to return a value.
+
+## Rule Details
+
+This rule enforces that a return statement is present in property getters.
+
+Examples of **incorrect** code for this rule:
+
+```js
+/*eslint getter-return: "error"*/
+
+p = {
+    get name(){
+        // no returns.
+    }
+};
+
+Object.defineProperty(p, "age", {
+    get: function (){
+        // no returns.
+    }
+});
+
+class P{
+    get name(){
+        // no returns.
+    }
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/*eslint getter-return: "error"*/
+
+p = {
+    get name(){
+        return "nicholas";
+    }
+};
+
+Object.defineProperty(p, "age", {
+    get: function (){
+        return 18;
+    }
+});
+
+class P{
+    get name(){
+        return "nicholas";
+    }
+}
+```
+
+## Options
+
+This rule has an object option:
+
+* `"allowImplicit": false` (default) disallows implicitly returning undefined with a return; statement.
+
+Examples of **correct** code for the `{ "allowImplicit": true }` option:
+
+```js
+/*eslint getter-return: ["error", { allowImplicit: true }]*/
+p = {
+    get name(){
+        return; // return undefined implicitly.
+    }
+};
+```
+
+## When Not To Use It
+
+If your project will not be using ES5 property getters you do not need this rule.
+
+## Further Reading
+
+* [MDN: Functions getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)
+* [Understanding ES6: Accessor Properties](https://leanpub.com/understandinges6/read/#leanpub-auto-accessor-properties)
diff --git a/lib/config.js b/lib/config.js
index c69d120ef..2db87d942 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -66,13 +66,14 @@ class Config {
         this.parser = options.parser;
         this.parserOptions = options.parserOptions || {};
 
+        this.configCache = new ConfigCache();
+
         this.baseConfig = options.baseConfig
             ? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this))
             : { rules: {} };
         this.baseConfig.filePath = "";
         this.baseConfig.baseDirectory = this.options.cwd;
 
-        this.configCache = new ConfigCache();
         this.configCache.setConfig(this.baseConfig.filePath, this.baseConfig);
         this.configCache.setMergedVectorConfig(this.baseConfig.filePath, this.baseConfig);
 
diff --git a/lib/config/config-cache.js b/lib/config/config-cache.js
index b89f33430..07436a87c 100644
--- a/lib/config/config-cache.js
+++ b/lib/config/config-cache.js
@@ -29,7 +29,7 @@ function hash(vector) {
 module.exports = class ConfigCache {
 
     constructor() {
-        this.filePathCache = new Map();
+        this.configFullNameCache = new Map();
         this.localHierarchyCache = new Map();
         this.mergedVectorCache = new Map();
         this.mergedCache = new Map();
@@ -37,23 +37,25 @@ module.exports = class ConfigCache {
 
     /**
      * Gets a config object from the cache for the specified config file path.
-     * @param {string} configFilePath the absolute path to the config file
+     * @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
+     * or the absolute path to a config file. This should uniquely identify a config.
      * @returns {Object|null} config object, if found in the cache, otherwise null
      * @private
      */
-    getConfig(configFilePath) {
-        return this.filePathCache.get(configFilePath);
+    getConfig(configFullName) {
+        return this.configFullNameCache.get(configFullName);
     }
 
     /**
      * Sets a config object in the cache for the specified config file path.
-     * @param {string} configFilePath the absolute path to the config file
+     * @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
+     * or the absolute path to a config file. This should uniquely identify a config.
      * @param {Object} config the config object to add to the cache
      * @returns {void}
      * @private
      */
-    setConfig(configFilePath, config) {
-        this.filePathCache.set(configFilePath, config);
+    setConfig(configFullName, config) {
+        this.configFullNameCache.set(configFullName, config);
     }
 
     /**
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index 832529952..3c790cf3b 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -488,12 +488,15 @@ function normalizePackageName(name, prefix) {
  * @returns {Object} An object containing 3 properties:
  * - 'filePath' (required) the resolved path that can be used directly to load the configuration.
  * - 'configName' the name of the configuration inside the plugin.
- * - 'configFullName' the name of the configuration as used in the eslint config (e.g. 'plugin:node/recommended').
+ * - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
+ *     or the absolute path to a config file. This should uniquely identify a config.
  * @private
  */
 function resolve(filePath, relativeTo) {
     if (isFilePath(filePath)) {
-        return { filePath: path.resolve(relativeTo || "", filePath) };
+        const fullPath = path.resolve(relativeTo || "", filePath);
+
+        return { filePath: fullPath, configFullName: fullPath };
     }
     let normalizedPackageName;
 
@@ -510,7 +513,7 @@ function resolve(filePath, relativeTo) {
     normalizedPackageName = normalizePackageName(filePath, "eslint-config");
     debug(`Attempting to resolve ${normalizedPackageName}`);
     filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
-    return { filePath };
+    return { filePath, configFullName: filePath };
 
 
 }
@@ -560,11 +563,12 @@ function loadFromDisk(resolvedPath, configContext) {
 /**
  * Loads a config object, applying extends if present.
  * @param {Object} configObject a config object to load
+ * @param {Config} configContext Context for the config instance
  * @returns {Object} the config object with extends applied if present, or the passed config if not
  * @private
  */
-function loadObject(configObject) {
-    return configObject.extends ? applyExtends(configObject, "") : configObject;
+function loadObject(configObject, configContext) {
+    return configObject.extends ? applyExtends(configObject, configContext, "") : configObject;
 }
 
 /**
@@ -579,7 +583,7 @@ function loadObject(configObject) {
 function load(filePath, configContext, relativeTo) {
     const resolvedPath = resolve(filePath, relativeTo);
 
-    const cachedConfig = configContext.configCache.getConfig(resolvedPath.filePath);
+    const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName);
 
     if (cachedConfig) {
         return cachedConfig;
@@ -590,7 +594,7 @@ function load(filePath, configContext, relativeTo) {
     if (config) {
         config.filePath = resolvedPath.filePath;
         config.baseDirectory = path.dirname(resolvedPath.filePath);
-        configContext.configCache.setConfig(resolvedPath.filePath, config);
+        configContext.configCache.setConfig(resolvedPath.configFullName, config);
     }
 
     return config;
diff --git a/lib/linter.js b/lib/linter.js
index d2f1f4657..b8b3a5676 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -1205,6 +1205,7 @@ class Linter extends EventEmitter {
             fixedResult,
             fixed = false,
             passNumber = 0;
+        const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
 
         /**
          * This loop continues until one of the following is true:
@@ -1218,10 +1219,10 @@ class Linter extends EventEmitter {
         do {
             passNumber++;
 
-            debug(`Linting code for ${options.filename} (pass ${passNumber})`);
+            debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
             messages = this.verify(text, config, options);
 
-            debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
+            debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
             fixedResult = SourceCodeFixer.applyFixes(this.getSourceCode(), messages);
 
             // stop if there are any syntax errors.
diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js
index 55225b8cc..142f0f24a 100644
--- a/lib/rules/dot-notation.js
+++ b/lib/rules/dot-notation.js
@@ -116,6 +116,15 @@ module.exports = {
                                 return null;
                             }
 
+                            if (node.object.type === "Identifier" && node.object.name === "let") {
+
+                                /*
+                                 * A statement that starts with `let[` is parsed as a destructuring variable declaration, not
+                                 * a MemberExpression.
+                                 */
+                                return null;
+                            }
+
                             return fixer.replaceTextRange(
                                 [dot.range[0], node.property.range[1]],
                                 `[${textAfterDot}"${node.property.name}"]`
diff --git a/lib/rules/getter-return.js b/lib/rules/getter-return.js
new file mode 100644
index 000000000..8a095ad29
--- /dev/null
+++ b/lib/rules/getter-return.js
@@ -0,0 +1,151 @@
+/**
+ * @fileoverview Enforces that a return statement is present in property getters.
+ * @author Aladdin-ADD(hh_2013@foxmail.com)
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("../ast-utils");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Checks a given code path segment is reachable.
+ *
+ * @param {CodePathSegment} segment - A segment to check.
+ * @returns {boolean} `true` if the segment is reachable.
+ */
+function isReachable(segment) {
+    return segment.reachable;
+}
+
+/**
+ * Gets a readable location.
+ *
+ * - FunctionExpression -> the function name or `function` keyword.
+ *
+ * @param {ASTNode} node - A function node to get.
+ * @returns {ASTNode|Token} The node or the token of a location.
+ */
+function getId(node) {
+    return node.id || node;
+}
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "enforce `return` statements in getters",
+            category: "Possible Errors",
+            recommended: false
+        },
+        fixable: null,
+        schema: [
+            {
+                type: "object",
+                properties: {
+                    allowImplicit: {
+                        type: "boolean"
+                    }
+                },
+                additionalProperties: false
+            }
+        ]
+    },
+
+    create(context) {
+
+        const options = context.options[0] || { allowImplicit: false };
+
+        let funcInfo = {
+            upper: null,
+            codePath: null,
+            hasReturn: false,
+            shouldCheck: false,
+            node: null
+        };
+
+        /**
+         * Checks whether or not the last code path segment is reachable.
+         * Then reports this function if the segment is reachable.
+         *
+         * If the last code path segment is reachable, there are paths which are not
+         * returned or thrown.
+         *
+         * @param {ASTNode} node - A node to check.
+         * @returns {void}
+         */
+        function checkLastSegment(node) {
+            if (funcInfo.shouldCheck &&
+                funcInfo.codePath.currentSegments.some(isReachable)
+            ) {
+                context.report({
+                    node,
+                    loc: getId(node).loc.start,
+                    message: funcInfo.hasReturn
+                        ? "Expected {{name}} to always return a value."
+                        : "Expected to return a value in {{name}}.",
+                    data: {
+                        name: astUtils.getFunctionNameWithKind(funcInfo.node)
+                    }
+                });
+            }
+        }
+
+        return {
+
+            // Stacks this function's information.
+            onCodePathStart(codePath, node) {
+                const parent = node.parent;
+
+                funcInfo = {
+                    upper: funcInfo,
+                    codePath,
+                    hasReturn: false,
+                    shouldCheck:
+                        node.type === "FunctionExpression" &&
+                        node.body.type === "BlockStatement" &&
+
+                        // check if it is a "getter", or a method named "get".
+                        (parent.kind === "get" || astUtils.getStaticPropertyName(parent) === "get"),
+                    node
+                };
+            },
+
+            // Pops this function's information.
+            onCodePathEnd() {
+                funcInfo = funcInfo.upper;
+            },
+
+            // Checks the return statement is valid.
+            ReturnStatement(node) {
+                if (funcInfo.shouldCheck) {
+                    funcInfo.hasReturn = true;
+
+                    // if allowImplicit: false, should also check node.argument
+                    if (!options.allowImplicit && !node.argument) {
+                        context.report({
+                            node,
+                            message: "Expected to return a value in {{name}}.",
+                            data: {
+                                name: astUtils.getFunctionNameWithKind(funcInfo.node)
+                            }
+                        });
+                    }
+                }
+            },
+
+            // Reports a given function if the last path is reachable.
+            "FunctionExpression:exit": checkLastSegment
+        };
+    }
+};
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 3027e7720..30ecc4757 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -303,6 +303,18 @@ class OffsetStorage {
         }
     }
 
+    /**
+     * Sets the desired offset of a token, ignoring the usual collapsing behavior.
+     * **WARNING**: This is usually not what you want to use. See `setDesiredOffset` instead.
+     * @param {Token} token The token
+     * @param {Token} offsetFrom The token that `token` should be offset from
+     * @param {number} offset The desired indent level
+     * @returns {void}
+     */
+    forceSetDesiredOffset(token, offsetFrom, offset) {
+        this.desiredOffsets[token.range[0]] = { offset, from: offsetFrom };
+    }
+
     /**
     * Sets the desired offset of multiple tokens
     * @param {Token[]} tokens A list of tokens. These tokens should be consecutive.
@@ -1274,7 +1286,35 @@ module.exports = {
             VariableDeclaration(node) {
                 const variableIndent = options.VariableDeclarator.hasOwnProperty(node.kind) ? options.VariableDeclarator[node.kind] : DEFAULT_VARIABLE_INDENT;
 
-                offsets.setDesiredOffsets(getTokensAndComments(node), sourceCode.getFirstToken(node), variableIndent);
+                if (node.declarations[node.declarations.length - 1].loc.start.line > node.loc.start.line) {
+
+                    /*
+                     * VariableDeclarator indentation is a bit different from other forms of indentation, in that the
+                     * indentation of an opening bracket sometimes won't match that of a closing bracket. For example,
+                     * the following indentations are correct:
+                     *
+                     * var foo = {
+                     *   ok: true
+                     * };
+                     *
+                     * var foo = {
+                     *     ok: true,
+                     *   },
+                     *   bar = 1;
+                     *
+                     * Account for when exiting the AST (after indentations have already been set for the nodes in
+                     * the declaration) by manually increasing the indentation level of the tokens in this declarator
+                     * on the same line as the start of the declaration, provided that there are declarators that
+                     * follow this one.
+                     */
+                    getTokensAndComments(node).forEach((token, index, tokens) => {
+                        if (index !== 0) {
+                            offsets.forceSetDesiredOffset(token, tokens[0], variableIndent);
+                        }
+                    });
+                } else {
+                    offsets.setDesiredOffsets(getTokensAndComments(node), sourceCode.getFirstToken(node), variableIndent);
+                }
                 const lastToken = sourceCode.getLastToken(node);
 
                 if (astUtils.isSemicolonToken(lastToken)) {
@@ -1294,37 +1334,6 @@ module.exports = {
                 }
             },
 
-            "VariableDeclarator:exit"(node) {
-
-                /*
-                 * VariableDeclarator indentation is a bit different from other forms of indentation, in that the
-                 * indentation of an opening bracket sometimes won't match that of a closing bracket. For example,
-                 * the following indentations are correct:
-                 *
-                 * var foo = {
-                 *   ok: true
-                 * };
-                 *
-                 * var foo = {
-                 *     ok: true,
-                 *   },
-                 *   bar = 1;
-                 *
-                 * Account for when exiting the AST (after indentations have already been set for the nodes in
-                 * the declaration) by manually increasing the indentation level of the tokens in the first declarator if the
-                 * parent declaration has more than one declarator.
-                 */
-                if (node.parent.declarations.length > 1 && node.parent.declarations[0] === node && node.init) {
-                    const valueTokens = new Set(getTokensAndComments(node.init));
-
-                    valueTokens.forEach(token => {
-                        if (!valueTokens.has(offsets.getFirstDependency(token))) {
-                            offsets.increaseOffset(token, options.VariableDeclarator[node.parent.kind]);
-                        }
-                    });
-                }
-            },
-
             WhileStatement: node => addBlocklessNodeIndent(node.body),
 
             "*:exit": checkForUnknownNode,
diff --git a/lib/rules/no-debugger.js b/lib/rules/no-debugger.js
index a158724fe..d79cb1816 100644
--- a/lib/rules/no-debugger.js
+++ b/lib/rules/no-debugger.js
@@ -5,6 +5,8 @@
 
 "use strict";
 
+const astUtils = require("../ast-utils");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -28,7 +30,10 @@ module.exports = {
                     node,
                     message: "Unexpected 'debugger' statement.",
                     fix(fixer) {
-                        return fixer.remove(node);
+                        if (astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
+                            return fixer.remove(node);
+                        }
+                        return null;
                     }
                 });
             }
diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js
index 9f48f1d81..fd6fd0b78 100644
--- a/lib/rules/no-extra-parens.js
+++ b/lib/rules/no-extra-parens.js
@@ -426,7 +426,7 @@ module.exports = {
                     secondToken.type === "Keyword" && (
                         secondToken.value === "function" ||
                         secondToken.value === "class" ||
-                        secondToken.value === "let" && astUtils.isOpeningBracketToken(sourceCode.getTokenAfter(secondToken))
+                        secondToken.value === "let" && astUtils.isOpeningBracketToken(sourceCode.getTokenAfter(secondToken, astUtils.isNotClosingParenToken))
                     )
                 )
             ) {
@@ -512,16 +512,27 @@ module.exports = {
             ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration),
             ExpressionStatement: node => checkExpressionOrExportStatement(node.expression),
 
-            ForInStatement(node) {
-                if (hasExcessParens(node.right)) {
-                    report(node.right);
-                }
-                if (hasExcessParens(node.left)) {
-                    report(node.left);
-                }
-            },
+            "ForInStatement, ForOfStatement"(node) {
+                if (node.left.type !== "VariableDeclarator") {
+                    const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken);
+
+                    if (
+                        firstLeftToken.value === "let" && (
+
+                            // If `let` is the only thing on the left side of the loop, it's the loop variable: `for ((let) of foo);`
+                            // Removing it will cause a syntax error, because it will be parsed as the start of a VariableDeclarator.
+                            firstLeftToken.range[1] === node.left.range[1] ||
 
-            ForOfStatement(node) {
+                            // If `let` is followed by a `[` token, it's a property access on the `let` value: `for ((let[foo]) of bar);`
+                            // Removing it will cause the property access to be parsed as a destructuring declaration of `foo` instead.
+                            astUtils.isOpeningBracketToken(
+                                sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken)
+                            )
+                        )
+                    ) {
+                        tokensToIgnore.add(firstLeftToken);
+                    }
+                }
                 if (hasExcessParens(node.right)) {
                     report(node.right);
                 }
diff --git a/package.json b/package.json
index b538001f6..15c0eb561 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint",
-  "version": "4.1.0",
+  "version": "4.1.1",
   "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
   "description": "An AST-based pattern checker for JavaScript.",
   "bin": {


Test patch:
diff --git a/tests/fixtures/config-file/plugins/.eslintrc2.yml b/tests/fixtures/config-file/plugins/.eslintrc2.yml
new file mode 100644
index 000000000..973d77096
--- /dev/null
+++ b/tests/fixtures/config-file/plugins/.eslintrc2.yml
@@ -0,0 +1,3 @@
+extends:
+    - plugin:test/foo
+    - plugin:test/bar
diff --git a/tests/lib/config.js b/tests/lib/config.js
index 935c210bd..59e1a47d0 100644
--- a/tests/lib/config.js
+++ b/tests/lib/config.js
@@ -158,6 +158,24 @@ describe("Config", () => {
             assert.deepEqual(customBaseConfig, { foo: "bar" });
             assert.equal(configHelper.options.format, "foo");
         });
+
+        it("should create config object when using baseConfig with extends", () => {
+            const customBaseConfig = {
+                extends: path.resolve(__dirname, "..", "fixtures", "config-extends", "array", ".eslintrc")
+            };
+            const configHelper = new Config({ baseConfig: customBaseConfig }, linter);
+
+            assert.deepEqual(configHelper.baseConfig.env, {
+                browser: false,
+                es6: true,
+                node: true
+            });
+            assert.deepEqual(configHelper.baseConfig.rules, {
+                "no-empty": 1,
+                "comma-dangle": 2,
+                "no-console": 2
+            });
+        });
     });
 
     describe("findLocalConfigFiles()", () => {
diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js
index a9d2baa0d..23a206b05 100644
--- a/tests/lib/config/config-file.js
+++ b/tests/lib/config/config-file.js
@@ -899,6 +899,49 @@ describe("ConfigFile", () => {
                     }
                 });
             });
+
+            it("should load two separate configs from a plugin", () => {
+                const stubConfig = new Config({}, new Linter());
+                const resolvedPath = path.resolve(PROJECT_PATH, "./node_modules/eslint-plugin-test/index.js");
+
+                const configDeps = {
+                    "../util/module-resolver": createStubModuleResolver({
+                        "eslint-plugin-test": resolvedPath
+                    }),
+                    "require-uncached"(filename) {
+                        return configDeps[filename];
+                    }
+                };
+
+                configDeps[resolvedPath] = {
+                    configs: {
+                        foo: { rules: { semi: 2, quotes: 1 } },
+                        bar: { rules: { quotes: 2, yoda: 2 } }
+                    }
+                };
+
+                const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps);
+
+                const configFilePath = getFixturePath("plugins/.eslintrc2.yml");
+                const config = StubbedConfigFile.load(configFilePath, stubConfig);
+
+                assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
+                    parserOptions: {},
+                    globals: {},
+                    env: {},
+                    rules: {
+                        semi: 2,
+                        quotes: 2,
+                        yoda: 2
+                    },
+                    extends: [
+                        "plugin:test/foo",
+                        "plugin:test/bar"
+                    ]
+                });
+            });
         });
 
         describe("even if config files have Unicode BOM,", () => {
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 4acc46b3f..88ce63cb1 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -3776,6 +3776,20 @@ describe("eslint", () => {
             assert.equal(messages.output, "var a;", "Fixes were applied correctly");
             assert.isTrue(messages.fixed);
         });
+
+        it("does not require a third argument", () => {
+            const fixResult = linter.verifyAndFix("var a", {
+                rules: {
+                    semi: 2
+                }
+            });
+
+            assert.deepEqual(fixResult, {
+                fixed: true,
+                messages: [],
+                output: "var a;"
+            });
+        });
     });
 
     describe("Edge cases", () => {
diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js
index f20334f66..08013540a 100644
--- a/tests/lib/rules/dot-notation.js
+++ b/tests/lib/rules/dot-notation.js
@@ -178,6 +178,12 @@ ruleTester.run("dot-notation", rule, {
             code: "foo['bar']instanceof baz",
             output: "foo.bar instanceof baz",
             errors: [{ message: "[\"bar\"] is better written in dot notation." }]
+        },
+        {
+            code: "let.if()",
+            output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration
+            options: [{ allowKeywords: false }],
+            errors: [{ message: ".if is a syntax error." }]
         }
     ]
 });
diff --git a/tests/lib/rules/getter-return.js b/tests/lib/rules/getter-return.js
new file mode 100644
index 000000000..646d44893
--- /dev/null
+++ b/tests/lib/rules/getter-return.js
@@ -0,0 +1,109 @@
+/**
+ * @fileoverview Enforces that a return statement is present in property getters.
+ * @author Aladdin-ADD(hh_2013@foxmail.com)
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/getter-return");
+const RuleTester = require("../../../lib/testers/rule-tester");
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+
+// data is not working, so specify a name: "getter 'bar'"
+const name = "getter 'bar'";
+const noReturnMessage = `Expected to return a value in ${name}.`;
+const noLastReturnMessage = `Expected ${name} to always return a value.`;
+const options = [{ allowImplicit: true }];
+
+ruleTester.run("getter-return", rule, {
+
+    valid: [
+
+        // test obj: get
+        // option: {allowImplicit: false}
+        { code: "var foo = { get bar(){return true;} };" },
+
+        // option: {allowImplicit: true}
+        { code: "var foo = { get bar() {return;} };", options },
+        { code: "var foo = { get bar(){return true;} };", options },
+        { code: "var foo = { get bar(){if(bar) {return;} return true;} };", options },
+
+        // test class: get
+        // option: {allowImplicit: false}
+        { code: "class foo { get bar(){return true;} }" },
+        { code: "class foo { get bar(){if(baz){return true;} else {return false;} } }" },
+        { code: "class foo { get(){return true;} }" },
+
+        // option: {allowImplicit: true}
+        { code: "class foo { get bar(){return true;} }", options },
+        { code: "class foo { get bar(){return;} }", options },
+
+        // test object.defineProperty(s)
+        // option: {allowImplicit: false}
+        { code: "Object.defineProperty(foo, \"bar\", { get: function () {return true;}});" },
+        { code: "Object.defineProperty(foo, \"bar\", { get: function () { ~function (){ return true; }();return true;}});" },
+        { code: "Object.defineProperies(foo, { bar: { get: function () {return true;}} });" },
+        { code: "Object.defineProperies(foo, { bar: { get: function () { ~function (){ return true; }(); return true;}} });" },
+
+        // option: {allowImplicit: true}
+        { code: "Object.defineProperty(foo, \"bar\", { get: function () {return true;}});", options },
+        { code: "Object.defineProperty(foo, \"bar\", { get: function (){return;}});", options },
+        { code: "Object.defineProperies(foo, { bar: { get: function () {return true;}} });", options },
+        { code: "Object.defineProperies(foo, { bar: { get: function () {return;}} });", options },
+
+        // not getter.
+        { code: "var get = function(){};" },
+        { code: "var get = function(){ return true; };" },
+        { code: "var foo = { bar(){} };" },
+        { code: "var foo = { bar(){ return true; } };" },
+        { code: "var foo = { bar: function(){} };" },
+        { code: "var foo = { bar(){ return true; } };" },
+        { code: "var foo = { bar: function(){return;} };" },
+        { code: "var foo = { bar: function(){return true;} };" }
+    ],
+
+    invalid: [
+
+        // test obj: get
+        // option: {allowImplicit: false}
+        { code: "var foo = { get bar() {} };", errors: [{ message: noReturnMessage }] },
+        { code: "var foo = { get bar(){if(baz) {return true;}} };", errors: [{ message: noLastReturnMessage }] },
+        { code: "var foo = { get bar() { ~function () {return true;}} };", errors: [{ message: noReturnMessage }] },
+
+        // option: {allowImplicit: true}
+        { code: "var foo = { get bar() {} };", options, errors: [{ message: noReturnMessage }] },
+        { code: "var foo = { get bar() {if (baz) {return;}} };", options, errors: [{ message: noLastReturnMessage }] },
+
+        // test class: get
+        // option: {allowImplicit: false}
+        { code: "class foo { get bar(){} }", errors: [{ message: noReturnMessage }] },
+        { code: "class foo { get bar(){ if (baz) { return true; }}}", errors: [{ noLastReturnMessage }] },
+        { code: "class foo { get bar(){ ~function () { return true; }()}}", errors: [{ noLastReturnMessage }] },
+
+        // option: {allowImplicit: true}
+        { code: "class foo { get bar(){} }", options, errors: [{ message: noReturnMessage }] },
+        { code: "class foo { get bar(){if (baz) {return true;} } }", options, errors: [{ message: noLastReturnMessage }] },
+
+        // test object.defineProperty(s)
+        // option: {allowImplicit: false}
+        { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", errors: [{ noReturnMessage }] },
+        { code: "Object.defineProperty(foo, \"bar\", { get: function (){if(bar) {return true;}}});", errors: [{ message: "Expected method 'get' to always return a value." }] },
+        { code: "Object.defineProperty(foo, \"bar\", { get: function (){ ~function () { return true; }()}});", errors: [{ noReturnMessage }] },
+        { code: "Object.defineProperies(foo, { bar: { get: function () {}} });", options, errors: [{ noReturnMessage }] },
+        { code: "Object.defineProperies(foo, { bar: { get: function (){if(bar) {return true;}}}});", options, errors: [{ message: "Expected method 'get' to always return a value." }] },
+        { code: "Object.defineProperies(foo, { bar: { get: function () {~function () { return true; }()}} });", options, errors: [{ noReturnMessage }] },
+
+        // option: {allowImplicit: true}
+        { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ message: "Expected to return a value in method 'get'." }] },
+        { code: "Object.defineProperies(foo, { bar: { get: function () {}} });", options, errors: [{ noReturnMessage }] }
+    ]
+});
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 12b42d712..e5aa32b2d 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -518,6 +518,64 @@ ruleTester.run("indent", rule, {
             `,
             options: [2, { VariableDeclarator: 2, SwitchCase: 1 }]
         },
+        {
+            code: unIndent`
+                var
+                    x = {
+                        a: 1,
+                    },
+                    y = {
+                        b: 2
+                    }
+            `
+        },
+        {
+            code: unIndent`
+                const
+                    x = {
+                        a: 1,
+                    },
+                    y = {
+                        b: 2
+                    }
+            `
+        },
+        {
+            code: unIndent`
+                let
+                    x = {
+                        a: 1,
+                    },
+                    y = {
+                        b: 2
+                    }
+            `
+        },
+        {
+            code: unIndent`
+                var foo = { a: 1 }, bar = {
+                    b: 2
+                };
+            `
+        },
+        {
+            code: unIndent`
+                var foo = { a: 1 }, bar = {
+                        b: 2
+                    },
+                    baz = {
+                        c: 3
+                    }
+            `
+        },
+        {
+            code: unIndent`
+                const {
+                        foo
+                    } = 1,
+                    bar = 2
+            `
+        },
         {
             code: unIndent`
                 var foo = 1,
diff --git a/tests/lib/rules/no-debugger.js b/tests/lib/rules/no-debugger.js
index a907405c8..9edf059c4 100644
--- a/tests/lib/rules/no-debugger.js
+++ b/tests/lib/rules/no-debugger.js
@@ -23,6 +23,15 @@ ruleTester.run("no-debugger", rule, {
         "var test = { debugger: 1 }; test.debugger;"
     ],
     invalid: [
-        { code: "debugger", errors: [{ message: "Unexpected 'debugger' statement.", type: "DebuggerStatement" }], output: "" }
+        {
+            code: "debugger",
+            output: "",
+            errors: [{ message: "Unexpected 'debugger' statement.", type: "DebuggerStatement" }]
+        },
+        {
+            code: "if (foo) debugger",
+            output: null,
+            errors: [{ message: "Unexpected 'debugger' statement.", type: "DebuggerStatement" }]
+        }
     ]
 });
diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js
index 54990b96e..be32041c8 100644
--- a/tests/lib/rules/no-extra-parens.js
+++ b/tests/lib/rules/no-extra-parens.js
@@ -450,7 +450,12 @@ ruleTester.run("no-extra-parens", rule, {
         {
             code: "((function(){}).foo)();",
             options: ["functions"]
-        }
+        },
+        "(let)[foo]",
+        "for ((let) in foo);",
+        "for ((let[foo]) in bar);",
+        "for ((let)[foo] in bar);",
+        "for ((let[foo].bar) in baz);"
     ],
 
     invalid: [
@@ -1052,6 +1057,24 @@ ruleTester.run("no-extra-parens", rule, {
             "MemberExpression",
             1,
             { parserOptions: { ecmaVersion: 2015 } }
+        ),
+        invalid(
+            "(let).foo",
+            "let.foo",
+            "Identifier",
+            1
+        ),
+        invalid(
+            "for ((let.foo) in bar);",
+            "for (let.foo in bar);",
+            "MemberExpression",
+            1
+        ),
+        invalid(
+            "for ((let).foo.bar in baz);",
+            "for (let.foo.bar in baz);",
+            "Identifier",
+            1
         )
     ]
 });