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

Feature/warn missing fallthrough #1094

Closed
Closed
Show file tree
Hide file tree
Changes from 6 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
29 changes: 29 additions & 0 deletions docs/errors/E0423.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# E0423: missing fallthrough comment in switch case
Copy link
Collaborator

Choose a reason for hiding this comment

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

"missing fallthrough comment" is good, but if the programmer intended to write a 'break', this diagnostic message is misleading.

Maybe this is a better message:

missing 'break;' or '// fallthrough' comment between statement and 'case'

For reference, here is the message from ESLint's no-fallthrough rule:

Expected a 'break' statement before 'case'.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good.
Switched to missing 'break;' or '// fallthrough' comment between statement and 'case'
FYI: Also had to switch diag code to E0427 since E0423 was used recently.


Switch Cases in javascript fallthrough to the next case if the `break` statement is not added at the end of the case.
Since there is no explicit way of communication whether the fallthrough is intentional or not, it is recommended to use a comment indicating fallthrough.

```javascript
function test (c) {
switch (c) {
case 1:
foo();
default:
bar();
}
}
```

To fix this error, place a comment at the end of `case 1` indicating fallthrough

```javascript
function test (c) {
switch (c) {
case 1:
foo();
//fallthrough
default:
bar();
}
}
```
4 changes: 4 additions & 0 deletions po/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@ msgstr ""
msgid "this case will run instead"
msgstr ""

#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
msgid "missing fallthrough comment"
msgstr ""

#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
msgid "'else' has no corresponding 'if'"
msgstr ""
Expand Down
14 changes: 14 additions & 0 deletions src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = {
},
},

// Diag_Explicit_Fallthrough_Comment_In_Switch
{
.code = 423,
.severity = Diagnostic_Severity::warning,
.message_formats = {
QLJS_TRANSLATABLE("missing fallthrough comment"),
},
.message_args = {
{
Diagnostic_Message_Arg_Info(offsetof(Diag_Explicit_Fallthrough_Comment_In_Switch, end_of_case), Diagnostic_Arg_Type::source_code_span),
},
},
},

