Skip to main content

rustfmt_nightly/
matches.rs

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