Skip to content

Commit 422f3b4

Browse files
yashmasanistrager
authored andcommitted
feat: warn missing fallthrough
Add and report a diagnostic for missing fallthrough within switch statements. Fixes #1081
1 parent 4ac79ee commit 422f3b4

13 files changed

+147
-6
lines changed

docs/errors/E0427.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# E0427: missing 'break;' or '// fallthrough' comment between statement and 'case'
2+
3+
Switch Cases in javascript fallthrough to the next case if the `break` statement is not added at the end of the case.
4+
Since there is no explicit way of communication whether the fallthrough is intentional or not, it is recommended to use a comment indicating fallthrough.
5+
6+
```javascript
7+
function test (c) {
8+
switch (c) {
9+
case 1:
10+
console.log('case 1');
11+
default:
12+
console.log('default');
13+
}
14+
}
15+
```
16+
17+
To fix this error, place a comment at the end of `case 1` indicating fallthrough
18+
19+
```javascript
20+
function test (c) {
21+
switch (c) {
22+
case 1:
23+
console.log('case 1');
24+
//fallthrough
25+
default:
26+
console.log('default');
27+
}
28+
}
29+
```

po/messages.pot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,10 @@ msgstr ""
557557
msgid "this case will run instead"
558558
msgstr ""
559559

560+
#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
561+
msgid "missing 'break;' or '// fallthrough' comment between statement and 'case'"
562+
msgstr ""
563+
560564
#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
561565
msgid "'else' has no corresponding 'if'"
562566
msgstr ""

src/quick-lint-js/diag/diagnostic-metadata-generated.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = {
14231423
},
14241424
},
14251425

