Repository: eslint/eslint
Pull request #10010 (2018-02-23 21:45:37)

Chore: avoid useless catch clauses that just rethrow errors
<!--
    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)**

[x] 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 (https://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 commit removes an unnecessary `catch` clause from the codebase that just rethrows the error. It also adds an internal linting rule to avoid code like that in the future.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular



Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index b7b73ec55..ea75710e8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -25,6 +25,7 @@ module.exports = {
         "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"],
         "eslint-plugin/test-case-property-ordering": "error",
         "eslint-plugin/test-case-shorthand-strings": "error",
-        "rulesdir/multiline-comment-style": "error"
+        "rulesdir/multiline-comment-style": "error",
+        "rulesdir/no-useless-catch": "error"
     }
 };
diff --git a/conf/default-config-options.js b/conf/default-config-options.js
deleted file mode 100644
index 96fe25ce6..000000000
--- a/conf/default-config-options.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * @fileoverview Default config options
- * @author Teddy Katz
- */
-
-"use strict";
-
-/**
- * Freezes an object and all its nested properties
- * @param {Object} obj The object to deeply freeze
- * @returns {Object} `obj` after freezing it
- */
-function deepFreeze(obj) {
-    if (obj === null || typeof obj !== "object") {
-        return obj;
-    }
-
-    Object.keys(obj).map(key => obj[key]).forEach(deepFreeze);
-    return Object.freeze(obj);
-}
-
-module.exports = deepFreeze({
-    env: {},
-    globals: {},
-    rules: {},
-    settings: {},
-    parser: "espree",
-    parserOptions: {}
-});
diff --git a/lib/linter.js b/lib/linter.js
index 8e95c3a71..207e2cad0 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -14,7 +14,6 @@ const eslintScope = require("eslint-scope"),
     levn = require("levn"),
     lodash = require("lodash"),
     blankScriptAST = require("../conf/blank-script.json"),
-    defaultConfig = require("../conf/default-config-options.js"),
     CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
     ConfigOps = require("./config/config-ops"),
     validator = require("./config/config-validator"),
@@ -33,6 +32,7 @@ const eslintScope = require("eslint-scope"),
 
 const debug = require("debug")("eslint:linter");
 const MAX_AUTOFIX_PASSES = 10;
+const DEFAULT_PARSER_NAME = "espree";
 
 //------------------------------------------------------------------------------
 // Typedefs
@@ -415,16 +415,13 @@ function normalizeEcmaVersion(ecmaVersion, isModule) {
 }
 
 /**
- * Process initial config to make it safe to extend by file comment config
+ * Populates a config file with the default values, and merges any parserOptions stored in an env into
+ * the parserOptions of the populated config.
  * @param  {Object} config Initial config
  * @param  {Environments} envContext Env context
- * @returns {Object}        Processed config
+ * @returns {Object} Processed config
  */
