Repository: eslint/eslint
Pull request #8107 (2017-02-20 00:54:56)

Update: fix quotes false negative for string literals as template tags
**What is the purpose of this pull request? (put an "X" next to item)**

[x] Bug fix

**Tell us about your environment**

* **ESLint Version:** master
* **Node Version:** 7.5.0
* **npm Version:** 4.1.2

**What parser (default, Babel-ESLint, etc.) are you using?**

default

**Please show your full configuration:**

```yml
rules:
  quotes: error
parserOptions:
  ecmaVersion: 6
```

**What did you do? Please include the actual source code causing the issue.**

I absentmindedly spammed `` ` `` while working on something else, and I ended up with something like this:

`````js
````
`````

This is a tagged template literal, where the tag is the first ``` `` ``` and the quasi is the second ``` `` ```.

**What did you expect to happen?**

I expected the first ``` `` ``` to be reported, since it can be in double quotes. Only the second ``` `` ``` needs to be a template literal.

**What actually happened? Please include the actual, raw output from ESLint.**

No errors were reported.

**What changes did you make? (Give an overview)**

This fixes a tagged template check in `quotes`. Since the "literal" of a tagged template literal can't be converted to single/double quotes (e.g. `` foo`bar` ``), the rule currently ignores template literals that have a `TaggedTemplateLiteral` parent. However, it should only ignore these if the template literal is actually the `quasi` if the parent -- an error should still be reported if the template literal is the `tag` of the parent.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

eslintbot: LGTM
mention-bot: @not-an-aardvark, thanks for your PR! By analyzing the history of the files in this pull request, we identified @mysticatea, @nzakas and @vitorbal to be potential reviewers.
not-an-aardvark: That's true, but I suppose it's still better for the rule to be correct here.

Now I'm imagining this as a minification strategy :smile::

`````js
function foo() {
  throw new TypeError('not implemented yet');
}

// ->

