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

#265 Warn on redundant semicolon after else #290

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/errors/E204.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# E204: semicolon after else may be causing unexpected behavior.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Someone else took error code E204. Choose a different error code.


A semicolon next to the `else` keyword may cause its body to be executed even when previous `if` statement condition is true.

if (true) {
console.log("true");
} else; {
console.log("else");
}

Example output
```
> true
> else
```

To avoid this behavior, remove the semicolon between the else keyword and the opening brace of the else block.

if (true) {
console.log("true");
} else {
console.log("else");
}

Or if the else body is not required remove it completely.

if (true) {
console.log("true");
}
console.log("always");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lovely docs!

13 changes: 13 additions & 0 deletions src/quick-lint-js/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@
QLJS_TRANSLATABLE("commas are not allowed after spread parameter"), \
comma)) \
\
QLJS_ERROR_TYPE( \
error_else_has_empty_body, "E205", { source_code_span where; }, \
.warning(QLJS_TRANSLATABLE("else has empty body; consider removing the " \
"redundant 'else'"), \
where)) \
\
QLJS_ERROR_TYPE( \
error_else_has_no_if, "E065", { source_code_span else_token; }, \
.error(QLJS_TRANSLATABLE("'else' has no corresponding 'if'"), \
Expand Down Expand Up @@ -1048,6 +1054,13 @@
QLJS_TRANSLATABLE("for-of loop expression cannot have semicolons"), \
semicolon)) \
\
QLJS_ERROR_TYPE( \
error_unexpected_semicolon_after_else, "E204", \
{ source_code_span semicolon; }, \
.warning(QLJS_TRANSLATABLE("semicolon after else may be causing " \
"unexpected behavior"), \
semicolon)) \
\
QLJS_ERROR_TYPE( \
error_no_digits_in_binary_number, "E049", \
{ source_code_span characters; }, \
Expand Down
13 changes: 13 additions & 0 deletions src/quick-lint-js/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -2516,7 +2516,20 @@ class parser {

if (this->peek().type == token_type::kw_else) {
this->skip();

const token token_after_else = this->peek();
parse_and_visit_body();

if (token_after_else.type == token_type::semicolon &&
this->peek().type == token_type::left_curly) {
if (this->peek().has_leading_newline) {
this->error_reporter_->report(
error_else_has_empty_body{.where = this->peek().span()});
} else {
this->error_reporter_->report(error_unexpected_semicolon_after_else{
.semicolon = token_after_else.span()});
}
}
}
}

Expand Down
38 changes: 38 additions & 0 deletions test/test-parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,44 @@ TEST(test_parse, else_without_if) {
}
}

TEST(test_parse, else_unexpected_semicolon) {
{
spy_visitor v;
padded_string code(u8"if (cond) { body; } else; { body; }"_sv);
parser p(&code, &v);
EXPECT_TRUE(p.parse_and_visit_statement(v));
EXPECT_THAT(v.visits, ElementsAre("visit_variable_use", // cond
"visit_enter_block_scope", // (if)
"visit_variable_use", // body
"visit_exit_block_scope")); // (if)

EXPECT_THAT(v.errors,
ElementsAre(ERROR_TYPE_FIELD(
error_unexpected_semicolon_after_else, semicolon,
offsets_matcher(&code, strlen(u8"if (cond) { body; } else"),
u8";"))));
}
}

TEST(test_parse, else_has_empty_body) {
{
spy_visitor v;
padded_string code(u8"if (cond) { body; } else;\n{ }"_sv);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: I don't think it's worth having a diagnostic for this code. If there's a newline after the else;, I think the empty body is intentional.

parser p(&code, &v);
EXPECT_TRUE(p.parse_and_visit_statement(v));
EXPECT_THAT(v.visits, ElementsAre("visit_variable_use", // cond
"visit_enter_block_scope", // (if)
"visit_variable_use", // body
"visit_exit_block_scope")); // (if)
EXPECT_THAT(
v.errors,
ElementsAre(ERROR_TYPE_FIELD(
error_else_has_empty_body, where,
offsets_matcher(&code, strlen(u8"if (cond) { body; } else {"),
u8"}"))));
Comment on lines +614 to +615
Copy link
Collaborator

Choose a reason for hiding this comment

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

Blocking: This strlen string doesn't match the code. The u8"}" looks wrong too. Did you mean the following instead?

Suggested change
offsets_matcher(&code, strlen(u8"if (cond) { body; } else {"),
u8"}"))));
offsets_matcher(&code, strlen(u8"if (cond) { body; } else;\n"),
u8"{"))));

If so, the error looks misplaced. It should point at the else token or the ;, not at the following unrelated {.

Copy link
Author

Choose a reason for hiding this comment

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

Right now it detects else;{} (semicolon after else) and else;\n{} (empty body), the following in parse.h:2534 should change to only trigger else has empty body when it is not preceded by a newline:

-   if (token_after_else.type == token_type::semicolon &&
-          this->peek().type == token_type::left_curly) {
+   if (token_after_else.type == token_type::semicolon &&
+          this->peek().type == token_type::left_curly &&
+          !this->peek().has_leading_newline) {

-     if (this->peek().has_leading_newline) {
+     if (body is empty) {
          this->error_reporter_->report(
              error_else_has_empty_body{.where = this->peek().span()});

       } else {
          this->error_reporter_->report(error_unexpected_semicolon_after_else{
              .semicolon = token_after_else.span()});
        }
    }

Should I omit the else has empty body warning altogether, or should I detect the empty body?
I do not know how to do it without making use of a transaction, advancing the peek and then doing a rollback afterwards.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should I omit the else has empty body warning altogether

Yeah, let's only emit error_unexpected_semicolon_after_else in this PR.

I do not know how to do it without making use of a transaction, advancing the peek and then doing a rollback afterwards.

Would something like this work? Or am I missing an edge case?

      if (token_after_else.type == token_type::semicolon &&
          this->peek().type == token_type::left_curly &&
          !this->peek().has_leading_newline) {
        this->error_reporter_->report(error_unexpected_semicolon_after_else{
          .semicolon = token_after_else.span(),
        });
      }

}
}
someoneigna marked this conversation as resolved.
Show resolved Hide resolved

TEST(test_parse, block_statement) {
{
spy_visitor v = parse_and_visit_statement(u8"{ }"_sv);
Expand Down