rustfmt_nightly/
matches.rs

1//! Format match expression.
2
3use std::iter::repeat;
4
5use rustc_ast::{MatchKind, ast, ptr};
6use rustc_span::{BytePos, Span};
7use tracing::debug;
8
9use crate::comment::{FindUncommented, combine_strs_with_missing_comments, rewrite_comment};
10use crate::config::lists::*;
11use crate::config::{Config, ControlBraceStyle, IndentStyle, MatchArmLeadingPipe, StyleEdition};
12use crate::expr::{
13    ExprType, RhsTactics, format_expr, is_empty_block, is_simple_block, is_unsafe_block,
14    prefer_next_line, rewrite_cond,
15};
16use crate::lists::{ListFormatting, itemize_list, write_list};
17use crate::rewrite::{Rewrite, RewriteContext, RewriteError, RewriteErrorExt, RewriteResult};
18use crate::shape::Shape;
19use crate::source_map::SpanUtils;
20use crate::spanned::Spanned;
21use crate::utils::{
22    contains_skip, extra_offset, first_line_width, inner_attributes, last_line_extendable, mk_sp,
23    semicolon_for_expr, trimmed_last_line_width, unicode_str_width,
24};
25
26/// A simple wrapper type against `ast::Arm`. Used inside `write_list()`.
27struct ArmWrapper<'a> {
28    arm: &'a ast::Arm,
29    /// `true` if the arm is the last one in match expression. Used to decide on whether we should
30    /// add trailing comma to the match arm when `config.trailing_comma() == Never`.
31    is_last: bool,
32    /// Holds a byte position of `|` at the beginning of the arm pattern, if available.
33    beginning_vert: Option<BytePos>,
34}
35
36impl<'a> ArmWrapper<'a> {
37    fn new(arm: &'a ast::Arm, is_last: bool, beginning_vert: Option<BytePos>) -> ArmWrapper<'a> {
38        ArmWrapper {
39            arm,
40            is_last,
41            beginning_vert,
42        }
43    }
44}
45
46impl<'a> Spanned for ArmWrapper<'a> {
47    fn span(&self) -> Span {
48        if let Some(lo) = self.beginning_vert {
49            let lo = std::cmp::min(lo, self.arm.span().lo());
50            mk_sp(lo, self.arm.span().hi())
51        } else {
52            self.arm.span()
53        }
54    }
55}
56
57impl<'a> Rewrite for ArmWrapper<'a> {
58    fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> {
59        self.rewrite_result(context, shape).ok()
60    }
61
62    fn rewrite_result(&self, context: &RewriteContext<'_>, shape: Shape) -> RewriteResult {
63        rewrite_match_arm(
64            context,
65            self.arm,
66            shape,
67            self.is_last,
68            self.beginning_vert.is_some(),
69        )
70    }
71}
72
73pub(crate) fn rewrite_match(
74    context: &RewriteContext<'_>,
75    cond: &ast::Expr,
76    arms: &[ast::Arm],
77    shape: Shape,
78    span: Span,
79    attrs: &[ast::Attribute],
80    match_kind: MatchKind,
81) -> RewriteResult {
82    // Do not take the rhs overhead from the upper expressions into account
83    // when rewriting match condition.
84    let cond_shape = Shape {
85        width: context.budget(shape.used_width()),
86        ..shape
87    };
88    // 6 = `match `
89    let cond_shape = match context.config.indent_style() {
90        IndentStyle::Visual => cond_shape
91            .shrink_left(6)
92            .max_width_error(shape.width, span)?,
93        IndentStyle::Block => cond_shape
94            .offset_left(6)
95            .max_width_error(shape.width, span)?,
96    };
97    let cond_str = cond.rewrite_result(context, cond_shape)?;
98    let alt_block_sep = &shape.indent.to_string_with_newline(context.config);
99    let block_sep = match context.config.control_brace_style() {
100        ControlBraceStyle::AlwaysNextLine => alt_block_sep,
101        _ if last_line_extendable(&cond_str) => " ",
102        // 2 = ` {`
103        _ if cond_str.contains('\n') || cond_str.len() + 2 > cond_shape.width => alt_block_sep,
104        _ => " ",
105    };
106
107    let nested_indent_str = shape
108        .indent
109        .block_indent(context.config)
110        .to_string(context.config);
111    // Inner attributes.
112    let inner_attrs = &inner_attributes(attrs);
113    let inner_attrs_str = if inner_attrs.is_empty() {
114        String::new()
115    } else {
116        let shape = if context.config.style_edition() <= StyleEdition::Edition2021 {
117            shape
118        } else {
119            shape.block_indent(context.config.tab_spaces())
120        };
121        inner_attrs
122            .rewrite_result(context, shape)
123            .map(|s| format!("{}{}\n", nested_indent_str, s))?
124    };
125
126    let open_brace_pos = if inner_attrs.is_empty() {
127        let hi = if arms.is_empty() {
128            span.hi()
129        } else {
130            arms[0].span().lo()
131        };
132        context
133            .snippet_provider
134            .span_after(mk_sp(cond.span.hi(), hi), "{")
135    } else {
136        inner_attrs[inner_attrs.len() - 1].span.hi()
137    };
138
139    if arms.is_empty() {
140        let snippet = context.snippet(mk_sp(open_brace_pos, span.hi() - BytePos(1)));
141        if snippet.trim().is_empty() {
142            Ok(format!("match {cond_str} {{}}"))
143        } else {
144            // Empty match with comments or inner attributes? We are not going to bother, sorry ;)
145            Ok(context.snippet(span).to_owned())
146        }
147    } else {
148        let span_after_cond = mk_sp(cond.span.hi(), span.hi());
149
150        match match_kind {
151            MatchKind::Prefix => Ok(format!(
152                "match {}{}{{\n{}{}{}\n{}}}",
153                cond_str,
154                block_sep,
155                inner_attrs_str,
156                nested_indent_str,
157                rewrite_match_arms(context, arms, shape, span_after_cond, open_brace_pos)?,
158                shape.indent.to_string(context.config),
159            )),
160            MatchKind::Postfix => Ok(format!(
161                "{}.match{}{{\n{}{}{}\n{}}}",
162                cond_str,
163                block_sep,
164                inner_attrs_str,
165                nested_indent_str,
166                rewrite_match_arms(context, arms, shape, span_after_cond, open_brace_pos)?,
167                shape.indent.to_string(context.config),
168            )),
169        }
170    }
171}
172
173fn arm_comma(config: &Config, body: &ast::Expr, is_last: bool) -> &'static str {
174    if is_last && config.trailing_comma() == SeparatorTactic::Never {
175        ""
176    } else if config.match_block_trailing_comma() {
177        ","
178    } else if let ast::ExprKind::Block(ref block, _) = body.kind {
179        if let ast::BlockCheckMode::Default = block.rules {
180            ""
181        } else {
182            ","
183        }
184    } else {
185        ","
186    }
187}
188
189/// Collect a byte position of the beginning `|` for each arm, if available.
190fn collect_beginning_verts(
191    context: &RewriteContext<'_>,
192    arms: &[ast::Arm],
193) -> Vec<Option<BytePos>> {
194    arms.iter()
195        .map(|a| {
196            context
197                .snippet(a.pat.span)
198                .starts_with('|')
199                .then(|| a.pat.span().lo())
200        })
201        .collect()
202}
203
204fn rewrite_match_arms(
205    context: &RewriteContext<'_>,
206    arms: &[ast::Arm],
207    shape: Shape,
208    span: Span,
209    open_brace_pos: BytePos,
210) -> RewriteResult {
211    let arm_shape = shape
212        .block_indent(context.config.tab_spaces())
213        .with_max_width(context.config);
214
215    let arm_len = arms.len();
216    let is_last_iter = repeat(false)
217        .take(arm_len.saturating_sub(1))
218        .chain(repeat(true));
219    let beginning_verts = collect_beginning_verts(context, arms);
220    let items = itemize_list(
221        context.snippet_provider,
222        arms.iter()
223            .zip(is_last_iter)
224            .zip(beginning_verts.into_iter())
225            .map(|((arm, is_last), beginning_vert)| ArmWrapper::new(arm, is_last, beginning_vert)),
226        "}",
227        "|",
228        |arm| arm.span().lo(),
229        |arm| arm.span().hi(),
230        |arm| arm.rewrite_result(context, arm_shape),
231        open_brace_pos,
232        span.hi(),
233        false,
234    );
235    let arms_vec: Vec<_> = items.collect();
236    // We will add/remove commas inside `arm.rewrite()`, and hence no separator here.
237    let fmt = ListFormatting::new(arm_shape, context.config)
238        .separator("")
239        .preserve_newline(true);
240
241    write_list(&arms_vec, &fmt)
242}
243
244fn rewrite_match_arm(
245    context: &RewriteContext<'_>,
246    arm: &ast::Arm,
247    shape: Shape,
248    is_last: bool,
249    has_leading_pipe: bool,
250) -> RewriteResult {
251    let (missing_span, attrs_str) = if !arm.attrs.is_empty() {
252        if contains_skip(&arm.attrs) {
253            let (_, body) = flatten_arm_body(context, arm.body.as_deref().unknown_error()?, None);
254            // `arm.span()` does not include trailing comma, add it manually.
255            return Ok(format!(
256                "{}{}",
257                context.snippet(arm.span()),
258                arm_comma(context.config, body, is_last),
259            ));
260        }
261        let missing_span = mk_sp(arm.attrs[arm.attrs.len() - 1].span.hi(), arm.pat.span.lo());
262        (missing_span, arm.attrs.rewrite_result(context, shape)?)
263    } else {
264        (mk_sp(arm.span().lo(), arm.span().lo()), String::new())
265    };
266
267    // Leading pipe offset
268    // 2 = `| `
269    let (pipe_offset, pipe_str) = match context.config.match_arm_leading_pipes() {
270        MatchArmLeadingPipe::Never => (0, ""),
271        MatchArmLeadingPipe::Preserve if !has_leading_pipe => (0, ""),
272        MatchArmLeadingPipe::Preserve | MatchArmLeadingPipe::Always => (2, "| "),
273    };
274
275    // Patterns
276    let pat_shape = match &arm.body.as_ref().unknown_error()?.kind {
277        ast::ExprKind::Block(_, Some(label)) => {
278            // Some block with a label ` => 'label: {`
279            // 7 = ` => : {`
280            let label_len = label.ident.as_str().len();
281            shape
282                .sub_width(7 + label_len)
283                .and_then(|s| s.offset_left(pipe_offset))
284                .max_width_error(shape.width, arm.span)?
285        }
286        _ => {
287            // 5 = ` => {`
288            shape
289                .sub_width(5)
290                .and_then(|s| s.offset_left(pipe_offset))
291                .max_width_error(shape.width, arm.span)?
292        }
293    };
294    let pats_str = arm.pat.rewrite_result(context, pat_shape)?;
295
296    // Guard
297    let block_like_pat = trimmed_last_line_width(&pats_str) <= context.config.tab_spaces();
298    let new_line_guard = pats_str.contains('\n') && !block_like_pat;
299    let guard_str = rewrite_guard(
300        context,
301        &arm.guard,
302        shape,
303        trimmed_last_line_width(&pats_str),
304        new_line_guard,
305    )?;
306
307    let lhs_str = combine_strs_with_missing_comments(
308        context,
309        &attrs_str,
310        &format!("{pipe_str}{pats_str}{guard_str}"),
311        missing_span,
312        shape,
313        false,
314    )?;
315
316    let arrow_span = mk_sp(
317        arm.pat.span.hi(),
318        arm.body.as_ref().unknown_error()?.span().lo(),
319    );
320    rewrite_match_body(
321        context,
322        arm.body.as_ref().unknown_error()?,
323        &lhs_str,
324        shape,
325        guard_str.contains('\n'),
326        arrow_span,
327        is_last,
328    )
329}
330
331fn stmt_is_expr_mac(stmt: &ast::Stmt) -> bool {
332    if let ast::StmtKind::Expr(expr) = &stmt.kind {
333        if let ast::ExprKind::MacCall(_) = &expr.kind {
334            return true;
335        }
336    }
337    false
338}
339
340fn block_can_be_flattened<'a>(
341    context: &RewriteContext<'_>,
342    expr: &'a ast::Expr,
343) -> Option<&'a ast::Block> {
344    match expr.kind {
345        ast::ExprKind::Block(ref block, label)
346            if label.is_none()
347                && !is_unsafe_block(block)
348                && !context.inside_macro()
349                && is_simple_block(context, block, Some(&expr.attrs))
350                && !stmt_is_expr_mac(&block.stmts[0]) =>
351        {
352            Some(&*block)
353        }
354        _ => None,
355    }
356}
357
358// (extend, body)
359// @extend: true if the arm body can be put next to `=>`
360// @body: flattened body, if the body is block with a single expression
361fn flatten_arm_body<'a>(
362    context: &'a RewriteContext<'_>,
363    body: &'a ast::Expr,
364    opt_shape: Option<Shape>,
365) -> (bool, &'a ast::Expr) {
366    let can_extend =
367        |expr| !context.config.force_multiline_blocks() && can_flatten_block_around_this(expr);
368
369    if let Some(block) = block_can_be_flattened(context, body) {
370        if let ast::StmtKind::Expr(ref expr) = block.stmts[0].kind {
371            if let ast::ExprKind::Block(..) = expr.kind {
372                if expr.attrs.is_empty() {
373                    flatten_arm_body(context, expr, None)
374                } else {
375                    (true, body)
376                }
377            } else {
378                let cond_becomes_multi_line = opt_shape
379                    .and_then(|shape| rewrite_cond(context, expr, shape))
380                    .map_or(false, |cond| cond.contains('\n'));
381                if cond_becomes_multi_line {
382                    (false, &*body)
383                } else {
384                    (can_extend(expr), &*expr)
385                }
386            }
387        } else {
388            (false, &*body)
389        }
390    } else {
391        (can_extend(body), &*body)
392    }
393}
394
395fn rewrite_match_body(
396    context: &RewriteContext<'_>,
397    body: &ptr::P<ast::Expr>,
398    pats_str: &str,
399    shape: Shape,
400    has_guard: bool,
401    arrow_span: Span,
402    is_last: bool,
403) -> RewriteResult {
404    let (extend, body) = flatten_arm_body(
405        context,
406        body,
407        shape.offset_left(extra_offset(pats_str, shape) + 4),
408    );
409    let (is_block, is_empty_block) = if let ast::ExprKind::Block(ref block, _) = body.kind {
410        (true, is_empty_block(context, block, Some(&body.attrs)))
411    } else {
412        (false, false)
413    };
414
415    let comma = arm_comma(context.config, body, is_last);
416    let alt_block_sep = &shape.indent.to_string_with_newline(context.config);
417
418    let combine_orig_body = |body_str: &str| {
419        let block_sep = match context.config.control_brace_style() {
420            ControlBraceStyle::AlwaysNextLine if is_block => alt_block_sep,
421            _ => " ",
422        };
423
424        Ok(format!("{} =>{}{}{}", pats_str, block_sep, body_str, comma))
425    };
426
427    let next_line_indent = if !is_block || is_empty_block {
428        shape.indent.block_indent(context.config)
429    } else {
430        shape.indent
431    };
432
433    let forbid_same_line =
434        (has_guard && pats_str.contains('\n') && !is_empty_block) || !body.attrs.is_empty();
435
436    // Look for comments between `=>` and the start of the body.
437    let arrow_comment = {
438        let arrow_snippet = context.snippet(arrow_span).trim();
439        // search for the arrow starting from the end of the snippet since there may be a match
440        // expression within the guard
441        let arrow_index = if context.config.style_edition() <= StyleEdition::Edition2021 {
442            arrow_snippet.rfind("=>").unwrap()
443        } else {
444            arrow_snippet.find_last_uncommented("=>").unwrap()
445        };
446        // 2 = `=>`
447        let comment_str = arrow_snippet[arrow_index + 2..].trim();
448        if comment_str.is_empty() {
449            String::new()
450        } else {
451            rewrite_comment(comment_str, false, shape, context.config)?
452        }
453    };
454
455    let combine_next_line_body = |body_str: &str| {
456        let nested_indent_str = next_line_indent.to_string_with_newline(context.config);
457
458        if is_block {
459            let mut result = pats_str.to_owned();
460            result.push_str(" =>");
461            if !arrow_comment.is_empty() {
462                result.push_str(&nested_indent_str);
463                result.push_str(&arrow_comment);
464            }
465            result.push_str(&nested_indent_str);
466            result.push_str(body_str);
467            result.push_str(comma);
468            return Ok(result);
469        }
470
471        let indent_str = shape.indent.to_string_with_newline(context.config);
472        let (body_prefix, body_suffix) =
473            if context.config.match_arm_blocks() && !context.inside_macro() {
474                let comma = if context.config.match_block_trailing_comma() {
475                    ","
476                } else {
477                    ""
478                };
479                let semicolon = if context.config.style_edition() <= StyleEdition::Edition2021 {
480                    ""
481                } else {
482                    if semicolon_for_expr(context, body) {
483                        ";"
484                    } else {
485                        ""
486                    }
487                };
488                ("{", format!("{}{}}}{}", semicolon, indent_str, comma))
489            } else {
490                ("", String::from(","))
491            };
492
493        let block_sep = match context.config.control_brace_style() {
494            _ if body_prefix.is_empty() => "".to_owned(),
495            ControlBraceStyle::AlwaysNextLine => format!("{}{}", alt_block_sep, body_prefix),
496            _ if forbid_same_line || !arrow_comment.is_empty() => {
497                format!("{}{}", alt_block_sep, body_prefix)
498            }
499            _ => format!(" {}", body_prefix),
500        } + &nested_indent_str;
501
502        let mut result = pats_str.to_owned();
503        result.push_str(" =>");
504        if !arrow_comment.is_empty() {
505            result.push_str(&indent_str);
506            result.push_str(&arrow_comment);
507        }
508        result.push_str(&block_sep);
509        result.push_str(body_str);
510        result.push_str(&body_suffix);
511        Ok(result)
512    };
513
514    // Let's try and get the arm body on the same line as the condition.
515    // 4 = ` => `.len()
516    let orig_body_shape = shape
517        .offset_left(extra_offset(pats_str, shape) + 4)
518        .and_then(|shape| shape.sub_width(comma.len()));
519    let orig_body = if forbid_same_line || !arrow_comment.is_empty() {
520        Err(RewriteError::Unknown)
521    } else if let Some(body_shape) = orig_body_shape {
522        let rewrite = nop_block_collapse(
523            format_expr(body, ExprType::Statement, context, body_shape),
524            body_shape.width,
525        );
526
527        match rewrite {
528            Ok(ref body_str)
529                if is_block
530                    || (!body_str.contains('\n')
531                        && unicode_str_width(body_str) <= body_shape.width) =>
532            {
533                return combine_orig_body(body_str);
534            }
535            _ => rewrite,
536        }
537    } else {
538        Err(RewriteError::Unknown)
539    };
540    let orig_budget = orig_body_shape.map_or(0, |shape| shape.width);
541
542    // Try putting body on the next line and see if it looks better.
543    let next_line_body_shape = Shape::indented(next_line_indent, context.config);
544    let next_line_body = nop_block_collapse(
545        format_expr(body, ExprType::Statement, context, next_line_body_shape),
546        next_line_body_shape.width,
547    );
548    match (orig_body, next_line_body) {
549        (Ok(ref orig_str), Ok(ref next_line_str))
550            if prefer_next_line(orig_str, next_line_str, RhsTactics::Default) =>
551        {
552            combine_next_line_body(next_line_str)
553        }
554        (Ok(ref orig_str), _) if extend && first_line_width(orig_str) <= orig_budget => {
555            combine_orig_body(orig_str)
556        }
557        (Ok(ref orig_str), Ok(ref next_line_str)) if orig_str.contains('\n') => {
558            combine_next_line_body(next_line_str)
559        }
560        (Err(_), Ok(ref next_line_str)) => combine_next_line_body(next_line_str),
561        // When both orig_body and next_line_body result in errors, we currently propagate the
562        // error from the second attempt since it is more generous with width constraints.
563        // This decision is somewhat arbitrary and is open to change.
564        (Err(_), Err(next_line_err)) => Err(next_line_err),
565        (Ok(ref orig_str), _) => combine_orig_body(orig_str),
566    }
567}
568
569// The `if ...` guard on a match arm.
570fn rewrite_guard(
571    context: &RewriteContext<'_>,
572    guard: &Option<ptr::P<ast::Expr>>,
573    shape: Shape,
574    // The amount of space used up on this line for the pattern in
575    // the arm (excludes offset).
576    pattern_width: usize,
577    multiline_pattern: bool,
578) -> RewriteResult {
579    if let Some(ref guard) = *guard {
580        // First try to fit the guard string on the same line as the pattern.
581        // 4 = ` if `, 5 = ` => {`
582        let cond_shape = shape
583            .offset_left(pattern_width + 4)
584            .and_then(|s| s.sub_width(5));
585        if !multiline_pattern {
586            if let Some(cond_shape) = cond_shape {
587                if let Ok(cond_str) = guard.rewrite_result(context, cond_shape) {
588                    if !cond_str.contains('\n') || pattern_width <= context.config.tab_spaces() {
589                        return Ok(format!(" if {cond_str}"));
590                    }
591                }
592            }
593        }
594
595        // Not enough space to put the guard after the pattern, try a newline.
596        // 3 = `if `, 5 = ` => {`
597        let cond_shape = Shape::indented(shape.indent.block_indent(context.config), context.config)
598            .offset_left(3)
599            .and_then(|s| s.sub_width(5))
600            .max_width_error(shape.width, guard.span)?;
601        let cond_str = guard.rewrite_result(context, cond_shape)?;
602        Ok(format!(
603            "{}if {}",
604            cond_shape.indent.to_string_with_newline(context.config),
605            cond_str
606        ))
607    } else {
608        Ok(String::new())
609    }
610}
611
612fn nop_block_collapse(block_str: RewriteResult, budget: usize) -> RewriteResult {
613    debug!("nop_block_collapse {:?} {}", block_str, budget);
614    block_str.map(|block_str| {
615        if block_str.starts_with('{')
616            && budget >= 2
617            && (block_str[1..].find(|c: char| !c.is_whitespace()).unwrap() == block_str.len() - 2)
618        {
619            String::from("{}")
620        } else {
621            block_str
622        }
623    })
624}
625
626fn can_flatten_block_around_this(body: &ast::Expr) -> bool {
627    match body.kind {
628        // We do not allow `if` to stay on the same line, since we could easily mistake
629        // `pat => if cond { ... }` and `pat if cond => { ... }`.
630        ast::ExprKind::If(..) => false,
631        // We do not allow collapsing a block around expression with condition
632        // to avoid it being cluttered with match arm.
633        ast::ExprKind::ForLoop { .. } | ast::ExprKind::While(..) => false,
634        ast::ExprKind::Loop(..)
635        | ast::ExprKind::Match(..)
636        | ast::ExprKind::Block(..)
637        | ast::ExprKind::Closure(..)
638        | ast::ExprKind::Array(..)
639        | ast::ExprKind::Call(..)
640        | ast::ExprKind::MethodCall(..)
641        | ast::ExprKind::MacCall(..)
642        | ast::ExprKind::Struct(..)
643        | ast::ExprKind::Tup(..) => true,
644        ast::ExprKind::AddrOf(_, _, ref expr)
645        | ast::ExprKind::Try(ref expr)
646        | ast::ExprKind::Unary(_, ref expr)
647        | ast::ExprKind::Index(ref expr, _, _)
648        | ast::ExprKind::Cast(ref expr, _) => can_flatten_block_around_this(expr),
649        _ => false,
650    }
651}