diff --git a/lib/rules/no-unreachable.js b/lib/rules/no-unreachable.js index c28a6d2f2fa..2622d223987 100644 --- a/lib/rules/no-unreachable.js +++ b/lib/rules/no-unreachable.js @@ -26,6 +26,75 @@ function isUnreachable(segment) { return !segment.reachable; } +/** + * The class to distinguish consecutive unreachable statements. + */ +class ConsecutiveRange { + constructor(sourceCode) { + this.sourceCode = sourceCode; + this.startNode = null; + this.endNode = null; + } + + /** + * The location object of this range. + * @type {Object} + */ + get location() { + return { + start: this.startNode.loc.start, + end: this.endNode.loc.end + }; + } + + /** + * `true` if this range is empty. + * @type {boolean} + */ + get isEmpty() { + return !(this.startNode && this.endNode); + } + + /** + * Checks whether the given node is inside of this range. + * @param {ASTNode|Token} node - The node to check. + * @returns {boolean} `true` if the node is inside of this range. + */ + contains(node) { + return ( + node.range[0] >= this.startNode.range[0] && + node.range[1] <= this.endNode.range[1] + ); + } + + /** + * Checks whether the given node is consecutive to this range. + * @param {ASTNode} node - The node to check. + * @returns {boolean} `true` if the node is consecutive to this range. + */ + isConsecutive(node) { + return this.contains(this.sourceCode.getTokenBefore(node)); + } + + /** + * Merges the given node to this range. + * @param {ASTNode} node - The node to merge. + * @returns {void} + */ + merge(node) { + this.endNode = node; + } + + /** + * Resets this range by the given node or null. + * @param {ASTNode|null} node - The node to reset, or null. + * @returns {void} + */ + reset(node) { + this.startNode = this.endNode = node; + } +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -44,15 +113,50 @@ module.exports = { create: function(context) { var currentCodePath = null; + const range = new ConsecutiveRange(context.getSourceCode()); + /** * Reports a given node if it's unreachable. * @param {ASTNode} node - A statement node to report. * @returns {void} */ function reportIfUnreachable(node) { - if (currentCodePath.currentSegments.every(isUnreachable)) { - context.report({message: "Unreachable code.", node: node}); + let nextNode = null; + + if (node && currentCodePath.currentSegments.every(isUnreachable)) { + + // Store this statement to distinguish consecutive statements. + if (range.isEmpty) { + range.reset(node); + return; + } + + // Skip if this statement is inside of the current range. + if (range.contains(node)) { + return; + } + + // Merge if this statement is consecutive to the current range. + if (range.isConsecutive(node)) { + range.merge(node); + return; + } + + nextNode = node; + } + + // Report the current range since this statement is reachable or is + // not consecutive to the current range. + if (!range.isEmpty) { + context.report({ + message: "Unreachable code.", + loc: range.location, + node: range.startNode + }); } + + // Update the current range. + range.reset(nextNode); } return { @@ -96,7 +200,11 @@ module.exports = { WithStatement: reportIfUnreachable, ExportNamedDeclaration: reportIfUnreachable, ExportDefaultDeclaration: reportIfUnreachable, - ExportAllDeclaration: reportIfUnreachable + ExportAllDeclaration: reportIfUnreachable, + + "Program:exit": function() { + reportIfUnreachable(); + } }; } }; diff --git a/tests/lib/rules/no-unreachable.js b/tests/lib/rules/no-unreachable.js index 308b052202c..57b5079e7ff 100644 --- a/tests/lib/rules/no-unreachable.js +++ b/tests/lib/rules/no-unreachable.js @@ -62,6 +62,129 @@ ruleTester.run("no-unreachable", rule, { { code: "function foo() { var x = 1; do { return; } while (x); x = 2; }", errors: [{ message: "Unreachable code.", type: "ExpressionStatement"}] }, { code: "function foo() { var x = 1; while (x) { if (x) break; else continue; x = 2; } }", errors: [{ message: "Unreachable code.", type: "ExpressionStatement"}] }, { code: "function foo() { var x = 1; for (;;) { if (x) continue; } x = 2; }", errors: [{ message: "Unreachable code.", type: "ExpressionStatement"}] }, - { code: "function foo() { var x = 1; while (true) { } x = 2; }", errors: [{ message: "Unreachable code.", type: "ExpressionStatement"}] } + { code: "function foo() { var x = 1; while (true) { } x = 2; }", errors: [{ message: "Unreachable code.", type: "ExpressionStatement"}] }, + + // Merge the warnings of continuous unreachable nodes. + { + code: ` + function foo() { + return; + + a(); // ← ERROR: Unreachable code. (no-unreachable) + + b() // ↑ ';' token is included in the unreachable code, so this statement will be merged. + // comment + c(); // ↑ ')' token is included in the unreachable code, so this statement will be merged. + } + `, + errors: [ + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 5, + column: 21, + endLine: 9, + endColumn: 25 + } + ] + }, + { + code: ` + function foo() { + return; + + a(); + + if (b()) { + c() + } else { + d() + } + } + `, + errors: [ + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 5, + column: 21, + endLine: 11, + endColumn: 22 + } + ] + }, + { + code: ` + function foo() { + if (a) { + return + b(); + c(); + } else { + throw err + d(); + } + } + `, + errors: [ + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 5, + column: 25, + endLine: 6, + endColumn: 29 + }, + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 9, + column: 25, + endLine: 9, + endColumn: 29 + } + ] + }, + { + code: ` + function foo() { + if (a) { + return + b(); + c(); + } else { + throw err + d(); + } + e(); + } + `, + errors: [ + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 5, + column: 25, + endLine: 6, + endColumn: 29 + }, + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 9, + column: 25, + endLine: 9, + endColumn: 29 + }, + { + message: "Unreachable code.", + type: "ExpressionStatement", + line: 11, + column: 21, + endLine: 11, + endColumn: 25 + } + ] + } ] });