use super::*;
use crate::error::Applicability;
struct Link<'e> {
scrutinee: &'e Expr,
pattern: &'e Pattern,
continuation: &'e MatchBody,
fallback_pattern: &'e Pattern,
fallback: &'e MatchBody,
}
fn continuing(pattern: &Pattern) -> Option<&'static str> {
let Pattern::Variant {
type_name: None,
variant,
bindings,
..
} = pattern
else {
return None;
};
let [binding] = bindings.as_slice() else {
return None;
};
if !matches!(
binding.pattern(),
Pattern::Binding(_) | Pattern::Wildcard(_)
) {
return None;
}
match variant.name.as_str() {
"Some" => Some("None"),
"Ok" => Some("Err"),
_ => None,
}
}
fn is_fallback(pattern: &Pattern, want: &str) -> bool {
match pattern {
Pattern::Wildcard(_) => true,
Pattern::Variant {
type_name: None,
variant,
bindings,
..
} if variant.name == want => match bindings.as_slice() {
[] => want == "None",
[b] => {
want == "Err" && matches!(b.pattern(), Pattern::Binding(_) | Pattern::Wildcard(_))
}
_ => false,
},
_ => false,
}
}
fn link(m: &Expr) -> Option<Link<'_>> {
if m.is_let_else_match() {
return None;
}
let ExprKind::Match { discriminant, arms } = &m.kind else {
return None;
};
let [a, b] = arms.as_slice() else {
return None;
};
if a.guard.is_some() || b.guard.is_some() {
return None;
}
let (cont, fallback) = match (continuing(&a.pattern), continuing(&b.pattern)) {
(Some(want), None) if is_fallback(&b.pattern, want) => (a, b),
(None, Some(want)) if is_fallback(&a.pattern, want) => (b, a),
_ => return None,
};
Some(Link {
scrutinee: discriminant,
pattern: &cont.pattern,
continuation: &cont.body,
fallback_pattern: &fallback.pattern,
fallback: &fallback.body,
})
}
fn next_expr<'e>(l: &Link<'e>) -> Option<&'e Expr> {
match l.continuation {
MatchBody::Expr(e) => Some(e),
MatchBody::Block(b) => Some(&b.tail),
}
}
fn chain(m: &Expr) -> Vec<(&Expr, Link<'_>)> {
let mut out = Vec::new();
let mut cur = Some(m);
while let Some(e) = cur {
let Some(l) = link(e) else { break };
cur = next_expr(&l);
out.push((e, l));
}
out
}
impl<'a> Parser<'a> {
pub(crate) fn check_staircase(&mut self, tail: &Expr) {
let steps = chain(tail);
if steps.len() < 2 {
return;
}
let inner: Vec<usize> = steps.iter().skip(1).map(|(e, _)| e.span.start).collect();
self.warnings
.retain(|w| w.category != STAIRCASE_CODE || !inner.contains(&w.span.start));
let head = tail.span.start;
let span = Span::new_in(tail.span.file, head, steps[0].1.scrutinee.span.end);
let mut err = CompileError::new(
STAIRCASE_CODE,
span,
format!(
"these {} nested `match`es can be flattened with `let … else`",
steps.len()
),
)
.with_note(
"each `match` has one arm that carries on and one that gives up; \
`let P = e else { … }` says that without the nesting",
);
if let Some(flat) = self.flatten(&steps, tail.span) {
err = err.with_suggestion(
"flatten the nested `match`es into `let … else`",
vec![(tail.span, flat)],
Applicability::MachineApplicable,
);
}
self.warnings.push(err);
}
fn flatten(&self, steps: &[(&Expr, Link<'_>)], whole: Span) -> Option<String> {
if self.slice(whole).contains("//") {
return None;
}
let base = self.indent_of(whole.start);
let mut out = String::new();
let mut first = true;
let line = |out: &mut String, first: &mut bool, text: &str| {
if !*first {
out.push('\n');
if !text.is_empty() {
out.push_str(&base);
}
}
*first = false;
out.push_str(text);
};
for (i, (_, l)) in steps.iter().enumerate() {
let pat = self.slice(l.pattern.span());
let scrut = self.slice(l.scrutinee.span);
let binds = matches!(
l.fallback_pattern,
Pattern::Variant { bindings, .. }
if bindings.iter().any(|b| matches!(b.pattern(), Pattern::Binding(_)))
);
let fallback = match (binds, l.fallback) {
(false, MatchBody::Expr(e)) => {
format!("{{ {} }}", self.joined(self.slice(e.span), &base))
}
(false, MatchBody::Block(b)) => self.joined(self.slice(b.span), &base),
(true, body) => {
let body = match body {
MatchBody::Expr(e) => self.joined(self.slice(e.span), &base),
MatchBody::Block(b) => self.joined(self.slice(b.span), &base),
};
format!(
"match {{ {} => {} }}",
self.slice(l.fallback_pattern.span()),
body
)
}
};
line(
&mut out,
&mut first,
&format!("let {pat} = {scrut} else {fallback}"),
);
let last = i + 1 == steps.len();
match l.continuation {
MatchBody::Block(b) => {
if b.implicit_tail {
return None;
}
let prefix = &self.source[b.span.start + 1..b.tail.span.start];
for text in self.reindent(prefix) {
line(&mut out, &mut first, &text);
}
if last {
for text in self.reindent(self.slice(b.tail.span)) {
line(&mut out, &mut first, &text);
}
}
}
MatchBody::Expr(e) => {
if last {
for text in self.reindent(self.slice(e.span)) {
line(&mut out, &mut first, &text);
}
}
}
}
}
Some(out)
}
fn joined(&self, text: &str, base: &str) -> String {
let mut out = String::new();
for (i, line) in self.reindent(text).iter().enumerate() {
if i > 0 {
out.push('\n');
if !line.is_empty() {
out.push_str(base);
}
}
out.push_str(line);
}
out
}
fn indent_of(&self, at: usize) -> String {
let start = self.source[..at].rfind('\n').map_or(0, |i| i + 1);
self.source[start..at]
.chars()
.take_while(|c| c.is_whitespace())
.collect()
}
fn reindent(&self, text: &str) -> Vec<String> {
let text = text.trim();
let lines: Vec<&str> = text.lines().collect();
let common = lines
.iter()
.skip(1)
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
lines
.iter()
.enumerate()
.map(|(i, l)| {
if i == 0 {
l.trim_start().to_string()
} else {
l.chars().skip(common).collect()
}
})
.collect()
}
}
pub(crate) const STAIRCASE_CODE: &str = "bynk.parse.let_else_staircase";