1426+
// Diag_Fallthrough_Without_Comment_In_Switch
1427+
{
1428+
.code = 427,
1429+
.severity = Diagnostic_Severity::warning,
1430+
.message_formats = {
1431+
QLJS_TRANSLATABLE("missing 'break;' or '// fallthrough' comment between statement and 'case'"),
1432+
},
1433+
.message_args = {
1434+
{
1435+
Diagnostic_Message_Arg_Info(offsetof(Diag_Fallthrough_Without_Comment_In_Switch, end_of_case), Diagnostic_Arg_Type::source_code_span),
1436+
},
1437+
},
1438+
},
1439+
14261440
// Diag_Else_Has_No_If
14271441
{
14281442
.code = 65,

src/quick-lint-js/diag/diagnostic-metadata-generated.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ namespace quick_lint_js {
103103
QLJS_DIAG_TYPE_NAME(Diag_Dot_Not_Allowed_After_Generic_Arguments_In_Type) \
104104
QLJS_DIAG_TYPE_NAME(Diag_Dot_Dot_Is_Not_An_Operator) \
105105
QLJS_DIAG_TYPE_NAME(Diag_Duplicated_Cases_In_Switch_Statement) \
106+
QLJS_DIAG_TYPE_NAME(Diag_Fallthrough_Without_Comment_In_Switch) \
106107
QLJS_DIAG_TYPE_NAME(Diag_Else_Has_No_If) \
107108
QLJS_DIAG_TYPE_NAME(Diag_Equals_Does_Not_Distribute_Over_Or) \
108109
QLJS_DIAG_TYPE_NAME(Diag_Escaped_Character_Disallowed_In_Identifiers) \
@@ -447,7 +448,7 @@ namespace quick_lint_js {
447448
/* END */
448449
// clang-format on
449450

450-
inline constexpr int Diag_Type_Count = 433;
451+
inline constexpr int Diag_Type_Count = 434;
451452

452453
extern const Diagnostic_Info all_diagnostic_infos[Diag_Type_Count];
453454
}

src/quick-lint-js/diag/diagnostic-types-2.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,15 @@ struct Diag_Duplicated_Cases_In_Switch_Statement {
759759
Source_Code_Span duplicated_switch_case;
760760
};
761761

762+
struct Diag_Fallthrough_Without_Comment_In_Switch {
763+
[[qljs::diag("E0427", Diagnostic_Severity::warning)]] //
764+
[
765+
[qljs::message("missing 'break;' or '// fallthrough' comment between "
766+
"statement and 'case'",
767+
ARG(end_of_case))]] //
768+
Source_Code_Span end_of_case;
769+
};
770+
762771
struct Diag_Else_Has_No_If {
763772
[[qljs::diag("E0065", Diagnostic_Severity::error)]] //
764773
[[qljs::message("'else' has no corresponding 'if'", ARG(else_token))]] //

src/quick-lint-js/fe/lex.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ void Lexer::parse_bom_before_shebang() {
142142
[[gnu::noinline]] void Lexer::parse_current_token() {
143143
this->last_last_token_end_ = this->last_token_.end;
144144
this->last_token_.has_leading_newline = false;
145+
this->last_token_.has_leading_comment = false;
145146
this->skip_whitespace();
146147

147148
while (!this->try_parse_current_token()) {
@@ -906,6 +907,7 @@ Lexer::Parsed_Template_Body Lexer::parse_template_body(
906907
void Lexer::skip_in_jsx() {
907908
this->last_last_token_end_ = this->last_token_.end;
908909
this->last_token_.has_leading_newline = false;
910+
this->last_token_.has_leading_comment = false;
909911
this->skip_whitespace();
910912

911913
retry:
@@ -980,6 +982,7 @@ const Char8* Lexer::find_equal_greater_in_jsx_children() const {
980982
void Lexer::skip_less_less_as_less() {
981983
QLJS_ASSERT(this->peek().type == Token_Type::less_less);
982984
this->last_token_.has_leading_newline = false;
985+
this->last_token_.has_leading_comment = false;
983986
this->last_token_.type = Token_Type::less;
984987
this->last_token_.begin += 1;
985988
this->last_last_token_end_ = this->last_token_.begin;
@@ -1156,6 +1159,7 @@ void Lexer::insert_semicolon() {
11561159

11571160
this->last_token_.type = Token_Type::semicolon;
11581161
this->last_token_.has_leading_newline = false;
1162+
this->last_token_.has_leading_comment = false;
11591163
this->last_token_.begin = this->input_;
11601164
this->last_token_.end = this->input_;
11611165
}
@@ -1951,7 +1955,7 @@ QLJS_WARNING_POP
19511955
void Lexer::skip_block_comment() {
19521956
QLJS_SLOW_ASSERT(this->input_[0] == '/' && this->input_[1] == '*');
19531957
const Char8* c = this->input_ + 2;
1954-
1958+
this->last_token_.has_leading_comment = true;
19551959
#if QLJS_HAVE_X86_SSE2
19561960
using Bool_Vector = Bool_Vector_16_SSE2;
19571961
using Char_Vector = Char_Vector_16_SSE2;
@@ -2049,6 +2053,7 @@ void Lexer::skip_line_comment_body() {
20492053
using Char_Vector = Char_Vector_1;
20502054
#endif
20512055

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

src/quick-lint-js/fe/parse-statement.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2815,7 +2815,27 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
28152815

28162816
bool keep_going = true;
28172817
bool is_before_first_switch_case = true;
2818+
std::optional<Token> previous_statement_first_token;
28182819
Hash_Set<String8_View> cases;
2820+
auto is_valid_end_of_case = [](Token_Type tk) {
2821+
switch (tk) {
2822+
case Token_Type::kw_return:
2823+
case Token_Type::kw_continue:
2824+
case Token_Type::kw_throw:
2825+
case Token_Type::kw_break:
2826+
case Token_Type::kw_case:
2827+
// Temporarily return true to omit diag with these statments
2828+
case Token_Type::kw_if:
2829+
case Token_Type::kw_try:
2830+
case Token_Type::kw_while:
2831+
case Token_Type::kw_do:
2832+
case Token_Type::kw_for:
2833+
case Token_Type::left_curly:
2834+
return true;
2835+
default:
2836+
return false;
2837+
}
2838+
};
28192839
while (keep_going) {
28202840
switch (this->peek().type) {
28212841
case Token_Type::right_curly:
@@ -2824,6 +2844,13 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
28242844
break;
28252845

28262846
case Token_Type::kw_case: {
2847+
if (!is_before_first_switch_case &&
2848+
!is_valid_end_of_case(previous_statement_first_token->type) &&
2849+
!this->peek().has_leading_comment) {
2850+
this->diag_reporter_->report(Diag_Fallthrough_Without_Comment_In_Switch{
2851+
.end_of_case = Source_Code_Span::unit(this->peek().begin)});
2852+
}
2853+
previous_statement_first_token = this->peek();
28272854
is_before_first_switch_case = false;
28282855
Source_Code_Span case_token_span = this->peek().span();
28292856
this->skip();
@@ -2855,6 +2882,12 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
28552882
}
28562883

28572884
case Token_Type::kw_default:
2885+
if (!is_before_first_switch_case &&
2886+
!is_valid_end_of_case(previous_statement_first_token->type) &&
2887+
!this->peek().has_leading_comment) {
2888+
this->diag_reporter_->report(Diag_Fallthrough_Without_Comment_In_Switch{
2889+
.end_of_case = Source_Code_Span::unit(this->peek().begin)});
2890+
}
28582891
is_before_first_switch_case = false;
28592892
this->skip();
28602893
QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::colon);
@@ -2867,6 +2900,7 @@ void Parser::parse_and_visit_switch(Parse_Visitor_Base &v) {
28672900
.unexpected_statement = this->peek().span(),
28682901
});
28692902
}
2903+
previous_statement_first_token = this->peek();
28702904
bool parsed_statement = this->parse_and_visit_statement(
28712905
v, Parse_Statement_Options{
28722906
.possibly_followed_by_another_statement = true,

src/quick-lint-js/fe/token.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ struct Token {
318318
Token_Type type;
319319

320320
bool has_leading_newline;
321+
bool has_leading_comment;
321322

322323
// Used only if this is a keyword token or an identifier token.
323324
// If the token contains no escape sequences, .normalized_identifier is

src/quick-lint-js/i18n/translation-table-generated.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,8 @@ const Translation_Table translation_data = {
339339
{35, 20, 49, 55, 39, 38}, //
340340
{34, 44, 0, 36, 0, 38}, //
341341
{58, 48, 46, 50, 28, 52}, //
342-
{0, 40, 0, 28, 0, 27}, //
342+
{0, 0, 0, 0, 0, 27}, //
343+
{0, 40, 0, 28, 0, 74}, //
343344
{29, 22, 33, 28, 26, 26}, //
344345
{0, 0, 0, 55, 0, 51}, //
345346
{48, 27, 63, 27, 0, 24}, //
@@ -2140,6 +2141,7 @@ const Translation_Table translation_data = {
21402141
u8"missing ':' in conditional expression\0"
21412142
u8"missing '<>' and '</>' to enclose multiple children\0"
21422143
u8"missing '=' after variable\0"
2144+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'\0"
21432145
u8"missing 'if' after 'else'\0"
21442146
u8"missing 'while (condition)' for do-while statement\0"
21452147
u8"missing TypeScript type\0"

src/quick-lint-js/i18n/translation-table-generated.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ namespace quick_lint_js {
1818
using namespace std::literals::string_view_literals;
1919

2020
constexpr std::uint32_t translation_table_locale_count = 5;
21-
constexpr std::uint16_t translation_table_mapping_table_size = 530;
22-
constexpr std::size_t translation_table_string_table_size = 80362;
21+
constexpr std::uint16_t translation_table_mapping_table_size = 531;
22+
constexpr std::size_t translation_table_string_table_size = 80436;
2323
constexpr std::size_t translation_table_locale_table_size = 35;
2424

2525
QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
@@ -354,6 +354,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
354354
"missing ':' in conditional expression"sv,
355355
"missing '<>' and '</>' to enclose multiple children"sv,
356356
"missing '=' after variable"sv,
357+
"missing 'break;' or '// fallthrough' comment between statement and 'case'"sv,
357358
"missing 'if' after 'else'"sv,
358359
"missing 'while (condition)' for do-while statement"sv,
359360
"missing TypeScript type"sv,

src/quick-lint-js/i18n/translation-table-test-generated.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct Translated_String {
2727
};
2828

2929
// clang-format off
30-
inline const Translated_String test_translation_table[529] = {
30+
inline const Translated_String test_translation_table[530] = {
3131
{
3232
"\"global-groups\" entries must be strings"_translatable,
3333
u8"\"global-groups\" entries must be strings",
@@ -3636,6 +3636,17 @@ inline const Translated_String test_translation_table[529] = {
36363636
u8"saknar '=' efter variabel",
36373637
},
36383638
},
3639+
{
3640+
"missing 'break;' or '// fallthrough' comment between statement and 'case'"_translatable,
3641+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
3642+
{
3643+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
3644+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
3645+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
3646+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
3647+
u8"missing 'break;' or '// fallthrough' comment between statement and 'case'",
3648+
},
3649+
},
36393650
{
36403651
"missing 'if' after 'else'"_translatable,
36413652
u8"missing 'if' after 'else'",

test/test-parse-warning.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,34 @@ TEST_F(Test_Parse_Warning, warn_on_unintuitive_precedence_when_using_bitshift) {
457457
test_parse_and_visit_expression(u8"a & (b >> 0xBEEF)"_sv, no_diags);
458458
test_parse_and_visit_expression(u8"a & b >> c"_sv, no_diags);
459459
}
460+
461+
TEST_F(Test_Parse_Warning, Diag_Fallthrough_Without_Comment_In_Switch) {
462+
test_parse_and_visit_statement(
463+
u8"switch(cond1){case 1:\nfoo()\ncase 2:\nbar() //fallthrough\ndefault:}"_sv, //
464+
u8" ` Diag_Fallthrough_Without_Comment_In_Switch"_diag);
465+
test_parse_and_visit_statement(
466+
u8"switch(cond1){case 1:\nfoo()\ncase 2:\nlongBarFn()\ndefault:}"_sv, //
467+
u8" ` Diag_Fallthrough_Without_Comment_In_Switch"_diag, //
468+
u8" ` Diag_Fallthrough_Without_Comment_In_Switch"_diag);
469+
// check for false positive
470+
test_parse_and_visit_statement(
471+
u8R"(switch(cond1){
472+
case 1:
473+
case 2:
474+
default:
475+
})"_sv,
476+
no_diags);
477+
test_parse_and_visit_statement(
478+
u8R"(switch(cond1){
479+
case 1:
480+
foo()
481+
482+
//fallthrough
483+
case 2:
484+
bar()//fallthrough
485+
default:})"_sv,
486+
no_diags);
487+
}
460488
}
461489
}
462490

test/test-parse.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ TEST_F(Test_Parse, asi_between_expression_statement_and_switch_label) {
263263
switch (x) {
264264
case a:
265265
f()
266+
//fallthrough
266267
case b:
267268
g()
268269
}
@@ -278,6 +279,7 @@ TEST_F(Test_Parse, asi_between_expression_statement_and_switch_label) {
278279
switch (x) {
279280
case a:
280281
f()
282+
//fallthrough
281283
default:
282284
g()
283285
}

0 commit comments

Comments
 (0)