use super::program_ref;
use super::{Context, LintRule};
use crate::tags::{self, Tags};
use crate::Program;
use crate::ProgramRef;
use deno_ast::swc::common::comments::Comment;
use deno_ast::swc::{
ast::*,
ecma_visit::{noop_visit_type, Visit, VisitWith},
};
use deno_ast::SourceRangedForSpanned;
use derive_more::Display;
#[derive(Debug)]
pub struct NoFallthrough;
const CODE: &str = "no-fallthrough";
#[derive(Display)]
enum NoFallthroughMessage {
#[display(fmt = "Fallthrough is not allowed")]
Unexpected,
}
#[derive(Display)]
enum NoFallthroughHint {
#[display(
fmt = "Add `break` or comment `/* falls through */` to your case statement"
)]
BreakOrComment,
}
impl LintRule for NoFallthrough {
fn tags(&self) -> Tags {
&[tags::RECOMMENDED]
}
fn code(&self) -> &'static str {
CODE
}
fn lint_program_with_ast_view<'view>(
&self,
context: &mut Context<'view>,
program: Program<'view>,
) {
let program = program_ref(program);
let mut visitor = NoFallthroughVisitor { context };
match program {
ProgramRef::Module(m) => visitor.visit_module(m),
ProgramRef::Script(s) => visitor.visit_script(s),
}
}
}
struct NoFallthroughVisitor<'c, 'view> {
context: &'c mut Context<'view>,
}
impl Visit for NoFallthroughVisitor<'_, '_> {
noop_visit_type!();
fn visit_switch_cases(&mut self, cases: &[SwitchCase]) {
let mut should_emit_err = false;
let mut prev_range = None;
'cases: for case in cases.iter() {
case.visit_with(self);
if should_emit_err {
let comments = self.context.leading_comments_at(case.start());
if !allow_fall_through(comments) {
if let Some(prev_range) = prev_range.take() {
self.context.add_diagnostic_with_hint(
prev_range,
CODE,
NoFallthroughMessage::Unexpected,
NoFallthroughHint::BreakOrComment,
);
}
}
}
should_emit_err = true;
let mut stops_exec = false;
for (idx, stmt) in case.cons.iter().enumerate() {
let last = idx + 1 == case.cons.len();
let metadata = self.context.control_flow().meta(stmt.start());
stops_exec |= metadata.map(|v| v.stops_execution()).unwrap_or(false);
if stops_exec {
should_emit_err = false;
}
if last {
let comments = self.context.trailing_comments_at(stmt.end());
if allow_fall_through(comments) {
should_emit_err = false;
prev_range = Some(case.range());
continue 'cases;
}
}
}
let empty = case.cons.is_empty()
|| matches!(case.cons.as_slice(), [Stmt::Block(b)] if b.stmts.is_empty());
if empty {
should_emit_err = false;
}
prev_range = Some(case.range());
}
}
}
fn allow_fall_through<'c>(
mut comments: impl Iterator<Item = &'c Comment>,
) -> bool {
comments.any(|comment| {
let l = comment.text.to_ascii_lowercase();
l.contains("fallthrough")
|| l.contains("falls through")
|| l.contains("fall through")
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_fallthrough_valid() {
assert_lint_ok! {
NoFallthrough,
"switch(foo) { case 0: a(); /* falls through */ case 1: b(); }",
"switch(foo) { case 0: a()\n /* falls through */ case 1: b(); }",
"switch(foo) { case 0: a(); /* fall through */ case 1: b(); }",
"switch(foo) { case 0: a(); /* fallthrough */ case 1: b(); }",
"switch(foo) { case 0: a(); /* FALLS THROUGH */ case 1: b(); }",
"function foo() { switch(foo) { case 0: a(); return; case 1: b(); }; }",
"switch(foo) { case 0: a(); throw 'foo'; case 1: b(); }",
"while (a) { switch(foo) { case 0: a(); continue; case 1: b(); } }",
"switch(foo) { case 0: a(); break; case 1: b(); }",
"switch(foo) { case 0: case 1: a(); break; case 2: b(); }",
"switch(foo) { case 0: case 1: break; case 2: b(); }",
"switch(foo) { case 0: case 1: break; default: b(); }",
"switch(foo) { case 0: case 1: a(); }",
"switch(foo) { case 0: case 1: a(); break; }",
"switch(foo) { case 0: case 1: break; }",
"switch(foo) { case 0:\n case 1: break; }",
"switch(foo) { case 0: // comment\n case 1: break; }",
"function foo() { switch(foo) { case 0: case 1: return; } }",
"function foo() { switch(foo) { case 0: {return;}\n case 1: {return;} } }",
"switch(foo) { case 0: case 1: {break;} }",
"switch(foo) { }",
"switch(foo) { case 0: switch(bar) { case 2: break; } /* falls through */ case 1: break; }",
"function foo() { switch(foo) { case 1: return a; a++; }}",
"switch (foo) { case 0: a(); /* falls through */ default: b(); /* comment */ }",
"switch (foo) { case 0: a(); /* falls through */ default: /* comment */ b(); }",
"switch (foo) { case 0: if (a) { break; } else { throw 0; } default: b(); }",
"switch (foo) { case 0: try { break; } finally {} default: b(); }",
"switch (foo) { case 0: try {} finally { break; } default: b(); }",
"switch (foo) { case 0: try { throw 0; } catch (err) { break; } default: b(); }",
"switch (foo) { case 0: do { throw 0; } while(a); default: b(); }",
"switch('test') { case 'symbol':\n case 'function': default: b(); }",
"switch('test') { case 'symbol':\n case 'function':\n default: b(); }",
"switch('test') { case 'symbol': case 'function': default: b(); }",
"switch(foo) { case 1:\n\n default: a(); }",
"switch(foo) { case 1:\n// comment\n default: a(); }",
r#"
switch(someValue) {
case 0: {
// nothing to do for this value
} break;
case 1:
break;
}
"#,
r#"
switch(someValue) {
case 0: {
// nothing to do for this value
} break;
default:
console.log(42);
}
"#,
};
}
#[test]
fn no_fallthrough_invalid() {
assert_lint_err! {
NoFallthrough,
"switch(foo) { case 0: a();\ncase 1: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0: a();\ndefault: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0: a(); default: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0: if (a) { break; } default: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0: try { throw 0; } catch (err) {} default: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0: while (a) { break; } default: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0:\n\n b()\n default: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
],
"switch(foo) { case 0: a(); /* falling through */ default: b() }": [
{
col: 14,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
]
};
}
#[test]
#[ignore = "It ends with break statement"]
fn no_fallthrough_invalid_2() {
assert_lint_err! {
NoFallthrough,
"switch(foo) { case 0: do { break; } while (a); default: b() }": [
{
col: 47,
message: NoFallthroughMessage::Unexpected,
hint: NoFallthroughHint::BreakOrComment,
}
]
};
}
}