diff --git a/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs b/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs index 3cd4fa97b89cc..ea268565513d1 100644 --- a/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs +++ b/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs @@ -1,37 +1,322 @@ +use std::ops::Range; + +use itertools::Itertools; +use oxc_ast::{ + ast::{Statement, SwitchCase, SwitchStatement}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -// use oxc_span::Span; +use oxc_semantic::{ + petgraph::{visit::EdgeRef, Direction}, + pg::neighbors_filtered_by_edge_weight, + BasicBlockId, EdgeType, ErrorEdgeKind, InstructionKind, +}; +use oxc_span::{GetSpan, Span}; +use regex::Regex; +use rustc_hash::{FxHashMap, FxHashSet}; use crate::{context::LintContext, rule::Rule, AstNode}; -// Ported from https://github.com/eslint/eslint/blob/main/lib/rules/no-fallthrough.js -// #[derive(Debug, Error, Diagnostic)] -// #[error("")] -// #[diagnostic(severity(warning), help(""))] -// struct NoFallthroughDiagnostic(#[label] pub Span); +fn no_fallthrough_case_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("eslint(no-fallthrough): Expected a 'break' statement before 'case'.") + .with_labels([span.into()]) +} + +fn no_fallthrough_default_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("eslint(no-fallthrough): Expected a 'break' statement before 'default'.") + .with_labels([span.into()]) +} + +fn no_unused_fallthrough_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("eslint(no-fallthrough): Found a comment that would permit fallthrough, but case cannot fall through.") + .with_labels([span.into()]) +} + +const DEFAULT_FALLTHROUGH_COMMENT_PATTERN: &str = r"falls?\s?through"; + +#[derive(Debug, Clone)] +struct Config { + comment_pattern: Regex, + allow_empty_case: bool, + report_unused_fallthrough_comment: bool, +} + +#[derive(Debug, Clone)] +pub struct NoFallthrough(Box); + +impl NoFallthrough { + fn new( + comment_pattern: Option<&str>, + allow_empty_case: Option, + report_unused_fallthrough_comment: Option, + ) -> Self { + let comment_pattern = comment_pattern.unwrap_or(DEFAULT_FALLTHROUGH_COMMENT_PATTERN); + Self(Box::new(Config { + comment_pattern: Regex::new(format!("(?iu){comment_pattern}").as_str()).unwrap(), + allow_empty_case: allow_empty_case.unwrap_or(false), + report_unused_fallthrough_comment: report_unused_fallthrough_comment.unwrap_or(false), + })) + } +} -#[derive(Debug, Default, Clone)] -pub struct NoFallthrough; +impl Default for NoFallthrough { + fn default() -> Self { + Self::new(None, None, None) + } +} declare_oxc_lint!( /// ### What it does /// + /// Disallow fallthrough of `case` statements /// - /// ### Why is this bad? - /// - /// - /// ### Example - /// ```javascript - /// ``` NoFallthrough, - nursery + correctness ); impl Rule for NoFallthrough { - fn run<'a>(&self, _node: &AstNode<'a>, _ctx: &LintContext<'a>) { - // TODO + fn from_configuration(value: serde_json::Value) -> Self { + let Some(value) = value.get(0) else { return Self::default() }; + let comment_pattern = value.get("commentPattern").and_then(serde_json::Value::as_str); + let allow_empty_case = value.get("allowEmptyCase").and_then(serde_json::Value::as_bool); + let report_unused_fallthrough_comment = + value.get("reportUnusedFallthroughComment").and_then(serde_json::Value::as_bool); + + Self::new(comment_pattern, allow_empty_case, report_unused_fallthrough_comment) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::SwitchStatement(switch) = node.kind() else { return }; + + let switch_id = node.cfg_id(); + let cfg = ctx.semantic().cfg(); + let graph = &cfg.graph; + + let (cfg_ids, tests, default, exit) = get_switch_semantic_cases(ctx, node, switch); + + let Some(default_or_exit) = default.or(exit) else { + // TODO: our `get_switch_semantic_cases` can't evaluate cfg_ids for switch statements + // with conditional discriminant. If we can access the IDs correctly it should never be `None`. + return; + }; + + let fallthroughs: FxHashSet = neighbors_filtered_by_edge_weight( + graph, + switch_id, + &|e| match e { + EdgeType::Normal | EdgeType::Jump | EdgeType::Error(ErrorEdgeKind::Explicit) => { + None + } + _ => Some(None), + }, + &mut |node, last_cond: Option| { + let node = *node; + + if node == switch_id { + return (last_cond, true); + } + if node == default_or_exit { + return (last_cond, false); + } + if tests.contains_key(&node) { + return (last_cond, true); + } + if cfg.basic_block(node).unreachable { + return (None, false); + } + + let fallthrough = graph + .edges_directed(node, Direction::Outgoing) + .find(|it| { + let target = it.target(); + if let Some(default) = default { + if default == target { + return true; + } + } + tests.contains_key(&target) + }) + .map(|e| e.target()); + + (fallthrough, fallthrough.is_none()) + }, + ) + .into_iter() + .flatten() + .collect(); + + let mut iter = switch.cases.iter().zip(cfg_ids).peekable(); + while let Some((case, _)) = iter.next() { + let Some((next_case, next_cfg_id)) = iter.peek() else { continue }; + if !fallthroughs.contains(next_cfg_id) { + if self.0.report_unused_fallthrough_comment { + if let Some(span) = self.maybe_allow_fallthrough_trivia(ctx, case, next_case) { + ctx.diagnostic(no_unused_fallthrough_diagnostic(span)); + } + } + continue; + } + let is_illegal_fallthrough = { + let is_fallthrough = !case.consequent.is_empty() + || (!self.0.allow_empty_case + && Self::has_blanks_between(ctx, case.span.start..next_case.span.start)); + is_fallthrough + && self.maybe_allow_fallthrough_trivia(ctx, case, next_case).is_none() + }; + + if is_illegal_fallthrough { + let span = next_case.span; + if next_case.is_default_case() { + ctx.diagnostic(no_fallthrough_default_diagnostic(span)); + } else { + ctx.diagnostic(no_fallthrough_case_diagnostic(span)); + } + } + } + } +} + +fn possible_fallthrough_comment_span(case: &SwitchCase) -> (u32, Option) { + if let Ok(Statement::BlockStatement(block)) = case.consequent.iter().exactly_one() { + let span = block.span; + if let Some(last) = block.body.last() { + (last.span().end, Some(span.end)) + } else { + (span.start, Some(span.end)) + } + } else if let Some(last) = case.consequent.last() { + (last.span().end, None) + } else { + (case.span.end, None) } } +impl NoFallthrough { + fn has_blanks_between(ctx: &LintContext, range: Range) -> bool { + let in_between = &ctx.semantic().source_text()[range.start as usize..range.end as usize]; + // check for at least 2 new lines, we allow the first new line for formatting. + in_between.bytes().filter(|it| *it == b'\n').nth(1).is_some() + } + + fn maybe_allow_fallthrough_trivia( + &self, + ctx: &LintContext, + case: &SwitchCase, + fall: &SwitchCase, + ) -> Option { + let semantic = ctx.semantic(); + let is_fallthrough_comment_in_range = |range: Range| { + let comment = semantic + .trivias() + .comments_range(range) + .map(|(start, comment)| { + &semantic.source_text()[*start as usize..comment.end as usize] + }) + .last() + .map(str::trim); + + comment.is_some_and(|comment| { + (!comment.starts_with("oxlint-") && !comment.starts_with("eslint-")) + && self.0.comment_pattern.is_match(comment) + }) + }; + + let (start, end) = possible_fallthrough_comment_span(case); + + if let Some(end) = end { + let range = start..end; + if is_fallthrough_comment_in_range(range.clone()) { + return Some(Span::new(start, end)); + } + } + + let range = start..fall.span.start; + if is_fallthrough_comment_in_range(range.clone()) { + Some(Span::new(start, fall.span.start)) + } else { + None + } + } +} + +/// Get semantic information about a switch cases and its exit point. +// ----------------------------------------!README!----------------------------------------------- +// >> PLEASE DON'T MAKE IT A REPEATING PATTERN IN THE PROJECT, ONE TIME HACK TO GET IT DONE +// >> TODO: it is a hack to get our cases `cfg_id`s. please replace me with semantic API when +// one became available. This code is highly volitile and has a lot of assumptions about +// the current shape of the CFG, It is just a slow and dirty workaround! +// ---------------------------------------------------------------------------------------------- +// TREAT LIKE BLACK MAGIC, IT BREAKS WITH SMALLEST CHANGES TO THE SWITCH CASE CFG! +// NOTE: DO NOT COPY -- DO NOT REUSE -- DO NOT EXTEND +// NOTE: DO NOT COPY -- DO NOT REUSE -- DO NOT EXTEND +// NOTE: DO NOT COPY -- DO NOT REUSE -- DO NOT EXTEND +// NOTE: DO NOT COPY -- DO NOT REUSE -- DO NOT EXTEND +// NOTE: DO NOT COPY -- DO NOT REUSE -- DO NOT EXTEND +// IF U NEED THIS AS AN API COMMENT ON THE ISSUE OR CREATE A DUP IF IT IS CLOSED! +// TAKE IT AS A MAGICAL BLACK BOX, NO DOCUMENTATION TO PREVENT REUSE! +// Issue: +fn get_switch_semantic_cases( + ctx: &LintContext, + node: &AstNode, + switch: &SwitchStatement, +) -> ( + Vec, + FxHashMap, + /* default */ Option, + /* exit */ Option, +) { + let cfg = &ctx.semantic().cfg(); + let graph = &cfg.graph; + let has_default = switch.cases.iter().any(SwitchCase::is_default_case); + let (tests, exit) = graph + .edges_directed(node.cfg_id(), Direction::Outgoing) + .fold((Vec::new(), None), |(mut conds, exit), it| { + let target = it.target(); + if !matches!(it.weight(), EdgeType::Normal) { + (conds, exit) + } else if cfg + .basic_block(target) + .instructions() + .iter() + .any(|it| matches!(it.kind, InstructionKind::Condition)) + { + let is_empty = graph + .edges_directed(target, Direction::Outgoing) + .filter(|it| matches!(it.weight(), EdgeType::Jump)) + .exactly_one() + .ok() + .and_then(|it| { + cfg.basic_block(it.target()) + .instructions() + .first() + .and_then(|it| it.node_id) + .map(|id| ctx.nodes().parent_kind(id)) + .and_then(|it| match it { + Some(AstKind::SwitchCase(case)) => Some(case), + _ => None, + }) + }) + .is_some_and(|it| it.consequent.is_empty() || it.consequent.iter().exactly_one().is_ok_and(|it| matches!(it, Statement::BlockStatement(b) if b.body.is_empty()))); + conds.push((target, is_empty)); + (conds, exit) + } else { + (conds, Some(target)) + } + }); + + let mut cfg_ids: Vec<_> = tests.iter().rev().map(|it| it.0).collect(); + let (default, exit) = if has_default { + if let Some(exit) = exit { + cfg_ids.push(exit); + } + (exit, None) + } else { + (None, exit) + }; + (cfg_ids, FxHashMap::from_iter(tests), default, exit) +} + #[test] fn test() { use crate::tester::Tester; @@ -75,7 +360,12 @@ fn test() { ("switch (foo) { case 0: try {} finally { break; } default: b(); }", None), ("switch (foo) { case 0: try { throw 0; } catch (err) { break; } default: b(); }", None), ("switch (foo) { case 0: do { throw 0; } while(a); default: b(); }", None), - ("switch (foo) { case 0: a(); \n// eslint-disable-next-line no-fallthrough\n case 1: }", None), + // TODO: we need a way to handle disables in the higher context, For example purging + // disabled diagnostics. + // ( + // "switch (foo) { case 0: a(); \n// eslint-disable-next-line no-fallthrough\n case 1: }", + // None, + // ), ( "switch(foo) { case 0: a(); /* no break */ case 1: b(); }", Some(serde_json::json!([{ @@ -110,64 +400,78 @@ fn test() { ("switch(foo) { case 0: \n /* with comments */ \n case 1: b(); }", Some(serde_json::json!([{ "allowEmptyCase": true }]))), ("switch (a) {\n case 1: ; break; \n case 3: }", Some(serde_json::json!([{ "allowEmptyCase": true }]))), ("switch (a) {\n case 1: ; break; \n case 3: }", Some(serde_json::json!([{ "allowEmptyCase": false }]))), + ( + "switch(foo) { case 0: a(); break; /* falls through */ case 1: b(); }", + Some(serde_json::json!([{ + "reportUnusedFallthroughComment": false + }])), + ), ]; let fail = vec![ - // ("switch(foo) { case 0: a();\ncase 1: b() }", None), - // ("switch(foo) { case 0: a();\ndefault: b() }", None), - // ("switch(foo) { case 0: a(); default: b() }", None), - // ("switch(foo) { case 0: if (a) { break; } default: b() }", None), - // ("switch(foo) { case 0: try { throw 0; } catch (err) {} default: b() }", None), - // ("switch(foo) { case 0: while (a) { break; } default: b() }", None), - // ("switch(foo) { case 0: do { break; } while (a); default: b() }", None), - // ("switch(foo) { case 0:\n\n default: b() }", None), - // ("switch(foo) { case 0: {} default: b() }", None), - // ("switch(foo) { case 0: a(); { /* falls through */ } default: b() }", None), - // ("switch(foo) { case 0: { /* falls through */ } a(); default: b() }", None), - // ("switch(foo) { case 0: if (a) { /* falls through */ } default: b() }", None), - // ("switch(foo) { case 0: { { /* falls through */ } } default: b() }", None), - // ("switch(foo) { case 0: { /* comment */ } default: b() }", None), - // ("switch(foo) { case 0:\n // comment\n default: b() }", None), - // ("switch(foo) { case 0: a(); /* falling through */ default: b() }", None), - // ( - // "switch(foo) { case 0: a();\n/* no break */\ncase 1: b(); }", - // Some(serde_json::json!([{ - // "commentPattern": "break omitted" - // }])), - // ), - // ( - // "switch(foo) { case 0: a();\n/* no break */\n/* todo: fix readability */\ndefault: b() }", - // Some(serde_json::json!([{ - // "commentPattern": "no break" - // }])), - // ), - // ( - // "switch(foo) { case 0: { a();\n/* no break */\n/* todo: fix readability */ }\ndefault: b() }", - // Some(serde_json::json!([{ - // "commentPattern": "no break" - // }])), - // ), - // ("switch(foo) { case 0: \n /* with comments */ \ncase 1: b(); }", None), - // ( - // "switch(foo) { case 0:\n\ncase 1: b(); }", - // Some(serde_json::json!([{ - // "allowEmptyCase": false - // }])), - // ), - // ("switch(foo) { case 0:\n\ncase 1: b(); }", Some(serde_json::json!([{}]))), - // ( - // "switch (a) { case 1: \n ; case 2: }", - // Some(serde_json::json!([{ "allowEmptyCase": false }])), - // ), - // ( - // "switch (a) { case 1: ; case 2: ; case 3: }", - // Some(serde_json::json!([{ "allowEmptyCase": true }])), - // ), - // ( - // "switch (foo) { case 0: a(); \n// eslint-enable no-fallthrough\n case 1: }", - // Some(serde_json::json!([{}])), - // ), + ("switch(foo) { case 0: a();\ncase 1: b() }", None), + ("switch(foo) { case 0: a();\ndefault: b() }", None), + ("switch(foo) { case 0: a(); default: b() }", None), + ("switch(foo) { case 0: if (a) { break; } default: b() }", None), + ("switch(foo) { case 0: try { throw 0; } catch (err) {} default: b() }", None), + ("switch(foo) { case 0: while (a) { break; } default: b() }", None), + ("switch(foo) { case 0: do { break; } while (a); default: b() }", None), + ("switch(foo) { case 0:\n\n default: b() }", None), + ("switch(foo) { case 0: {} default: b() }", None), + ("switch(foo) { case 0: a(); { /* falls through */ } default: b() }", None), + ("switch(foo) { case 0: { /* falls through */ } a(); default: b() }", None), + ("switch(foo) { case 0: if (a) { /* falls through */ } default: b() }", None), + ("switch(foo) { case 0: { { /* falls through */ } } default: b() }", None), + ("switch(foo) { case 0: { /* comment */ } default: b() }", None), + ("switch(foo) { case 0:\n // comment\n default: b() }", None), + ("switch(foo) { case 0: a(); /* falling through */ default: b() }", None), + ( + "switch(foo) { case 0: a();\n/* no break */\ncase 1: b(); }", + Some(serde_json::json!([{ + "commentPattern": "break omitted" + }])), + ), + ( + "switch(foo) { case 0: a();\n/* no break */\n/* todo: fix readability */\ndefault: b() }", + Some(serde_json::json!([{ + "commentPattern": "no break" + }])), + ), + ( + "switch(foo) { case 0: { a();\n/* no break */\n/* todo: fix readability */ }\ndefault: b() }", + Some(serde_json::json!([{ + "commentPattern": "no break" + }])), + ), + ("switch(foo) { case 0: \n /* with comments */ \ncase 1: b(); }", None), + ( + "switch(foo) { case 0:\n\ncase 1: b(); }", + Some(serde_json::json!([{ + "allowEmptyCase": false + }])), + ), + ("switch(foo) { case 0:\n\ncase 1: b(); }", Some(serde_json::json!([{}]))), + ( + "switch (a) { case 1: \n ; case 2: }", + Some(serde_json::json!([{ "allowEmptyCase": false }])), + ), + ( + "switch (a) { case 1: ; case 2: ; case 3: }", + Some(serde_json::json!([{ "allowEmptyCase": true }])), + ), + ( + "switch (foo) { case 0: a(); \n// eslint-enable no-fallthrough\n case 1: }", + Some(serde_json::json!([{}])), + ), + ( + "switch(foo) { case 0: a(); break; /* falls through */ case 1: b(); }", + Some(serde_json::json!([{ + "reportUnusedFallthroughComment": true + }])), + ), + // TODO: it should fail but doesn't, we ignore conditional discriminants for now. + // ("switch (a === b ? c : d) { case 1: ; case 2: ; case 3: ; }", None) ]; - Tester::new(NoFallthrough::NAME, pass, fail).test(); + Tester::new(NoFallthrough::NAME, pass, fail).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/no_fallthrough.snap b/crates/oxc_linter/src/snapshots/no_fallthrough.snap new file mode 100644 index 0000000000000..632b7bca54c35 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_fallthrough.snap @@ -0,0 +1,177 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_fallthrough +--- + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:2:1] + 1 │ switch(foo) { case 0: a(); + 2 │ case 1: b() } + · ─────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:2:1] + 1 │ switch(foo) { case 0: a(); + 2 │ default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:28] + 1 │ switch(foo) { case 0: a(); default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:41] + 1 │ switch(foo) { case 0: if (a) { break; } default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:55] + 1 │ switch(foo) { case 0: try { throw 0; } catch (err) {} default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:44] + 1 │ switch(foo) { case 0: while (a) { break; } default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:48] + 1 │ switch(foo) { case 0: do { break; } while (a); default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:3:2] + 2 │ + 3 │ default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:26] + 1 │ switch(foo) { case 0: {} default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:52] + 1 │ switch(foo) { case 0: a(); { /* falls through */ } default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:52] + 1 │ switch(foo) { case 0: { /* falls through */ } a(); default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:54] + 1 │ switch(foo) { case 0: if (a) { /* falls through */ } default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:51] + 1 │ switch(foo) { case 0: { { /* falls through */ } } default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:41] + 1 │ switch(foo) { case 0: { /* comment */ } default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:3:2] + 2 │ // comment + 3 │ default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:1:50] + 1 │ switch(foo) { case 0: a(); /* falling through */ default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:3:1] + 2 │ /* no break */ + 3 │ case 1: b(); } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:4:1] + 3 │ /* todo: fix readability */ + 4 │ default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'default'. + ╭─[no_fallthrough.tsx:4:1] + 3 │ /* todo: fix readability */ } + 4 │ default: b() } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:3:1] + 2 │ /* with comments */ + 3 │ case 1: b(); } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:3:1] + 2 │ + 3 │ case 1: b(); } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:3:1] + 2 │ + 3 │ case 1: b(); } + · ──────────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:2:4] + 1 │ switch (a) { case 1: + 2 │ ; case 2: } + · ─────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:1:24] + 1 │ switch (a) { case 1: ; case 2: ; case 3: } + · ───────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:1:34] + 1 │ switch (a) { case 1: ; case 2: ; case 3: } + · ─────── + ╰──── + + ⚠ eslint(no-fallthrough): Expected a 'break' statement before 'case'. + ╭─[no_fallthrough.tsx:3:2] + 2 │ // eslint-enable no-fallthrough + 3 │ case 1: } + · ─────── + ╰──── + + ⚠ eslint(no-fallthrough): Found a comment that would permit fallthrough, but case cannot fall through. + ╭─[no_fallthrough.tsx:1:34] + 1 │ switch(foo) { case 0: a(); break; /* falls through */ case 1: b(); } + · ───────────────────── + ╰────