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 13 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/E0427.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# E0427: missing 'break;' or '// fallthrough' comment between statement and 'case'

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();
Copy link
Collaborator

Choose a reason for hiding this comment

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

These examples assume that globals foo and bar exist, but they don't. This causes warning E0057 ('use of undeclared variable: foo').

To fix this, I suggest writing console.log calls instead. (The console global should exist in these examples.)

}
}
```

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 'break;' or '// fallthrough' comment between statement and 'case'"
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_Fallthrough_Without_Comment_In_Switch
{
.code = 427,
.severity = Diagnostic_Severity::warning,
.message_formats = {
QLJS_TRANSLATABLE("missing 'break;' or '// fallthrough' comment between statement and 'case'"),
},
.message_args = {
{
Diagnostic_Message_Arg_Info(offsetof(Diag_Fallthrough_Without_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_Fallthrough_Without_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 @@ -447,7 +448,7 @@ namespace quick_lint_js {
/* END */
// clang-format on

inline constexpr int Diag_Type_Count = 433;
inline constexpr int Diag_Type_Count = 434;

extern const Diagnostic_Info all_diagnostic_infos[Diag_Type_Count];
}
Expand Down
9 changes: 9 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,15 @@ struct Diag_Duplicated_Cases_In_Switch_Statement {
Source_Code_Span duplicated_switch_case;
};

struct Diag_Fallthrough_Without_Comment_In_Switch {
[[qljs::diag("E0427", Diagnostic_Severity::warning)]] //
[
[qljs::message("missing 'break;' or '// fallthrough' comment between "
"statement and 'case'",
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
7 changes: 6 additions & 1 deletion src/quick-lint-js/fe/lex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ void Lexer::parse_bom_before_shebang() {
[[gnu::noinline]] void Lexer::parse_current_token() {
this->last_last_token_end_ = this->last_token_.end;
this->last_token_.has_leading_newline = false;
this->last_token_.has_leading_comment = false;
this->skip_whitespace();

while (!this->try_parse_current_token()) {
Expand Down Expand Up @@ -906,6 +907,7 @@ Lexer::Parsed_Template_Body Lexer::parse_template_body(
void Lexer::skip_in_jsx() {
this->last_last_token_end_ = this->last_token_.end;
this->last_token_.has_leading_newline = false;
this->last_token_.has_leading_comment = false;
this->skip_whitespace();

retry:
Expand Down Expand Up @@ -980,6 +982,7 @@ const Char8* Lexer::find_equal_greater_in_jsx_children() const {
void Lexer::skip_less_less_as_less() {
QLJS_ASSERT(this->peek().type == Token_Type::less_less);
this->last_token_.has_leading_newline = false;
this->last_token_.has_leading_comment = false;
this->last_token_.type = Token_Type::less;
this->last_token_.begin += 1;
this->last_last_token_end_ = this->last_token_.begin;
Expand Down Expand Up @@ -1156,6 +1159,7 @@ void Lexer::insert_semicolon() {

this->last_token_.type = Token_Type::semicolon;
this->last_token_.has_leading_newline = false;
this->last_token_.has_leading_comment = false;
this->last_token_.begin = this->input_;
this->last_token_.end = this->input_;
}
Expand Down Expand Up @@ -1951,7 +1955,7 @@ QLJS_WARNING_POP
void Lexer::skip_block_comment() {
QLJS_SLOW_ASSERT(this->input_[0] == '/' && this->input_[1] == '*');
const Char8* c = this->input_ + 2;

this->last_token_.has_leading_comment = true;
#if QLJS_HAVE_X86_SSE2
using Bool_Vector = Bool_Vector_16_SSE2;
using Char_Vector = Char_Vector_16_SSE2;
Expand Down Expand Up @@ -2049,6 +2053,7 @@ void Lexer::skip_line_comment_body() {
using Char_Vector = Char_Vector_1;
#endif

this->last_token_.has_leading_comment = true;
auto found_comment_end = [&]() {
int n = newline_character_size(this->input_);

Expand Down
33 changes: 33 additions & 0 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2815,7 +2815,26 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {

bool keep_going = true;
bool is_before_first_switch_case = true;
Token previous_statement_first_token;
Hash_Set<String8_View> cases;
auto is_valid_end_of_case = [](Token_Type tk) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 Nice helper function.

switch (tk) {
case Token_Type::kw_return:
case Token_Type::kw_continue:
case Token_Type::kw_throw:
case Token_Type::kw_break:
case Token_Type::kw_case:
// Temporarily return true to omit diag with these statments
case Token_Type::kw_if:
case Token_Type::kw_try:
case Token_Type::kw_while:
case Token_Type::kw_do:
case Token_Type::kw_for:
return true;
default:
return false;
}
};
while (keep_going) {
switch (this->peek().type) {
case Token_Type::right_curly:
Expand All @@ -2824,6 +2843,13 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
break;

case Token_Type::kw_case: {
if (!is_before_first_switch_case &&
!is_valid_end_of_case(previous_statement_first_token.type) &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

GCC reports a warning because it thinks previous_statement_first_token might be uninitialized here.

To fix the warning, I think making previous_statement_first_token a std::optional is a decent solution.

!this->peek().has_leading_comment) {
this->diag_reporter_->report(Diag_Fallthrough_Without_Comment_In_Switch{
.end_of_case = Source_Code_Span::unit(this->peek().begin)});
}
previous_statement_first_token = this->peek();
is_before_first_switch_case = false;
Source_Code_Span case_token_span = this->peek().span();
this->skip();
Expand Down Expand Up @@ -2855,6 +2881,12 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
}

case Token_Type::kw_default:
if (!is_before_first_switch_case &&
!is_valid_end_of_case(previous_statement_first_token.type) &&
!this->peek().has_leading_comment) {
this->diag_reporter_->report(Diag_Fallthrough_Without_Comment_In_Switch{
.end_of_case = Source_Code_Span::unit(this->peek().begin)});
}
is_before_first_switch_case = false;
this->skip();
QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::colon);
Expand All @@ -2867,6 +2899,7 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
.unexpected_statement = this->peek().span(),
});
}
previous_statement_first_token = this->peek();
bool parsed_statement = this->parse_and_visit_statement(
v, Parse_Statement_Options{
.possibly_followed_by_another_statement = true,
Expand Down
1 change: 1 addition & 0 deletions src/quick-lint-js/fe/token.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ struct Token {
Token_Type type;

bool has_leading_newline;
bool has_leading_comment;

// Used only if this is a keyword token or an identifier token.
// If the token contains no escape sequences, .normalized_identifier is
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 @@ -339,7 +339,8 @@ const Translation_Table translation_data = {
{35, 20, 49, 55, 39, 38}, //
{34, 44, 0, 36, 0, 38}, //
{58, 48, 46, 50, 28, 52}, //
{0, 40, 0, 28, 0, 27}, //
{0, 0, 0, 0, 0, 27}, //
{0, 40, 0, 28, 0, 74}, //
{29, 22, 33, 28, 26, 26}, //
{0, 0, 0, 55, 0, 51}, //
{48, 27, 63, 27, 0, 24}, //
Expand Down Expand Up @@ -2140,6 +2141,7 @@ const Translation_Table translation_data = {
u8"missing ':' in conditional expression\0"
u8"missing '<>' and '</>' to enclose multiple children\0"
u8"missing '=' after variable\0"
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'\0"
u8"missing 'if' after 'else'\0"
u8"missing 'while (condition)' for do-while statement\0"
u8"missing TypeScript type\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 = 530;
constexpr std::size_t translation_table_string_table_size = 80362;
constexpr std::uint16_t translation_table_mapping_table_size = 531;
constexpr std::size_t translation_table_string_table_size = 80436;
constexpr std::size_t translation_table_locale_table_size = 35;

QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
Expand Down Expand Up @@ -354,6 +354,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
"missing ':' in conditional expression"sv,
"missing '<>' and '</>' to enclose multiple children"sv,
"missing '=' after variable"sv,
"missing 'break;' or '// fallthrough' comment between statement and 'case'"sv,
"missing 'if' after 'else'"sv,
"missing 'while (condition)' for do-while statement"sv,
"missing TypeScript type"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[529] = {
inline const Translated_String test_translation_table[530] = {
{
"\"global-groups\" entries must be strings"_translatable,
u8"\"global-groups\" entries must be strings",
Expand Down Expand Up @@ -3636,6 +3636,17 @@ inline const Translated_String test_translation_table[529] = {
u8"saknar '=' efter variabel",
},
},
{
"missing 'break;' or '// fallthrough' comment between statement and 'case'"_translatable,
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
{
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
},
},
{
"missing 'if' after 'else'"_translatable,
u8"missing 'if' after 'else'",
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_Fallthrough_Without_Comment_In_Switch) {
test_parse_and_visit_statement(
u8"switch(cond1){case 1:\nfoo()\ncase 2:\nbar() //fallthrough\ndefault:}"_sv, //
u8" ` Diag_Fallthrough_Without_Comment_In_Switch"_diag);
test_parse_and_visit_statement(
u8"switch(cond1){case 1:\nfoo()\ncase 2:\nlongBarFn()\ndefault:}"_sv, //
u8" ` Diag_Fallthrough_Without_Comment_In_Switch"_diag, //
u8" ` Diag_Fallthrough_Without_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);
}
TEST_F(Test_Parse_Warning, warn_on_unintuitive_precedence_when_using_bitshift) {
test_parse_and_visit_expression(
u8"var1 & 0x01 >> 0x02"_sv,
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