Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: edge-cases of semi-style #9560

Merged
merged 2 commits into from Nov 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 44 additions & 19 deletions lib/rules/semi-style.js
Expand Up @@ -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: {
Expand All @@ -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.
Expand All @@ -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;
}

Expand All @@ -95,6 +116,10 @@ module.exports = {

return {
[SELECTOR](node) {
if (option === "first" && isLastChild(node)) {
return;
}

const lastToken = sourceCode.getLastToken(node);

if (astUtils.isSemicolonToken(lastToken)) {
Expand Down
87 changes: 86 additions & 1 deletion tests/lib/rules/semi-style.js
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please explain a little why this case is valid? IMHO, it's using options: ["last"], so it should be foo;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. This rule checks the location of the semicolon between two statements. I.e.:

first();
second()
// OR
first()
;second()

The rule does not add/remove semicolons because it's the responsibility of semi and no-extra-semi rules. Instead, the rule handles whitespaces of around semicolons.

Now, the current behavior is that the case 1:\n;foo is warned and fixed to case 1:;\nfoo, it was wrong.

}
`,
options: ["last"]
},
{
code: `
while (a)
;
foo
`,
options: ["last"]
},
{
code: `
do
;
while (a)
`,
options: ["last"]
}
],
invalid: [
{
Expand Down