mirror of
https://github.com/danbulant/oxc
synced 2026-05-20 12:48:38 +00:00
feat(linter): add no-fallthrough. (#3673)
[no-fallthrough](https://eslint.org/docs/latest/rules/no-fallthrough) [oxlint-ecosystem-ci](https://github.com/rzvxa/oxlint-ecosystem-ci/actions/runs/9546510803) related to #633 and closes #597 also related to #3662
This commit is contained in:
parent
815260ed2f
commit
080ecbd88d
2 changed files with 554 additions and 73 deletions
|
|
@ -1,35 +1,320 @@
|
||||||
|
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_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};
|
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||||
|
|
||||||
// Ported from https://github.com/eslint/eslint/blob/main/lib/rules/no-fallthrough.js
|
fn no_fallthrough_case_diagnostic(span: Span) -> OxcDiagnostic {
|
||||||
// #[derive(Debug, Error, Diagnostic)]
|
OxcDiagnostic::error("eslint(no-fallthrough): Expected a 'break' statement before 'case'.")
|
||||||
// #[error("")]
|
.with_labels([span.into()])
|
||||||
// #[diagnostic(severity(warning), help(""))]
|
}
|
||||||
// struct NoFallthroughDiagnostic(#[label] pub Span);
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
fn no_fallthrough_default_diagnostic(span: Span) -> OxcDiagnostic {
|
||||||
pub struct NoFallthrough;
|
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<Config>);
|
||||||
|
|
||||||
|
impl NoFallthrough {
|
||||||
|
fn new(
|
||||||
|
comment_pattern: Option<&str>,
|
||||||
|
allow_empty_case: Option<bool>,
|
||||||
|
report_unused_fallthrough_comment: Option<bool>,
|
||||||
|
) -> 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),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NoFallthrough {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(None, None, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare_oxc_lint!(
|
declare_oxc_lint!(
|
||||||
/// ### What it does
|
/// ### What it does
|
||||||
///
|
///
|
||||||
|
/// Disallow fallthrough of `case` statements
|
||||||
///
|
///
|
||||||
/// ### Why is this bad?
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ### Example
|
|
||||||
/// ```javascript
|
|
||||||
/// ```
|
|
||||||
NoFallthrough,
|
NoFallthrough,
|
||||||
nursery
|
correctness
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Rule for NoFallthrough {
|
impl Rule for NoFallthrough {
|
||||||
fn run<'a>(&self, _node: &AstNode<'a>, _ctx: &LintContext<'a>) {
|
fn from_configuration(value: serde_json::Value) -> Self {
|
||||||
// TODO
|
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<BasicBlockId> = 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<BasicBlockId>| {
|
||||||
|
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<u32>) {
|
||||||
|
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<u32>) -> 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<Span> {
|
||||||
|
let semantic = ctx.semantic();
|
||||||
|
let is_fallthrough_comment_in_range = |range: Range<u32>| {
|
||||||
|
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: <https://github.com/oxc-project/oxc/issues/3662>
|
||||||
|
fn get_switch_semantic_cases(
|
||||||
|
ctx: &LintContext,
|
||||||
|
node: &AstNode,
|
||||||
|
switch: &SwitchStatement,
|
||||||
|
) -> (
|
||||||
|
Vec<BasicBlockId>,
|
||||||
|
FxHashMap<BasicBlockId, /* is_empty */ bool>,
|
||||||
|
/* default */ Option<BasicBlockId>,
|
||||||
|
/* exit */ Option<BasicBlockId>,
|
||||||
|
) {
|
||||||
|
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]
|
#[test]
|
||||||
|
|
@ -75,7 +360,12 @@ fn test() {
|
||||||
("switch (foo) { case 0: try {} finally { break; } default: b(); }", None),
|
("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: 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: 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(); }",
|
"switch(foo) { case 0: a(); /* no break */ case 1: b(); }",
|
||||||
Some(serde_json::json!([{
|
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(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": true }]))),
|
||||||
("switch (a) {\n case 1: ; break; \n case 3: }", Some(serde_json::json!([{ "allowEmptyCase": false }]))),
|
("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![
|
let fail = vec![
|
||||||
// ("switch(foo) { case 0: a();\ncase 1: b() }", None),
|
("switch(foo) { case 0: a();\ncase 1: b() }", None),
|
||||||
// ("switch(foo) { case 0: a();\ndefault: b() }", None),
|
("switch(foo) { case 0: a();\ndefault: b() }", None),
|
||||||
// ("switch(foo) { case 0: a(); default: b() }", None),
|
("switch(foo) { case 0: a(); default: b() }", None),
|
||||||
// ("switch(foo) { case 0: if (a) { break; } 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: try { throw 0; } catch (err) {} default: b() }", None),
|
||||||
// ("switch(foo) { case 0: while (a) { break; } 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: do { break; } while (a); default: b() }", None),
|
||||||
// ("switch(foo) { case 0:\n\n default: b() }", None),
|
("switch(foo) { case 0:\n\n default: b() }", None),
|
||||||
// ("switch(foo) { case 0: {} default: b() }", None),
|
("switch(foo) { case 0: {} default: b() }", None),
|
||||||
// ("switch(foo) { case 0: a(); { /* falls through */ } 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: { /* falls through */ } a(); default: b() }", None),
|
||||||
// ("switch(foo) { case 0: if (a) { /* falls through */ } 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: { { /* falls through */ } } default: b() }", None),
|
||||||
// ("switch(foo) { case 0: { /* comment */ } 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:\n // comment\n default: b() }", None),
|
||||||
// ("switch(foo) { case 0: a(); /* falling through */ default: b() }", None),
|
("switch(foo) { case 0: a(); /* falling through */ default: b() }", None),
|
||||||
// (
|
(
|
||||||
// "switch(foo) { case 0: a();\n/* no break */\ncase 1: b(); }",
|
"switch(foo) { case 0: a();\n/* no break */\ncase 1: b(); }",
|
||||||
// Some(serde_json::json!([{
|
Some(serde_json::json!([{
|
||||||
// "commentPattern": "break omitted"
|
"commentPattern": "break omitted"
|
||||||
// }])),
|
}])),
|
||||||
// ),
|
),
|
||||||
// (
|
(
|
||||||
// "switch(foo) { case 0: a();\n/* no break */\n/* todo: fix readability */\ndefault: b() }",
|
"switch(foo) { case 0: a();\n/* no break */\n/* todo: fix readability */\ndefault: b() }",
|
||||||
// Some(serde_json::json!([{
|
Some(serde_json::json!([{
|
||||||
// "commentPattern": "no break"
|
"commentPattern": "no break"
|
||||||
// }])),
|
}])),
|
||||||
// ),
|
),
|
||||||
// (
|
(
|
||||||
// "switch(foo) { case 0: { a();\n/* no break */\n/* todo: fix readability */ }\ndefault: b() }",
|
"switch(foo) { case 0: { a();\n/* no break */\n/* todo: fix readability */ }\ndefault: b() }",
|
||||||
// Some(serde_json::json!([{
|
Some(serde_json::json!([{
|
||||||
// "commentPattern": "no break"
|
"commentPattern": "no break"
|
||||||
// }])),
|
}])),
|
||||||
// ),
|
),
|
||||||
// ("switch(foo) { case 0: \n /* with comments */ \ncase 1: b(); }", None),
|
("switch(foo) { case 0: \n /* with comments */ \ncase 1: b(); }", None),
|
||||||
// (
|
(
|
||||||
// "switch(foo) { case 0:\n\ncase 1: b(); }",
|
"switch(foo) { case 0:\n\ncase 1: b(); }",
|
||||||
// Some(serde_json::json!([{
|
Some(serde_json::json!([{
|
||||||
// "allowEmptyCase": false
|
"allowEmptyCase": false
|
||||||
// }])),
|
}])),
|
||||||
// ),
|
),
|
||||||
// ("switch(foo) { case 0:\n\ncase 1: b(); }", Some(serde_json::json!([{}]))),
|
("switch(foo) { case 0:\n\ncase 1: b(); }", Some(serde_json::json!([{}]))),
|
||||||
// (
|
(
|
||||||
// "switch (a) { case 1: \n ; case 2: }",
|
"switch (a) { case 1: \n ; case 2: }",
|
||||||
// Some(serde_json::json!([{ "allowEmptyCase": false }])),
|
Some(serde_json::json!([{ "allowEmptyCase": false }])),
|
||||||
// ),
|
),
|
||||||
// (
|
(
|
||||||
// "switch (a) { case 1: ; case 2: ; case 3: }",
|
"switch (a) { case 1: ; case 2: ; case 3: }",
|
||||||
// Some(serde_json::json!([{ "allowEmptyCase": true }])),
|
Some(serde_json::json!([{ "allowEmptyCase": true }])),
|
||||||
// ),
|
),
|
||||||
// (
|
(
|
||||||
// "switch (foo) { case 0: a(); \n// eslint-enable no-fallthrough\n case 1: }",
|
"switch (foo) { case 0: a(); \n// eslint-enable no-fallthrough\n case 1: }",
|
||||||
// Some(serde_json::json!([{}])),
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
crates/oxc_linter/src/snapshots/no_fallthrough.snap
Normal file
177
crates/oxc_linter/src/snapshots/no_fallthrough.snap
Normal file
|
|
@ -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(); }
|
||||||
|
· ─────────────────────
|
||||||
|
╰────
|
||||||
Loading…
Reference in a new issue