diff --git a/lib/rules/semi-style.js b/lib/rules/semi-style.js index 97fcc3ac820..41fb39246c7 100644 --- a/lib/rules/semi-style.js +++ b/lib/rules/semi-style.js @@ -18,13 +18,51 @@ const astUtils = require("../ast-utils"); const SELECTOR = `:matches(${ [ "BreakStatement", "ContinueStatement", "DebuggerStatement", - "DoWhileStatement", "EmptyStatement", "ExportAllDeclaration", + "DoWhileStatement", "ExportAllDeclaration", "ExportDefaultDeclaration", "ExportNamedDeclaration", "ExpressionStatement", "ImportDeclaration", "ReturnStatement", "ThrowStatement", "VariableDeclaration" ].join(",") })`; +/** + * Get the child node list of a given node. + * This returns `Program#body`, `BlockStatement#body`, or `SwitchCase#consequent`. + * This is used to check whether a node is the first/last child. + * @param {Node} node A node to get child node list. + * @returns {Node[]|null} The child node list. + */ +function getChildren(node) { + const t = node.type; + + if (t === "BlockStatement" || t === "Program") { + return node.body; + } + if (t === "SwitchCase") { + return node.consequent; + } + return null; +} + +/** + * Check whether a given node is the last statement in the parent block. + * @param {Node} node A node to check. + * @returns {boolean} `true` if the node is the last statement in the parent block. + */ +function isLastChild(node) { + const t = node.parent.type; + + if (t === "IfStatement" && node.parent.consequent === node && node.parent.alternate) { // before `else` keyword. + return true; + } + if (t === "DoWhileStatement") { // before `while` keyword. + return true; + } + const nodeList = getChildren(node.parent); + + return nodeList !== null && nodeList[nodeList.length - 1] === node; // before `}` or etc. +} + module.exports = { meta: { docs: { @@ -40,23 +78,6 @@ module.exports = { const sourceCode = context.getSourceCode(); const option = context.options[0] || "last"; - /** - * Check whether comments exist between the given 2 tokens. - * @param {Token} left The left token to check. - * @param {Token} right The right token to check. - * @returns {boolean} `true` if comments exist between the given 2 tokens. - */ - function commentsExistBetween(left, right) { - return sourceCode.getFirstTokenBetween( - left, - right, - { - includeComments: true, - filter: astUtils.isCommentToken - } - ) !== null; - } - /** * Check the given semicolon token. * @param {Token} semiToken The semicolon token to check. @@ -79,7 +100,7 @@ module.exports = { : "the beginning of the next line" }, fix(fixer) { - if (prevToken && nextToken && commentsExistBetween(prevToken, nextToken)) { + if (prevToken && nextToken && sourceCode.commentsExistBetween(prevToken, nextToken)) { return null; } @@ -95,6 +116,10 @@ module.exports = { return { [SELECTOR](node) { + if (option === "first" && isLastChild(node)) { + return; + } + const lastToken = sourceCode.getLastToken(node); if (astUtils.isSemicolonToken(lastToken)) { diff --git a/tests/lib/rules/semi-style.js b/tests/lib/rules/semi-style.js index 43968facffb..8b0834af3d5 100644 --- a/tests/lib/rules/semi-style.js +++ b/tests/lib/rules/semi-style.js @@ -27,18 +27,103 @@ ruleTester.run("semi-style", rule, { "for(a;\nb;\nc);", "for((a\n);\n(b\n);\n(c));", "if(a)foo;\nbar", + { code: ";", options: ["last"] }, { code: ";foo;bar;baz;", options: ["last"] }, { code: "foo;\nbar;", options: ["last"] }, { code: "for(a;b;c);", options: ["last"] }, { code: "for(a;\nb;\nc);", options: ["last"] }, { code: "for((a\n);\n(b\n);\n(c));", options: ["last"] }, { code: "if(a)foo;\nbar", options: ["last"] }, + { code: ";", options: ["first"] }, { code: ";foo;bar;baz;", options: ["first"] }, { code: "foo\n;bar;", options: ["first"] }, { code: "for(a;b;c);", options: ["first"] }, { code: "for(a;\nb;\nc);", options: ["first"] }, { code: "for((a\n);\n(b\n);\n(c));", options: ["first"] }, - { code: "if(a)foo\n;bar", options: ["first"] } + + // edge cases + { + code: ` + { + ; + } + `, + options: ["first"] + }, + { + code: ` + while (a) + ; + foo + `, + options: ["first"] + }, + { + code: ` + do + ; + while (a) + `, + options: ["first"] + }, + { + code: ` + do + foo; + while (a) + `, + options: ["first"] + }, + { + code: ` + if (a) + foo; + else + bar + `, + options: ["first"] + }, + { + code: ` + if (a) + foo + ;bar + `, + options: ["first"] + }, + { + code: ` + { + ; + } + `, + options: ["last"] + }, + { + code: ` + switch (a) { + case 1: + ;foo + } + `, + options: ["last"] + }, + { + code: ` + while (a) + ; + foo + `, + options: ["last"] + }, + { + code: ` + do + ; + while (a) + `, + options: ["last"] + } ], invalid: [ {