function foo(){
  ````
}
`````

Patch:
diff --git a/Makefile.js b/Makefile.js
index 477522cc3..ad0f82c39 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -762,7 +762,7 @@ target.checkRuleFiles = function() {
 
     echo("Validating rules");
 
-    const eslintConf = require("./conf/eslint.json").rules;
+    const eslintRecommended = require("./conf/eslint-recommended").rules;
 
     const ruleFiles = find("lib/rules/").filter(fileType("js"));
     let errors = 0;
@@ -772,12 +772,12 @@ target.checkRuleFiles = function() {
         const docFilename = `docs/rules/${basename}.md`;
 
         /**
-         * Check if basename is present in eslint conf
+         * Check if basename is present in eslint:recommended configuration.
          * @returns {boolean} true if present
          * @private
          */
         function isInConfig() {
-            return eslintConf.hasOwnProperty(basename);
+            return eslintRecommended.hasOwnProperty(basename);
         }
 
         /**
@@ -810,9 +810,9 @@ target.checkRuleFiles = function() {
             }
         }
 
-        // check for default configuration
+        // check for recommended configuration
         if (!isInConfig()) {
-            console.error("Missing default setting for %s in conf/eslint.json", basename);
+            console.error("Missing eslint:recommended setting for %s in conf/eslint-recommendd.js", basename);
             errors++;
         }
 
diff --git a/conf/eslint.json b/conf/eslint-recommended.js
similarity index 95%
rename from conf/eslint.json
rename to conf/eslint-recommended.js
index 774a06d8f..98a8cd26c 100755
--- a/conf/eslint.json
+++ b/conf/eslint-recommended.js
@@ -1,7 +1,81 @@
-{
-    "parser": "espree",
-    "ecmaFeatures": {},
-    "rules": {
+/**
+ * @fileoverview Configuration applied when a user configuration extends from
+ * eslint:recommended.
+ * @author Nicholas C. Zakas
+ */
+
+"use strict";
+
+/* eslint sort-keys: ["error", "asc"], quote-props: ["error", "consistent"] */
+/* eslint-disable sort-keys */
+
+module.exports = {
+    parser: "espree",
+    ecmaFeatures: {},
+
+    rules: {
+
+        /* eslint-enable sort-keys */
+        "accessor-pairs": "off",
+        "array-bracket-spacing": "off",
+        "array-callback-return": "off",
+        "arrow-body-style": "off",
+        "arrow-parens": "off",
+        "arrow-spacing": "off",
+        "block-scoped-var": "off",
+        "block-spacing": "off",
+        "brace-style": "off",
+        "callback-return": "off",
+        "camelcase": "off",
+        "capitalized-comments": "off",
+        "class-methods-use-this": "off",
+        "comma-dangle": "off",
+        "comma-spacing": "off",
+        "comma-style": "off",
+        "complexity": "off",
+        "computed-property-spacing": "off",
+        "consistent-return": "off",
+        "consistent-this": "off",
+        "constructor-super": "error",
+        "curly": "off",
+        "default-case": "off",
+        "dot-location": "off",
+        "dot-notation": "off",
+        "eol-last": "off",
+        "eqeqeq": "off",
+        "func-call-spacing": "off",
+        "func-name-matching": "off",
+        "func-names": "off",
+        "func-style": "off",
+        "generator-star-spacing": "off",
+        "global-require": "off",
+        "guard-for-in": "off",
+        "handle-callback-err": "off",
+        "id-blacklist": "off",
+        "id-length": "off",
+        "id-match": "off",
+        "indent": "off",
+        "init-declarations": "off",
+        "jsx-quotes": "off",
+        "key-spacing": "off",
+        "keyword-spacing": "off",
+        "line-comment-position": "off",
+        "linebreak-style": "off",
+        "lines-around-comment": "off",
+        "lines-around-directive": "off",
+        "max-depth": "off",
+        "max-len": "off",
+        "max-lines": "off",
+        "max-nested-callbacks": "off",
+        "max-params": "off",
+        "max-statements": "off",
+        "max-statements-per-line": "off",
+        "multiline-ternary": "off",
+        "new-cap": "off",
+        "new-parens": "off",
+        "newline-after-var": "off",
+        "newline-before-return": "off",
+        "newline-per-chained-call": "off",
         "no-alert": "off",
         "no-array-constructor": "off",
         "no-await-in-loop": "off",
@@ -100,20 +174,20 @@
         "no-sequences": "off",
         "no-shadow": "off",
         "no-shadow-restricted-names": "off",
-        "no-whitespace-before-property": "off",
         "no-spaced-func": "off",
         "no-sparse-arrays": "error",
         "no-sync": "off",
         "no-tabs": "off",
+        "no-template-curly-in-string": "off",
         "no-ternary": "off",
-        "no-trailing-spaces": "off",
         "no-this-before-super": "error",
         "no-throw-literal": "off",
+        "no-trailing-spaces": "off",
         "no-undef": "error",
         "no-undef-init": "off",
         "no-undefined": "off",
-        "no-unexpected-multiline": "error",
         "no-underscore-dangle": "off",
+        "no-unexpected-multiline": "error",
         "no-unmodified-loop-condition": "off",
         "no-unneeded-ternary": "off",
         "no-unreachable": "error",
@@ -130,70 +204,11 @@
         "no-useless-escape": "off",
         "no-useless-rename": "off",
         "no-useless-return": "off",
-        "no-void": "off",
         "no-var": "off",
+        "no-void": "off",
         "no-warning-comments": "off",
+        "no-whitespace-before-property": "off",
         "no-with": "off",
-        "array-bracket-spacing": "off",
-        "array-callback-return": "off",
-        "arrow-body-style": "off",
-        "arrow-parens": "off",
-        "arrow-spacing": "off",
-        "accessor-pairs": "off",
-        "block-scoped-var": "off",
-        "block-spacing": "off",
-        "brace-style": "off",
-        "callback-return": "off",
-        "camelcase": "off",
-        "capitalized-comments": "off",
-        "class-methods-use-this": "off",
-        "comma-dangle": "off",
-        "comma-spacing": "off",
-        "comma-style": "off",
-        "complexity": "off",
-        "computed-property-spacing": "off",
-        "consistent-return": "off",
-        "consistent-this": "off",
-        "constructor-super": "error",
-        "curly": "off",
-        "default-case": "off",
-        "dot-location": "off",
-        "dot-notation": "off",
-        "eol-last": "off",
-        "eqeqeq": "off",
-        "func-call-spacing": "off",
-        "func-names": "off",
-        "func-name-matching": "off",
-        "func-style": "off",
-        "generator-star-spacing": "off",
-        "global-require": "off",
-        "guard-for-in": "off",
-        "handle-callback-err": "off",
-        "id-blacklist": "off",
-        "id-length": "off",
-        "id-match": "off",
-        "indent": "off",
-        "init-declarations": "off",
-        "jsx-quotes": "off",
-        "key-spacing": "off",
-        "keyword-spacing": "off",
-        "linebreak-style": "off",
-        "line-comment-position": "off",
-        "lines-around-comment": "off",
-        "lines-around-directive": "off",
-        "max-depth": "off",
-        "max-len": "off",
-        "max-lines": "off",
-        "max-nested-callbacks": "off",
-        "max-params": "off",
-        "max-statements": "off",
-        "max-statements-per-line": "off",
-        "multiline-ternary": "off",
-        "new-cap": "off",
-        "new-parens": "off",
-        "newline-after-var": "off",
-        "newline-before-return": "off",
-        "newline-per-chained-call": "off",
         "object-curly-newline": "off",
         "object-curly-spacing": ["off", "never"],
         "object-property-newline": "off",
@@ -221,8 +236,8 @@
         "rest-spread-spacing": "off",
         "semi": "off",
         "semi-spacing": "off",
-        "sort-keys": "off",
         "sort-imports": "off",
+        "sort-keys": "off",
         "sort-vars": "off",
         "space-before-blocks": "off",
         "space-before-function-paren": "off",
@@ -241,8 +256,7 @@
         "vars-on-top": "off",
         "wrap-iife": "off",
         "wrap-regex": "off",
-        "no-template-curly-in-string": "off",
         "yield-star-spacing": "off",
         "yoda": "off"
     }
-}
+};
diff --git a/lib/config.js b/lib/config.js
index 9c56e7ad9..03fda87c9 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -234,8 +234,9 @@ class Config {
     }
 
     /**
-     * Build a config object merging the base config (conf/eslint.json), the
-     * environments config (conf/environments.js) and eventually the user config.
+     * Build a config object merging the base config (conf/eslint-recommended),
+     * the environments config (conf/environments.js) and eventually the user
+     * config.
      * @param {string} filePath a file in whose directory we start looking for a local config
      * @returns {Object} config object
      */
diff --git a/lib/config/autoconfig.js b/lib/config/autoconfig.js
index 4752a3444..4a50ce25c 100644
--- a/lib/config/autoconfig.js
+++ b/lib/config/autoconfig.js
@@ -13,7 +13,7 @@ const lodash = require("lodash"),
     eslint = require("../eslint"),
     configRule = require("./config-rule"),
     ConfigOps = require("./config-ops"),
-    recConfig = require("../../conf/eslint.json");
+    recConfig = require("../../conf/eslint-recommended");
 
 const debug = require("debug")("eslint:autoconfig");
 
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index 9817f8510..2c4dde64c 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -23,7 +23,7 @@ const fs = require("fs"),
     stripBom = require("strip-bom"),
     stripComments = require("strip-json-comments"),
     stringify = require("json-stable-stringify"),
-    defaultOptions = require("../../conf/eslint.json"),
+    defaultOptions = require("../../conf/eslint-recommended"),
     requireUncached = require("require-uncached");
 
 const debug = require("debug")("eslint:config-file");
@@ -364,10 +364,10 @@ function applyExtends(config, filePath, relativeTo) {
         if (parentPath === "eslint:recommended") {
 
             /*
-             * Add an explicit substitution for eslint:recommended to conf/eslint.json
-             * this lets us use the eslint.json file as the recommended rules
+             * Add an explicit substitution for eslint:recommended to
+             * conf/eslint-recommended.js.
              */
-            parentPath = path.resolve(__dirname, "../../conf/eslint.json");
+            parentPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
         } else if (parentPath === "eslint:all") {
 
             /*
@@ -390,6 +390,13 @@ function applyExtends(config, filePath, relativeTo) {
             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
diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js
index c7e1d4523..0062a4650 100644
--- a/lib/config/config-initializer.js
+++ b/lib/config/config-initializer.js
@@ -17,7 +17,7 @@ const util = require("util"),
     ConfigOps = require("./config-ops"),
     getSourceCodeOfFiles = require("../util/source-code-util").getSourceCodeOfFiles,
     npmUtil = require("../util/npm-util"),
-    recConfig = require("../../conf/eslint.json"),
+    recConfig = require("../../conf/eslint-recommended"),
     log = require("../logging");
 
 const debug = require("debug")("eslint:config-initializer");
diff --git a/lib/eslint.js b/lib/eslint.js
index 15cb0b4b6..01df167fb 100755
--- a/lib/eslint.js
+++ b/lib/eslint.js
@@ -14,7 +14,7 @@ const assert = require("assert"),
     escope = require("escope"),
     levn = require("levn"),
     blankScriptAST = require("../conf/blank-script.json"),
-    DEFAULT_PARSER = require("../conf/eslint.json").parser,
+    DEFAULT_PARSER = require("../conf/eslint-recommended").parser,
     replacements = require("../conf/replacements.json"),
     CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
     ConfigOps = require("./config/config-ops"),
@@ -1192,7 +1192,7 @@ module.exports = (function() {
      * @returns {Object} Object mapping rule IDs to their default configurations
      */
     api.defaults = function() {
-        return require("../conf/eslint.json");
+        return require("../conf/eslint-recommended");
     };
 
     /**
diff --git a/lib/rules/quotes.js b/lib/rules/quotes.js
index dbef55854..1085ea33b 100644
--- a/lib/rules/quotes.js
+++ b/lib/rules/quotes.js
@@ -257,7 +257,11 @@ module.exports = {
             TemplateLiteral(node) {
 
                 // If backticks are expected or it's a tagged template, then this shouldn't throw an errors
-                if (allowTemplateLiterals || quoteOption === "backtick" || node.parent.type === "TaggedTemplateExpression") {
+                if (
+                    allowTemplateLiterals ||
+                    quoteOption === "backtick" ||
+                    node.parent.type === "TaggedTemplateExpression" && node === node.parent.quasi
+                ) {
                     return;
                 }
 
diff --git a/messages/extend-config-missing.txt b/messages/extend-config-missing.txt
new file mode 100644
index 000000000..38e645819
--- /dev/null
+++ b/messages/extend-config-missing.txt
@@ -0,0 +1,3 @@
+ESLint couldn't find the config "<%- configName %>" to extend from. Please check that the name of the config is correct.
+
+If you still have problems, please stop by https://gitter.im/eslint/eslint to chat with the team.


Test patch:
diff --git a/tests/bench/bench.js b/tests/bench/bench.js
index bf2f12edd..394cd7dd0 100644
--- a/tests/bench/bench.js
+++ b/tests/bench/bench.js
@@ -1,7 +1,7 @@
 var eslint = require("../../lib/eslint"),
     fs = require("fs");
 
-var config = require("../../conf/eslint.json");
+var config = require("../../conf/eslint-recommended");
 
 var large = fs.readFileSync(__dirname + "/large.js", "utf8"), 
     medium = fs.readFileSync(__dirname + "/medium.js", "utf8"),
diff --git a/tests/fixtures/config-file/js/.eslintrc.parser3.js b/tests/fixtures/config-file/js/.eslintrc.parser3.js
index 0912b5b82..66463377b 100644
--- a/tests/fixtures/config-file/js/.eslintrc.parser3.js
+++ b/tests/fixtures/config-file/js/.eslintrc.parser3.js
@@ -1,4 +1,4 @@
-var defaultOptions = require("../../../../conf/eslint.json");
+var defaultOptions = require("../../../../conf/eslint-recommended");
 
 module.exports = {
     parser: defaultOptions.parser,
diff --git a/tests/lib/cli.js b/tests/lib/cli.js
index 53aee5447..8deac208c 100644
--- a/tests/lib/cli.js
+++ b/tests/lib/cli.js
@@ -460,7 +460,7 @@ describe("cli", () => {
                 getFixturePath("globals-node.js")
             ];
 
-            cli.execute(`--no-eslintrc --config ./conf/eslint.json --no-ignore ${files.join(" ")}`);
+            cli.execute(`--no-eslintrc --config ./conf/eslint-recommended.js --no-ignore ${files.join(" ")}`);
 
             assert.equal(log.info.args[0][0].split("\n").length, 11);
         });
diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js
index ee99669be..ed5842779 100644
--- a/tests/lib/config/config-file.js
+++ b/tests/lib/config/config-file.js
@@ -199,7 +199,7 @@ describe("ConfigFile", () => {
 
         });
 
-        it("should throw an error when extends config is not found", () => {
+        it("should throw an error when extends config module is not found", () => {
 
             const configDeps = {
                 "../util/module-resolver": createStubModuleResolver({})
@@ -216,6 +216,40 @@ describe("ConfigFile", () => {
 
         });
 
+        it("should throw an error when an eslint config is not found", () => {
+
+            const configDeps = {
+                "../util/module-resolver": createStubModuleResolver({})
+            };
+
+            const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps);
+
+            assert.throws(() => {
+                StubbedConfigFile.applyExtends({
+                    extends: "eslint:foo",
+                    rules: { eqeqeq: 2 }
+                }, "/whatever");
+            }, /Failed to load config "eslint:foo" to extend from./);
+
+        });
+
+        it("should throw an error when a plugin config is not found", () => {
+
+            const configDeps = {
+                "../util/module-resolver": createStubModuleResolver({})
+            };
+
+            const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps);
+
+            assert.throws(() => {
+                StubbedConfigFile.applyExtends({
+                    extends: "plugin:foo",
+                    rules: { eqeqeq: 2 }
+                }, "/whatever");
+            }, /Failed to load config "plugin:foo" to extend from./);
+
+        });
+
         it("should apply extensions recursively when specified from package", () => {
 
             const resolvedPaths = [
diff --git a/tests/lib/rules/quotes.js b/tests/lib/rules/quotes.js
index 4439e988b..19a9c65bb 100644
--- a/tests/lib/rules/quotes.js
+++ b/tests/lib/rules/quotes.js
@@ -302,6 +302,12 @@ ruleTester.run("quotes", rule, {
             output: "var foo = \"foo\\\\\\\nbar\";",
             parserOptions: { ecmaVersion: 6 },
             errors: [{ message: "Strings must use doublequote.", type: "TemplateLiteral" }]
+        },
+        {
+            code: "````",
+            output: "\"\"``",
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "Strings must use doublequote.", type: "TemplateLiteral", line: 1, column: 1 }]
         }
     ]
 });