// Diag_Else_Has_No_If
{
.code = 65,
Expand Down
3 changes: 2 additions & 1 deletion src/quick-lint-js/diag/diagnostic-metadata-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ namespace quick_lint_js {
QLJS_DIAG_TYPE_NAME(Diag_Dot_Not_Allowed_After_Generic_Arguments_In_Type) \
QLJS_DIAG_TYPE_NAME(Diag_Dot_Dot_Is_Not_An_Operator) \
QLJS_DIAG_TYPE_NAME(Diag_Duplicated_Cases_In_Switch_Statement) \
QLJS_DIAG_TYPE_NAME(Diag_Explicit_Fallthrough_Comment_In_Switch) \
QLJS_DIAG_TYPE_NAME(Diag_Else_Has_No_If) \
QLJS_DIAG_TYPE_NAME(Diag_Equals_Does_Not_Distribute_Over_Or) \
QLJS_DIAG_TYPE_NAME(Diag_Escaped_Character_Disallowed_In_Identifiers) \
Expand Down Expand Up @@ -442,7 +443,7 @@ namespace quick_lint_js {
/* END */
// clang-format on

inline constexpr int Diag_Type_Count = 428;
inline constexpr int Diag_Type_Count = 429;

extern const Diagnostic_Info all_diagnostic_infos[Diag_Type_Count];
}
Expand Down
7 changes: 7 additions & 0 deletions src/quick-lint-js/diag/diagnostic-types-2.h
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,13 @@ struct Diag_Duplicated_Cases_In_Switch_Statement {
Source_Code_Span duplicated_switch_case;
};

struct Diag_Explicit_Fallthrough_Comment_In_Switch {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: We normally name the diagnostic struct after the problem, not the solution. Therefore this should be named something like Diag_Fallthrough_Without_Comment_In_Switch.

[[qljs::diag("E0423", Diagnostic_Severity::warning)]] //
[[qljs::message("missing fallthrough comment",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: This message and the message in docs/errors/E0423.md should match.

ARG(end_of_case))]] //
Source_Code_Span end_of_case;
};

struct Diag_Else_Has_No_If {
[[qljs::diag("E0065", Diagnostic_Severity::error)]] //
[[qljs::message("'else' has no corresponding 'if'", ARG(else_token))]] //
Expand Down
44 changes: 44 additions & 0 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2766,15 +2766,51 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {

bool keep_going = true;
bool is_before_first_switch_case = true;
Token prev_token;
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 think this variable's name is bad. prev_token does not refer to the token just before the current token; it refers to a prior case or default token or the first token of a statement.

Maybe previous_statement_first_token is a better name?

Hash_Set<String8_View> cases;
while (keep_going) {
auto is_comment_line = [&]() -> bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@strager the function to find the comment, is_comment_line skips all whitespace, which is similar to the lexer function. However, the lexer handles many more cases of characters. Do you think all those cases should be included within is_comment_line as well ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Must fix: We should not repeat comment-parsing code like you have done here.

I see two options:

  • Call Lexer's code directly instead of reimplementing or copy-pasting it here; or
  • Teach the lexer to communicate when a comment was seen before a token.
    We should try the latter solution as I think it will be more reliable and useful. I think adding a bool has_leading_comment; into the Token class should be simple enough for our needs. Token::has_leading_newline already exists, and the code for has_leading_comment might be very similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. I went with the suggestion of adding has_leading_comment and it worked very well 👍

const Char8 *th = this->lexer().end_of_previous_token();
bool f = false;
int i = 0;
for (;;) {
switch (th[i]) {
case '/':
if (th[i + 1] == '/') {
f = true;
}
goto exit_loop;
// skip empty lines to reach comment
case ' ':
case '\n':
case '\r':
case '\t':
case '\f':
case '\v':
break;
default:
goto exit_loop;
}
i++;
}
exit_loop:;
return f;
};
switch (this->peek().type) {
case Token_Type::right_curly:
this->skip();
keep_going = false;
break;

case Token_Type::kw_case: {
if (!is_before_first_switch_case &&
prev_token.type != Token_Type::kw_break &&
prev_token.type != Token_Type::kw_case && !is_comment_line()) {
this->diag_reporter_->report(
Diag_Explicit_Fallthrough_Comment_In_Switch{.end_of_case =
prev_token.span()});
}
prev_token = this->peek();
is_before_first_switch_case = false;
Source_Code_Span case_token_span = this->peek().span();
this->skip();
Expand Down Expand Up @@ -2806,6 +2842,13 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
}

case Token_Type::kw_default:
if (!is_before_first_switch_case &&
prev_token.type != Token_Type::kw_break &&
prev_token.type != Token_Type::kw_case && !is_comment_line()) {
this->diag_reporter_->report(
Diag_Explicit_Fallthrough_Comment_In_Switch{.end_of_case =
prev_token.span()});
}
is_before_first_switch_case = false;
this->skip();
QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::colon);
Expand All @@ -2818,6 +2861,7 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
.unexpected_statement = this->peek().span(),
});
}
prev_token = this->peek();
bool parsed_statement = this->parse_and_visit_statement(
v, Parse_Statement_Options{
.possibly_followed_by_another_statement = true,
Expand Down
4 changes: 3 additions & 1 deletion src/quick-lint-js/i18n/translation-table-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ const Translation_Table translation_data = {
{0, 0, 0, 0, 0, 35}, //
{36, 34, 39, 35, 40, 37}, //
{0, 0, 0, 0, 0, 39}, //
{33, 7, 40, 40, 33, 58}, //
{0, 0, 0, 0, 0, 58}, //
{33, 7, 40, 40, 33, 28}, //
{24, 11, 33, 22, 23, 24}, //
{34, 63, 43, 41, 33, 32}, //
{41, 11, 49, 43, 0, 39}, //
Expand Down Expand Up @@ -2156,6 +2157,7 @@ const Translation_Table translation_data = {
u8"missing exported name in import type\0"
u8"missing expression between parentheses\0"
u8"missing expression in placeholder within template literal\0"
u8"missing fallthrough comment\0"
u8"missing for loop header\0"
u8"missing function parameter list\0"
u8"missing header and body for 'for' loop\0"
Expand Down
5 changes: 3 additions & 2 deletions src/quick-lint-js/i18n/translation-table-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ namespace quick_lint_js {
using namespace std::literals::string_view_literals;

constexpr std::uint32_t translation_table_locale_count = 5;
constexpr std::uint16_t translation_table_mapping_table_size = 522;
constexpr std::size_t translation_table_string_table_size = 79941;
constexpr std::uint16_t translation_table_mapping_table_size = 523;
constexpr std::size_t translation_table_string_table_size = 79969;
constexpr std::size_t translation_table_locale_table_size = 35;

QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
Expand Down Expand Up @@ -378,6 +378,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
"missing exported name in import type"sv,
"missing expression between parentheses"sv,
"missing expression in placeholder within template literal"sv,
"missing fallthrough comment"sv,
"missing for loop header"sv,
"missing function parameter list"sv,
"missing header and body for 'for' loop"sv,
Expand Down
13 changes: 12 additions & 1 deletion src/quick-lint-js/i18n/translation-table-test-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct Translated_String {
};

// clang-format off
inline const Translated_String test_translation_table[521] = {
inline const Translated_String test_translation_table[522] = {
{
"\"global-groups\" entries must be strings"_translatable,
u8"\"global-groups\" entries must be strings",
Expand Down Expand Up @@ -3900,6 +3900,17 @@ inline const Translated_String test_translation_table[521] = {
u8"missing expression in placeholder within template literal",
},
},
{
"missing fallthrough comment"_translatable,
u8"missing fallthrough comment",
{
u8"missing fallthrough comment",
u8"missing fallthrough comment",
u8"missing fallthrough comment",
u8"missing fallthrough comment",
u8"missing fallthrough comment",
},
},
{
"missing for loop header"_translatable,
u8"missing for loop header",
Expand Down
27 changes: 27 additions & 0 deletions test/test-parse-warning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,33 @@ TEST_F(Test_Parse_Warning, warn_on_xor_operation_used_as_exponentiation) {
test_parse_and_visit_expression(u8"4 ^ 3"_sv, no_diags);
test_parse_and_visit_expression(u8"(x+2)^a"_sv, no_diags);
}
TEST_F(Test_Parse_Warning, Diag_Explicit_Fallthrough_Comment_In_Switch) {
test_parse_and_visit_statement(
u8"switch(cond1){case 1:\nfoo()\ncase 2:\nbar() //fallthrough\ndefault:}"_sv, //
u8" ^^^ Diag_Explicit_Fallthrough_Comment_In_Switch"_diag);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Must fix: I think the diagnostic is in the wrong place. As a user, I would expect the diagnostic to be between the statement and the following case, not at the beginning of the statement. In other words, the diagnostic should point me to where I should fix the problem. For example, the diagnostic could be just before case:

      u8"switch(cond1){case 1:\nfoo()\ncase 2:\nbar() //fallthrough\ndefault:}"_sv,  //
      u8"                              ` Diag_Explicit_Fallthrough_Comment_In_Switch"_diag);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. I went with the suggestion of adding has_leading_comment 👍
Also switched the diag to be just before case or default.

test_parse_and_visit_statement(
u8"switch(cond1){case 1:\nfoo()\ncase 2:\nlongBarFn()\ndefault:}"_sv, //
u8" ^^^^^^^^^ Diag_Explicit_Fallthrough_Comment_In_Switch"_diag, //
u8" ^^^ Diag_Explicit_Fallthrough_Comment_In_Switch"_diag);
// check for false positive
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

test_parse_and_visit_statement(
u8R"(switch(cond1){
case 1:
case 2:
default:
})"_sv,
no_diags);
test_parse_and_visit_statement(
u8R"(switch(cond1){
case 1:
foo()

//fallthrough
case 2:
bar()//fallthrough
default:})"_sv,
no_diags);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions test/test-parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ TEST_F(Test_Parse, asi_between_expression_statement_and_switch_label) {
switch (x) {
case a:
f()
//fallthrough
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

case b:
g()
}
Expand All @@ -278,6 +279,7 @@ TEST_F(Test_Parse, asi_between_expression_statement_and_switch_label) {
switch (x) {
case a:
f()
//fallthrough
default:
g()
}
Expand Down
Loading