-function prepareConfig(config, envContext) {
-    config.globals = config.globals || {};
-    const copiedRules = {};
-    let parserOptions = {};
-
+function populateConfig(config, envContext) {
     if (typeof config.rules === "object") {
         Object.keys(config.rules).forEach(k => {
             const rule = config.rules[k];
@@ -432,32 +429,30 @@ function prepareConfig(config, envContext) {
             if (rule === null) {
                 throw new Error(`Invalid config for rule '${k}'.`);
             }
-            if (Array.isArray(rule)) {
-                copiedRules[k] = rule.slice();
-            } else {
-                copiedRules[k] = rule;
-            }
         });
     }
 
     // merge in environment parserOptions
-    if (typeof config.env === "object") {
-        Object.keys(config.env).forEach(envName => {
+    const envNamesEnabledInConfig = typeof config.env === "object"
+        ? Object.keys(config.env).filter(envName => config.env[envName])
+        : [];
+
+    const parserOptionsFromEnv = envNamesEnabledInConfig
+        .reduce((parserOptions, envName) => {
             const env = envContext.get(envName);
 
-            if (config.env[envName] && env && env.parserOptions) {
-                parserOptions = ConfigOps.merge(parserOptions, env.parserOptions);
-            }
-        });
-    }
+            return env && env.parserOptions
+                ? ConfigOps.merge(parserOptions, env.parserOptions)
+                : parserOptions;
+        }, {});
 
     const preparedConfig = {
-        rules: copiedRules,
-        parser: config.parser || defaultConfig.parser,
-        globals: ConfigOps.merge(defaultConfig.globals, config.globals),
-        env: ConfigOps.merge(defaultConfig.env, config.env || {}),
-        settings: ConfigOps.merge(defaultConfig.settings, config.settings || {}),
-        parserOptions: ConfigOps.merge(parserOptions, config.parserOptions || {})
+        rules: config.rules || {},
+        parser: config.parser || DEFAULT_PARSER_NAME,
+        globals: config.globals || {},
+        env: config.env || {},
+        settings: config.settings || {},
+        parserOptions: ConfigOps.merge(parserOptionsFromEnv, config.parserOptions || {})
     };
     const isModule = preparedConfig.parserOptions.sourceType === "module";
 
@@ -810,7 +805,7 @@ module.exports = class Linter {
         }
 
         // process initial config to make it safe to extend
-        config = prepareConfig(config, this.environments);
+        config = populateConfig(config, this.environments);
 
         if (!lastSourceCodes.get(this)) {
 
diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js
index 35ac803b6..5a6eaa72f 100644
--- a/lib/rules/max-len.js
+++ b/lib/rules/max-len.js
@@ -213,7 +213,8 @@ module.exports = {
          * @returns {ASTNode[]} An array of string nodes.
          */
         function getAllStrings() {
-            return sourceCode.ast.tokens.filter(token => token.type === "String");
+            return sourceCode.ast.tokens.filter(token => (token.type === "String" ||
+                (token.type === "JSXText" && sourceCode.getNodeByRangeIndex(token.range[0] - 1).type === "JSXAttribute")));
         }
 
         /**
diff --git a/tools/internal-rules/no-useless-catch.js b/tools/internal-rules/no-useless-catch.js
new file mode 100644
index 000000000..86e0e2a70
--- /dev/null
+++ b/tools/internal-rules/no-useless-catch.js
@@ -0,0 +1,44 @@
+/**
+ * @fileoverview Reports useless `catch` clauses that just rethrow their error.
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow unnecessary `catch` clauses",
+            category: "Internal",
+            recommended: false
+        },
+
+        schema: []
+    },
+
+    create(context) {
+        return {
+            CatchClause(node) {
+                if (
+                    node.param.type === "Identifier" &&
+                    node.body.body.length &&
+                    node.body.body[0].type === "ThrowStatement" &&
+                    node.body.body[0].argument.type === "Identifier" &&
+                    node.body.body[0].argument.name === node.param.name
+                ) {
+                    if (node.parent.finalizer) {
+                        context.report({
+                            node,
+                            message: "Unnecessary catch clause."
+                        });
+                    } else {
+                        context.report({
+                            node,
+                            message: "Unnecessary try/catch wrapper."
+                        });
+                    }
+                }
+            }
+        };
+    }
+};


Test patch:
diff --git a/tests/fixtures/config-file/js/.eslintrc.parser3.js b/tests/fixtures/config-file/js/.eslintrc.parser3.js
index fd5f04ff1..9324e0065 100644
--- a/tests/fixtures/config-file/js/.eslintrc.parser3.js
+++ b/tests/fixtures/config-file/js/.eslintrc.parser3.js
@@ -1,7 +1,5 @@
-var defaultOptions = require("../../../../conf/default-config-options");
-
 module.exports = {
-    parser: defaultOptions.parser,
+    parser: "espree",
     rules: {
         semi: [2, "always"]
     }
diff --git a/tests/lib/rules/max-len.js b/tests/lib/rules/max-len.js
index 4180a8879..caa5f3d56 100644
--- a/tests/lib/rules/max-len.js
+++ b/tests/lib/rules/max-len.js
@@ -109,6 +109,11 @@ ruleTester.run("max-len", rule, {
             code: "var str = \"this is a very long string\\\nwith continuation\\\nand with another very very long continuation\\\nand ending\";",
             options: [29, 4, { ignoreStrings: true }]
         },
+        {
+            code: "var foo = <div className=\"this is a very long string\"></div>;",
+            options: [29, 4, { ignoreStrings: true }],
+            parserOptions: { ecmaFeatures: { jsx: true } }
+        },
         {
             code: "var foo = veryLongIdentifier;\nvar bar = `this is a very long string`;",
             options: [29, 4, { ignoreTemplateLiterals: true }],
@@ -553,6 +558,19 @@ ruleTester.run("max-len", rule, {
                 }
             ]
         },
+        {
+            code: "var foo = <div>this is a very very very long string</div>;",
+            options: [29, 4, { ignoreStrings: true }],
+            parserOptions: { ecmaFeatures: { jsx: true } },
+            errors: [
+                {
+                    message: "Line 1 exceeds the maximum line length of 29.",
+                    type: "Program",
+                    line: 1,
+                    column: 1
+                }
+            ]
+        },
 
         // Multi-code-point unicode glyphs
         {
diff --git a/tests/lib/testers/no-test-runners.js b/tests/lib/testers/no-test-runners.js
index b77c6bb90..5ca2de128 100644
--- a/tests/lib/testers/no-test-runners.js
+++ b/tests/lib/testers/no-test-runners.js
@@ -27,8 +27,6 @@ try {
             ]
         });
     }, /Output is incorrect\. \(' foo = bar;' == 'invalid output'\)$/);
-} catch (e) {
-    throw e;
 } finally {
     it = tmpIt;
     describe = tmpDescribe;
diff --git a/tests/tools/internal-rules/no-useless-catch.js b/tests/tools/internal-rules/no-useless-catch.js
new file mode 100644
index 000000000..6e1a65099
--- /dev/null
+++ b/tests/tools/internal-rules/no-useless-catch.js
@@ -0,0 +1,62 @@
+/**
+ * @fileoverview Tests for no-useless-throw rule
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../tools/internal-rules/no-useless-catch");
+const RuleTester = require("../../../lib/testers/rule-tester");
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+new RuleTester({ parserOptions: { ecmaVersion: 6 } }).run("rulesdir/no-useless-catch", rule, {
+    valid: [
+        `
+            try {
+                foo();
+            } catch (err) {
+                console.error(err);
+            } finally {
+                foo;
+            }
+        `,
+        `
+            try {
+                foo();
+            } catch ({ err }) {
+                throw err;
+            }
+        `
+    ],
+    invalid: [
+        {
+            code: `
+                try {
+                    foo();
+                } catch (e) {
+                    throw e;
+                }
+            `,
+            errors: [{ message: "Unnecessary try/catch wrapper." }]
+        },
+        {
+            code: `
+                try {
+                    foo();
+                } catch (e) {
+                    throw e;
+                } finally {
+                    foo();
+                }
+            `,
+            errors: [{ message: "Unnecessary catch clause." }]
+        }
+    ]
+});