Repository: eslint/eslint (js)

Issue:
"Extends" with plugins displays a confusing error message
**Tell us about your environment**

* **ESLint Version:** 3.16.1
* **Node Version:** 7.6.0
* **yarn Version:** 0.20.3

**What parser (default, Babel-ESLint, etc.) are you using?** babel-eslint

**Please show your full configuration:**
```js
module.exports = {
  extends: ['airbnb', 'plugin:flowtype/recommended'],
  plugins: ['flowtype'],
  rules: {
    'flowtype/define-flow-type': 1,
    'flowtype/use-flow-type': 1,
    'flowtype/delimiter-dangle': [2, 'always'],
    'flowtype/semi': [2, 'always'],
  },
  settings: {
    flowtype: {
      onlyFilesWithFlowAnnotation: true,
    },
  },
};
```

package.json:
```json
...
"eslint-plugin-flowtype": "2.30.0",
...
```

**What did you expect to happen?**
My code would be linted with 'flowtype' rules.

**What actually happened? Please include the actual, raw output from ESLint.**
I get:
```
Oops! Something went wrong! :(

ESLint couldn't find the config "plugin:flowtype/recommended" to extend from. Please check that the name of the config is correct.
```

Pull request:
Fix: wrong comment when module not found in config (fixes #8192)
**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[X] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] 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)**
PR https://github.com/eslint/eslint/pull/8100 modified the message when there was an error loading a plugin or config, but didn't account for other possible errors. In this case, `babel-eslint` was not present, and the error was misleading.

