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: Check division operator in astUtils.canTokensBeAdjacent #12879

Merged
merged 3 commits into from Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 4 additions & 4 deletions lib/rules/no-extra-parens.js
Expand Up @@ -307,13 +307,13 @@ module.exports = {
*/
function requiresLeadingSpace(node) {
const leftParenToken = sourceCode.getTokenBefore(node);
const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1);
const firstToken = sourceCode.getFirstToken(node);
const tokenBeforeLeftParen = sourceCode.getTokenBefore(leftParenToken, { includeComments: true });
const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParenToken, { includeComments: true });

return tokenBeforeLeftParen &&
tokenBeforeLeftParen.range[1] === leftParenToken.range[0] &&
leftParenToken.range[1] === firstToken.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, firstToken);
leftParenToken.range[1] === tokenAfterLeftParen.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, tokenAfterLeftParen);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions lib/rules/operator-assignment.js
Expand Up @@ -214,12 +214,12 @@ module.exports = {
) {
rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`;
} else {
const firstRightToken = sourceCode.getFirstToken(node.right);
const tokenAfterOperator = sourceCode.getTokenAfter(operatorToken, { includeComments: true });
let rightTextPrefix = "";

if (
operatorToken.range[1] === firstRightToken.range[0] &&
!astUtils.canTokensBeAdjacent({ type: "Punctuator", value: newOperator }, firstRightToken)
operatorToken.range[1] === tokenAfterOperator.range[0] &&
!astUtils.canTokensBeAdjacent({ type: "Punctuator", value: newOperator }, tokenAfterOperator)
) {
rightTextPrefix = " "; // foo+=+bar -> foo= foo+ +bar
}
Expand Down
39 changes: 36 additions & 3 deletions lib/rules/utils/ast-utils.js
Expand Up @@ -1357,17 +1357,43 @@ module.exports = {
* next to each other, behavior is undefined (although it should return `true` in most cases).
*/
canTokensBeAdjacent(leftValue, rightValue) {
const espreeOptions = { ecmaVersion: 2020, comment: true, range: true };
Copy link
Member

Choose a reason for hiding this comment

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

Non-blocker for this PR, but it could be nice to add the ability to specify “latest” somehow, so that this never gets out of date.

Copy link
Member Author

Choose a reason for hiding this comment

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

That would be great! But, I'm not sure if there is a way to do that at the moment? Does espree support something like "latest" or provides a list of available versions?

Copy link
Member

Choose a reason for hiding this comment

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

v/_ \ I don't think espree has such a thing.

Copy link
Member

Choose a reason for hiding this comment

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

It doesn’t, no! Definitely not a blocker for this - just an idea I had and wanted to write down for posterity 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

A list of all versions could be also useful for the online demo, it's hard-coded there and has to be updated manually.

Copy link
Member

Choose a reason for hiding this comment

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

PR/proposal here: eslint/espree#430


let leftToken;

if (typeof leftValue === "string") {
const leftTokens = espree.tokenize(leftValue, { ecmaVersion: 2015 });
const tokens = espree.tokenize(leftValue, espreeOptions);
Copy link
Member

@kaicataldo kaicataldo Feb 6, 2020

Choose a reason for hiding this comment

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

Do you mind adding a test for when a Shebang comment token is given as the left value? I don't think this could ever be a real world use case (since another token can't be on the same line as the shebang), but I do think we should make sure that this won't crash the process when Espree tries to tokenize this.

For reference, we mutate the text input to make the shebang a regular JS line comment before we parse in lib/linter/linter.js and then transform the token post-parse to a Shebang comment token.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right, that would crash! Maybe we could already add try-catch as suggested in #12291 (comment) and also close #12291 with this PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

canTokensBeAdjacent also didn't assume it could get a code (as a string argument) without any tokens, so it doesn't support a comment-only code. This isn't changed with this PR, it would still crash, so it isn't possible to call it with e.g.,:

astUtils.canTokensBeAdjacent(someToken, "/* comment */");

Then, it might also make sense to support empty code as well, and maybe even to account for spaces at the start/end of the code (e.g., canTokensBeAdjacent("a ", "b") would return true).

Maybe we could do that as well in this PR?

Copy link
Member

@kaicataldo kaicataldo Feb 7, 2020

Choose a reason for hiding this comment

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

That sounds good to me! I guess another option (or something we could do additionally) is to short circuit it and add an early return of false if the left value token is of type Shebang. That would save the process from having to run the tokenizing at all.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, interesting! Since this is already an improvement over the existing code, I would be fine with splitting these bug fixes over multiple PRs to keep them manageable. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

That sounds good to me! I guess another option (or something we could do additionally) is to short circuit it and add an early return of false if the left value token is of type Shebang. That would save the process from having to run the tokenizing at all.

Maybe I didn't understand, but I think it isn't possible to check if the token is Shebang before tokenizing, because we don't have the token yet?

Copy link
Member

@kaicataldo kaicataldo Feb 7, 2020

Choose a reason for hiding this comment

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

Oh, I see, I missed that it could be a string or a token. Yes, if the left value is a string that won't work. I think the approach you suggested sounds good 👍

const comments = tokens.comments;

leftToken = tokens[tokens.length - 1];
if (comments.length) {
const lastComment = comments[comments.length - 1];

leftToken = leftTokens[leftTokens.length - 1];
if (lastComment.range[0] > leftToken.range[0]) {
leftToken = lastComment;
}
}
} else {
leftToken = leftValue;
}

const rightToken = typeof rightValue === "string" ? espree.tokenize(rightValue, { ecmaVersion: 2015 })[0] : rightValue;
let rightToken;

if (typeof rightValue === "string") {
const tokens = espree.tokenize(rightValue, espreeOptions);
const comments = tokens.comments;

rightToken = tokens[0];
if (comments.length) {
const firstComment = comments[0];

if (firstComment.range[0] < rightToken.range[0]) {
rightToken = firstComment;
}
}
} else {
rightToken = rightValue;
}

if (leftToken.type === "Punctuator" || rightToken.type === "Punctuator") {
if (leftToken.type === "Punctuator" && rightToken.type === "Punctuator") {
Expand All @@ -1379,6 +1405,9 @@ module.exports = {
MINUS_TOKENS.has(leftToken.value) && MINUS_TOKENS.has(rightToken.value)
);
}
if (leftToken.type === "Punctuator" && leftToken.value === "/") {
return !["Block", "Line", "RegularExpression"].includes(rightToken.type);
}
return true;
}

Expand All @@ -1393,6 +1422,10 @@ module.exports = {
return true;
}

if (leftToken.type === "Block" || rightToken.type === "Block" || rightToken.type === "Line") {
return true;
}

return false;
},

Expand Down
16 changes: 15 additions & 1 deletion tests/lib/rules/no-extra-parens.js
Expand Up @@ -2192,6 +2192,20 @@ ruleTester.run("no-extra-parens", rule, {
code: "var foo = { [((bar1, bar2))]: baz };",
output: "var foo = { [(bar1, bar2)]: baz };",
errors: [{ messageId: "unexpected" }]
}
},

// adjacent tokens tests for division operator, comments and regular expressions
invalid("a+/**/(/**/b)", "a+/**//**/b", "Identifier"),
invalid("a+/**/(//\nb)", "a+/**///\nb", "Identifier"),
invalid("a in(/**/b)", "a in/**/b", "Identifier"),
invalid("a in(//\nb)", "a in//\nb", "Identifier"),
invalid("a+(/**/b)", "a+/**/b", "Identifier"),
invalid("a+/**/(b)", "a+/**/b", "Identifier"),
invalid("a+(//\nb)", "a+//\nb", "Identifier"),
invalid("a+//\n(b)", "a+//\nb", "Identifier"),
invalid("a+(/^b$/)", "a+/^b$/", "Literal"),
invalid("a/(/**/b)", "a/ /**/b", "Identifier"),
invalid("a/(//\nb)", "a/ //\nb", "Identifier"),
invalid("a/(/^b$/)", "a/ /^b$/", "Literal")
]
});
15 changes: 15 additions & 0 deletions tests/lib/rules/operator-assignment.js
Expand Up @@ -363,6 +363,21 @@ ruleTester.run("operator-assignment", rule, {
output: "foo= foo/bar", // tokens can be adjacent
options: ["never"],
errors: UNEXPECTED_OPERATOR_ASSIGNMENT
}, {
code: "foo/=/**/bar",
output: "foo= foo/ /**/bar", // // tokens cannot be adjacent, insert a space between
options: ["never"],
errors: UNEXPECTED_OPERATOR_ASSIGNMENT
}, {
code: "foo/=//\nbar",
output: "foo= foo/ //\nbar", // // tokens cannot be adjacent, insert a space between
options: ["never"],
errors: UNEXPECTED_OPERATOR_ASSIGNMENT
}, {
code: "foo/=/^bar$/",
output: "foo= foo/ /^bar$/", // // tokens cannot be adjacent, insert a space between
options: ["never"],
errors: UNEXPECTED_OPERATOR_ASSIGNMENT
}, {
code: "foo+=+bar",
output: "foo= foo+ +bar", // tokens cannot be adjacent, insert a space between
Expand Down
16 changes: 15 additions & 1 deletion tests/lib/rules/utils/ast-utils.js
Expand Up @@ -1342,7 +1342,21 @@ describe("ast-utils", () => {
[["++", "+"], false],
[["--", "-"], false],
[["+", "++"], false],
[["-", "--"], false]
[["-", "--"], false],
[["a/", "b"], true],
[["a/", "+b"], true],
[["a+", "/^regex$/"], true],
[["a/", "/^regex$/"], false],
[["a+", "/**/b"], true],
[["a/", "/**/b"], false],
[["a+", "//\nb"], true],
[["a/", "//\nb"], false],
[["a/**/", "b"], true],
[["/**/a", "b"], false],
[["a", "/**/b"], true],
[["a", "b/**/"], false],
[["a", "//\nb"], true],
[["a", "b//"], false]
]);

CASES.forEach((expectedResult, tokenStrings) => {
Expand Down