Repository: eslint/eslint
Pull request #10007 (2018-02-23 20:26:50)

Chore: refactor directive comment processing
<!--
    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 makes a few improvements to directive comment processing in `linter.js`:

* Avoids internally adding some unrelated properties to config objects
* Fixes some code duplication
* Avoids some unnecessary branches

Note: it might be useful to view the diff with whitespace changes omitted ([link](https://github.com/eslint/eslint/pull/10007/files?w=1))

**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..889448ab0 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
@@ -179,12 +179,12 @@ function parseListConfig(string) {
  * scope.
  * @param {Scope} globalScope The global scope.
  * @param {Object} config The existing configuration data.
+ * @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration
  * @param {Environments} envContext Env context
  * @returns {void}
  */
-function addDeclaredGlobals(globalScope, config, envContext) {
+function addDeclaredGlobals(globalScope, config, commentDirectives, envContext) {
     const declaredGlobals = {},
-        exportedGlobals = {},
         explicitGlobals = {},
         builtin = envContext.get("builtin");
 
@@ -199,9 +199,8 @@ function addDeclaredGlobals(globalScope, config, envContext) {
         }
     });
 
-    Object.assign(exportedGlobals, config.exported);
     Object.assign(declaredGlobals, config.globals);
-    Object.assign(explicitGlobals, config.astGlobals);
+    Object.assign(explicitGlobals, commentDirectives.enabledGlobals);
 
     Object.keys(declaredGlobals).forEach(name => {
         let variable = globalScope.set.get(name);
@@ -229,7 +228,7 @@ function addDeclaredGlobals(globalScope, config, envContext) {
     });
 
     // mark all exported variables as such
-    Object.keys(exportedGlobals).forEach(name => {
+    Object.keys(commentDirectives.exportedVariables).forEach(name => {
         const variable = globalScope.set.get(name);
 
         if (variable) {
@@ -283,108 +282,90 @@ function createDisableDirectives(type, loc, value) {
  * where reporting is disabled or enabled and merges them with reporting config.
  * @param {string} filename The file being checked.
  * @param {ASTNode} ast The top node of the AST.
- * @param {Object} config The existing configuration data.
  * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
- * @returns {{config: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
- * Modified config object, along with any problems encountered while parsing config comments
+ * @returns {{configuredRules: Object, enabledGlobals: Object, exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
+ * A collection of the directive comments that were found, along with any problems that occurred when parsing
  */
-function modifyConfigsFromComments(filename, ast, config, ruleMapper) {
-
-    const commentConfig = {
-        exported: {},
-        astGlobals: {},
-        rules: {},
-        env: {}
-    };
-    const commentRules = {};
+function getDirectiveComments(filename, ast, ruleMapper) {
+    const configuredRules = {};
+    const enabledGlobals = {};
+    const exportedVariables = {};
     const problems = [];
     const disableDirectives = [];
 
     ast.comments.filter(token => token.type !== "Shebang").forEach(comment => {
+        const trimmedCommentText = comment.value.trim();
+        const match = /^(eslint(-\w+){0,3}|exported|globals?)(\s|$)/.exec(trimmedCommentText);
 
-        let value = comment.value.trim();
-        const match = /^(eslint(-\w+){0,3}|exported|globals?)(\s|$)/.exec(value);
-
-        if (match) {
-            value = value.slice(match.index + match[1].length);
-            if (comment.type === "Block") {
-                switch (match[1]) {
-                    case "exported":
-                        Object.assign(commentConfig.exported, parseBooleanConfig(value, comment));
-                        break;
-
-                    case "globals":
-                    case "global":
-                        Object.assign(commentConfig.astGlobals, parseBooleanConfig(value, comment));
-                        break;
-
-                    case "eslint-disable":
-                        [].push.apply(disableDirectives, createDisableDirectives("disable", comment.loc.start, value));
-                        break;
-
-                    case "eslint-disable-line":
-                        if (comment.loc.start.line === comment.loc.end.line) {
-                            [].push.apply(disableDirectives, createDisableDirectives("disable-line", comment.loc.start, value));
-                        }
-                        break;
-
-                    case "eslint-disable-next-line":
-                        if (comment.loc.start.line === comment.loc.end.line) {
-                            [].push.apply(disableDirectives, createDisableDirectives("disable-next-line", comment.loc.start, value));
-                        }
-                        break;
-
-                    case "eslint-enable":
-                        [].push.apply(disableDirectives, createDisableDirectives("enable", comment.loc.start, value));
-                        break;
-
-                    case "eslint": {
-                        const parseResult = parseJsonConfig(value, comment.loc);
-
-                        if (parseResult.success) {
-                            Object.keys(parseResult.config).forEach(name => {
-                                const ruleValue = parseResult.config[name];
-
-                                try {
-                                    validator.validateRuleOptions(ruleMapper(name), name, ruleValue);
-                                } catch (err) {
-                                    problems.push({
-                                        ruleId: name,
-                                        severity: 2,
-                                        source: null,
-                                        message: err.message,
-                                        line: comment.loc.start.line,
-                                        column: comment.loc.start.column + 1,
-                                        endLine: comment.loc.end.line,
-                                        endColumn: comment.loc.end.column + 1,
-                                        nodeType: null
-                                    });
-                                }
-                                commentRules[name] = ruleValue;
-                            });
-                        } else {
-                            problems.push(parseResult.error);
-                        }
+        if (!match) {
+            return;
+        }
 
-                        break;
+        const directiveValue = trimmedCommentText.slice(match.index + match[1].length);
+
+        if (/^eslint-disable-(next-)?line$/.test(match[1]) && comment.loc.start.line === comment.loc.end.line) {
+            const directiveType = match[1].slice("eslint-".length);
+
+            [].push.apply(disableDirectives, createDisableDirectives(directiveType, comment.loc.start, directiveValue));
+        } else if (comment.type === "Block") {
+            switch (match[1]) {
+                case "exported":
+                    Object.assign(exportedVariables, parseBooleanConfig(directiveValue, comment));
+                    break;
+
+                case "globals":
+                case "global":
+                    Object.assign(enabledGlobals, parseBooleanConfig(directiveValue, comment));
+                    break;
+
+                case "eslint-disable":
+                    [].push.apply(disableDirectives, createDisableDirectives("disable", comment.loc.start, directiveValue));
+                    break;
+
+                case "eslint-enable":
+                    [].push.apply(disableDirectives, createDisableDirectives("enable", comment.loc.start, directiveValue));
+                    break;
+
+                case "eslint": {
+                    const parseResult = parseJsonConfig(directiveValue, comment.loc);
+
+                    if (parseResult.success) {
+                        Object.keys(parseResult.config).forEach(name => {
+                            const ruleValue = parseResult.config[name];
+
+                            try {
+                                validator.validateRuleOptions(ruleMapper(name), name, ruleValue);
+                            } catch (err) {
+                                problems.push({
+                                    ruleId: name,
+                                    severity: 2,
+                                    source: null,
+                                    message: err.message,
+                                    line: comment.loc.start.line,
+                                    column: comment.loc.start.column + 1,
+                                    endLine: comment.loc.end.line,
+                                    endColumn: comment.loc.end.column + 1,
+                                    nodeType: null
+                                });
+                            }
+                            configuredRules[name] = ruleValue;
+                        });
+                    } else {
+                        problems.push(parseResult.error);
                     }
 
-                    // no default
-                }
-            } else { // comment.type === "Line"
-                if (match[1] === "eslint-disable-line") {
-                    [].push.apply(disableDirectives, createDisableDirectives("disable-line", comment.loc.start, value));
-                } else if (match[1] === "eslint-disable-next-line") {
-                    [].push.apply(disableDirectives, createDisableDirectives("disable-next-line", comment.loc.start, value));
+                    break;
                 }
+
+                // no default
             }
         }
     });
 
-    Object.assign(commentConfig.rules, commentRules);
-
     return {
-        config: ConfigOps.merge(config, commentConfig),
+        configuredRules,
+        enabledGlobals,
+        exportedVariables,
         problems,
         disableDirectives
     };
@@ -415,16 +396,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 +410,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";
 
@@ -780,10 +756,11 @@ module.exports = class Linter {
         // evaluate arguments
         if (typeof filenameOrOptions === "object") {
             providedFilename = filenameOrOptions.filename;
-            allowInlineConfig = filenameOrOptions.allowInlineConfig;
+            allowInlineConfig = filenameOrOptions.allowInlineConfig !== false;
             reportUnusedDisableDirectives = filenameOrOptions.reportUnusedDisableDirectives;
         } else {
             providedFilename = filenameOrOptions;
+            allowInlineConfig = true;
         }
 
         if (typeof textOrSourceCode === "string") {
@@ -810,7 +787,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)) {
 
@@ -866,24 +843,23 @@ module.exports = class Linter {
             }
         }
 
-        const problems = [];
         const sourceCode = lastSourceCodes.get(this);
-        let disableDirectives;
+        const commentDirectives = allowInlineConfig
+            ? getDirectiveComments(filename, sourceCode.ast, ruleId => this.rules.get(ruleId))
+            : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
 
-        // parse global comments and modify config
-        if (allowInlineConfig !== false) {
-            const modifyConfigResult = modifyConfigsFromComments(filename, sourceCode.ast, config, ruleId => this.rules.get(ruleId));
+        const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
 
-            config = modifyConfigResult.config;
-            modifyConfigResult.problems.forEach(problem => problems.push(problem));
-            disableDirectives = modifyConfigResult.disableDirectives;
-        } else {
-            disableDirectives = [];
-        }
+        // augment global scope with declared global variables
+        addDeclaredGlobals(
+            sourceCode.scopeManager.scopes[0],
+            config,
+            { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals },
+            this.environments
+        );
 
         const emitter = createEmitter();
         const traverser = new Traverser();
-        const scopeManager = sourceCode.scopeManager;
 
         /*
          * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
@@ -895,11 +871,11 @@ module.exports = class Linter {
                 Object.create(BASE_TRAVERSAL_CONTEXT),
                 {
                     getAncestors: () => traverser.parents(),
-                    getDeclaredVariables: scopeManager.getDeclaredVariables.bind(scopeManager),
+                    getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
                     getFilename: () => filename,
-                    getScope: () => getScope(scopeManager, traverser.current(), config.parserOptions.ecmaVersion),
+                    getScope: () => getScope(sourceCode.scopeManager, traverser.current(), config.parserOptions.ecmaVersion),
                     getSourceCode: () => sourceCode,
-                    markVariableAsUsed: name => markVariableAsUsed(scopeManager, traverser.current(), config.parserOptions, name),
+                    markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, traverser.current(), config.parserOptions, name),
                     parserOptions: config.parserOptions,
                     parserPath: config.parser,
                     parserServices: sourceCode.parserServices,
@@ -921,9 +897,11 @@ module.exports = class Linter {
             )
         );
 
+        const lintingProblems = [];
+
         // enable appropriate rules
-        Object.keys(config.rules).forEach(ruleId => {
-            const severity = ConfigOps.getRuleSeverity(config.rules[ruleId]);
+        Object.keys(configuredRules).forEach(ruleId => {
+            const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
 
             if (severity === 0) {
                 return;
@@ -937,7 +915,7 @@ module.exports = class Linter {
                     Object.create(sharedTraversalContext),
                     {
                         id: ruleId,
-                        options: getRuleOptions(config.rules[ruleId]),
+                        options: getRuleOptions(configuredRules[ruleId]),
                         report() {
 
                             /*
@@ -958,7 +936,7 @@ module.exports = class Linter {
                             if (problem.fix && rule.meta && !rule.meta.fixable) {
                                 throw new Error("Fixable rules should export a `meta.fixable` property.");
                             }
-                            problems.push(problem);
+                            lintingProblems.push(problem);
 
                             /*
                              * This is used to avoid breaking rules that used monkeypatch Linter, and relied on
@@ -1000,9 +978,6 @@ module.exports = class Linter {
             }
         });
 
-        // augment global scope with declared global variables
-        addDeclaredGlobals(scopeManager.scopes[0], config, this.environments);
-
         const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
 
         /*
@@ -1023,8 +998,10 @@ module.exports = class Linter {
         });
 
         return applyDisableDirectives({
-            directives: disableDirectives,
-            problems: problems.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
+            directives: commentDirectives.disableDirectives,
+            problems: lintingProblems
+                .concat(commentDirectives.problems)
+                .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
             reportUnusedDisableDirectives
         });
     }
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." }]
+        }
+    ]
+});