**Is there anything you'd like reviewers to focus on?**
Is there a cleaner way to check for this rather than using the error message?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2cd543207..3b2fe22cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+v3.17.1 - March 6, 2017
+
+* f8c8e6e Build: change mock-fs path without SSH (fixes #8207) (#8208) (Toru Nagashima)
+* f713f11 Fix: nonblock-statement-body-position multiline error (fixes #8202) (#8203) (Teddy Katz)
+* 41e3d9c Fix: `operator-assignment` with parenthesized expression (fixes #8190) (#8197) (alberto)
+* 5e3bca7 Chore: add eslint-plugin-eslint-plugin (#8198) (Teddy Katz)
+* 580da36 Chore: add missing `output` property to tests (#8195) (alberto)
+
 v3.17.0 - March 3, 2017
 
 * 4fdf6d7 Update: deprecate `applyDefaultPatterns` in `line-comment-position` (#8183) (alberto)
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index 2c4dde64c..4e886b8af 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -183,6 +183,22 @@ function loadPackageJSONConfigFile(filePath) {
     }
 }
 
+/**
+ * Creates an error to notify about a missing config to extend from.
+ * @param {string} configName The name of the missing config.
+ * @returns {Error} The error object to throw
+ * @private
+ */
+function configMissingError(configName) {
+    const error = new Error(`Failed to load config "${configName}" to extend from.`);
+
+    error.messageTemplate = "extend-config-missing";
+    error.messageData = {
+        configName
+    };
+    return error;
+}
+
 /**
  * Loads a configuration file regardless of the source. Inspects the file path
  * to determine the correctly way to load the config file.
@@ -199,6 +215,9 @@ function loadConfigFile(file) {
             config = loadJSConfigFile(filePath);
             if (file.configName) {
                 config = config.configs[file.configName];
+                if (!config) {
+                    throw configMissingError(file.configFullName);
+                }
             }
             break;
 
@@ -340,6 +359,33 @@ function getLookupPath(configFilePath) {
     return path.join(basedir, "node_modules");
 }
 
+/**
+ * Resolves a eslint core config path
+ * @param {string} name The eslint config name.
+ * @returns {string} The resolved path of the config.
+ * @private
+ */
+function getEslintCoreConfigPath(name) {
+    if (name === "eslint:recommended") {
+
+       /*
+        * Add an explicit substitution for eslint:recommended to
+        * conf/eslint-recommended.js.
+        */
+        return path.resolve(__dirname, "../../conf/eslint-recommended.js");
+    }
+
+    if (name === "eslint:all") {
+
+       /*
+        * Add an explicit substitution for eslint:all to conf/eslint-all.js
+        */
+        return path.resolve(__dirname, "../../conf/eslint-all.js");
+    }
+
+    throw configMissingError(name);
+}
+
 /**
  * Applies values from the "extends" field in a configuration file.
  * @param {Object} config The configuration information.
@@ -360,43 +406,23 @@ function applyExtends(config, filePath, relativeTo) {
 
     // Make the last element in an array take the highest precedence
     config = configExtends.reduceRight((previousValue, parentPath) => {
-
-        if (parentPath === "eslint:recommended") {
-
-            /*
-             * Add an explicit substitution for eslint:recommended to
-             * conf/eslint-recommended.js.
-             */
-            parentPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
-        } else if (parentPath === "eslint:all") {
-
-            /*
-             * Add an explicit substitution for eslint:all to conf/eslint-all.js
-             */
-            parentPath = path.resolve(__dirname, "../../conf/eslint-all.js");
-        } else if (isFilePath(parentPath)) {
-
-            /*
-             * If the `extends` path is relative, use the directory of the current configuration
-             * file as the reference point. Otherwise, use as-is.
-             */
-            parentPath = (!path.isAbsolute(parentPath)
-                ? path.join(relativeTo || path.dirname(filePath), parentPath)
-                : parentPath
-            );
-        }
-
         try {
+            if (parentPath.startsWith("eslint:")) {
+                parentPath = getEslintCoreConfigPath(parentPath);
+            } else if (isFilePath(parentPath)) {
+
+                /*
+                 * If the `extends` path is relative, use the directory of the current configuration
+                 * file as the reference point. Otherwise, use as-is.
+                 */
+                parentPath = (path.isAbsolute(parentPath)
+                    ? parentPath
+                    : path.join(relativeTo || path.dirname(filePath), parentPath)
+                );
+            }
             debug(`Loading ${parentPath}`);
             return ConfigOps.merge(load(parentPath, false, relativeTo), previousValue);
         } catch (e) {
-            if (parentPath.indexOf("plugin:") === 0 || parentPath.indexOf("eslint:") === 0) {
-                e.message = `Failed to load config "${parentPath}" to extend from.`;
-                e.messageTemplate = "extend-config-missing";
-                e.messageData = {
-                    configName: parentPath
-                };
-            }
 
             /*
              * If the file referenced by `extends` failed to load, add the path
@@ -462,7 +488,10 @@ function normalizePackageName(name, prefix) {
  * or package name.
  * @param {string} filePath The filepath to resolve.
  * @param {string} [relativeTo] The path to resolve relative to.
- * @returns {Object} A path that can be used directly to load the configuration.
+ * @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').
  * @private
  */
 function resolve(filePath, relativeTo) {
@@ -471,14 +500,15 @@ function resolve(filePath, relativeTo) {
     }
     let normalizedPackageName;
 
-    if (filePath.indexOf("plugin:") === 0) {
-        const packagePath = filePath.substr(7, filePath.lastIndexOf("/") - 7);
+    if (filePath.startsWith("plugin:")) {
+        const configFullName = filePath;
+        const pluginName = filePath.substr(7, filePath.lastIndexOf("/") - 7);
         const configName = filePath.substr(filePath.lastIndexOf("/") + 1, filePath.length - filePath.lastIndexOf("/") - 1);
 
-        normalizedPackageName = normalizePackageName(packagePath, "eslint-plugin");
+        normalizedPackageName = normalizePackageName(pluginName, "eslint-plugin");
         debug(`Attempting to resolve ${normalizedPackageName}`);
         filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
-        return { filePath, configName };
+        return { filePath, configName, configFullName };
     }
     normalizedPackageName = normalizePackageName(filePath, "eslint-config");
     debug(`Attempting to resolve ${normalizedPackageName}`);
diff --git a/lib/rules/nonblock-statement-body-position.js b/lib/rules/nonblock-statement-body-position.js
index d6ee126da..212e36a57 100644
--- a/lib/rules/nonblock-statement-body-position.js
+++ b/lib/rules/nonblock-statement-body-position.js
@@ -77,7 +77,7 @@ module.exports = {
                     message: "Expected a linebreak before this statement.",
                     fix: fixer => fixer.insertTextBefore(node, "\n")
                 });
-            } else if (tokenBefore.loc.end.line !== node.loc.end.line && option === "beside") {
+            } else if (tokenBefore.loc.end.line !== node.loc.start.line && option === "beside") {
                 context.report({
                     node,
                     message: "Expected no linebreak before this statement.",
diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js
index a6bbb177a..43f21abc8 100644
--- a/lib/rules/operator-assignment.js
+++ b/lib/rules/operator-assignment.js
@@ -135,7 +135,7 @@ module.exports = {
                                 const equalsToken = getOperatorToken(node);
                                 const operatorToken = getOperatorToken(expr);
                                 const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
-                                const rightText = sourceCode.getText().slice(operatorToken.range[1], node.range[1]);
+                                const rightText = sourceCode.getText().slice(operatorToken.range[1], expr.right.range[1]);
 
                                 return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`);
                             }
diff --git a/package.json b/package.json
index cdd8cc0dc..d9a0110b6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint",
-  "version": "3.17.0",
+  "version": "3.17.1",
   "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
   "description": "An AST-based pattern checker for JavaScript.",
   "bin": {
@@ -97,7 +97,7 @@
     "load-perf": "^0.2.0",
     "markdownlint": "^0.3.1",
     "mocha": "^2.4.5",
-    "mock-fs": "git://github.com/not-an-aardvark/mock-fs/#06868bbd7724707f9324b237bdde28f05f7a01d5",
+    "mock-fs": "not-an-aardvark/mock-fs#06868bbd7724707f9324b237bdde28f05f7a01d5",
     "npm-license": "^0.3.2",
     "phantomjs-prebuilt": "^2.1.7",
     "proxyquire": "^1.7.10",


Test patch:
diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js
index ed5842779..5b6521909 100644
--- a/tests/lib/config/config-file.js
+++ b/tests/lib/config/config-file.js
@@ -233,20 +233,63 @@ describe("ConfigFile", () => {
 
         });
 
+        it("should throw an error when a parser in a plugin config is not found", () => {
+            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: {
+                    bar: {
+                        parser: "babel-eslint"
+                    }
+                }
+            };
+
+            const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps);
+
+            assert.throws(() => {
+                StubbedConfigFile.applyExtends({
+                    extends: "plugin:test/bar",
+                    rules: { eqeqeq: 2 }
+                }, "/whatever");
+            }, /Cannot find module 'babel-eslint'/);
+
+        });
+
         it("should throw an error when a plugin config is not found", () => {
+            const resolvedPath = path.resolve(PROJECT_PATH, "./node_modules/eslint-plugin-test/index.js");
 
             const configDeps = {
-                "../util/module-resolver": createStubModuleResolver({})
+                "../util/module-resolver": createStubModuleResolver({
+                    "eslint-plugin-test": resolvedPath
+                }),
+                "require-uncached"(filename) {
+                    return configDeps[filename];
+                }
+            };
+
+            configDeps[resolvedPath] = {
+                configs: {
+                    baz: {}
+                }
             };
 
             const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps);
 
             assert.throws(() => {
                 StubbedConfigFile.applyExtends({
-                    extends: "plugin:foo",
+                    extends: "plugin:test/bar",
                     rules: { eqeqeq: 2 }
                 }, "/whatever");
-            }, /Failed to load config "plugin:foo" to extend from./);
+            }, /Failed to load config "plugin:test\/bar" to extend from./);
 
         });
 
diff --git a/tests/lib/rules/nonblock-statement-body-position.js b/tests/lib/rules/nonblock-statement-body-position.js
index 37a3b2756..37dffb20f 100644
--- a/tests/lib/rules/nonblock-statement-body-position.js
+++ b/tests/lib/rules/nonblock-statement-body-position.js
@@ -32,6 +32,11 @@ ruleTester.run("nonblock-statement-body-position", rule, {
         "for (foo in bar) baz;",
         "for (foo of bar) baz;",
         "if (foo) bar; else baz;",
+        `
+            if (foo) bar(
+                baz
+            );
+        `,
         {
             code: "if (foo) bar();",
             options: ["beside"]
diff --git a/tests/lib/rules/operator-assignment.js b/tests/lib/rules/operator-assignment.js
index d586878d7..e0f3e4e13 100644
--- a/tests/lib/rules/operator-assignment.js
+++ b/tests/lib/rules/operator-assignment.js
@@ -149,6 +149,11 @@ ruleTester.run("operator-assignment", rule, {
         output: "x += y",
         options: ["always"],
         errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x = (x + y)",
+        output: "x += y",
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
     }, {
         code: "x += y",
         output: "x = x + y",