rustfmt_nightly/
utils.rs

1use std::borrow::Cow;
2
3use rustc_ast::ast::{
4    self, Attribute, MetaItem, MetaItemInner, MetaItemKind, NodeId, Path, Visibility,
5    VisibilityKind,
6};
7use rustc_ast::ptr;
8use rustc_ast_pretty::pprust;
9use rustc_span::{BytePos, LocalExpnId, Span, Symbol, SyntaxContext, sym, symbol};
10use unicode_width::UnicodeWidthStr;
11
12use crate::comment::{CharClasses, FullCodeCharKind, LineClasses, filter_normal_code};
13use crate::config::{Config, StyleEdition};
14use crate::rewrite::RewriteContext;
15use crate::shape::{Indent, Shape};
16
17#[inline]
18pub(crate) fn depr_skip_annotation() -> Symbol {
19    Symbol::intern("rustfmt_skip")
20}
21
22#[inline]
23pub(crate) fn skip_annotation() -> Symbol {
24    Symbol::intern("rustfmt::skip")
25}
26
27pub(crate) fn rewrite_ident<'a>(context: &'a RewriteContext<'_>, ident: symbol::Ident) -> &'a str {
28    context.snippet(ident.span)
29}
30
31// Computes the length of a string's last line, minus offset.
32pub(crate) fn extra_offset(text: &str, shape: Shape) -> usize {
33    match text.rfind('\n') {
34        // 1 for newline character
35        Some(idx) => text.len().saturating_sub(idx + 1 + shape.used_width()),
36        None => text.len(),
37    }
38}
39
40pub(crate) fn is_same_visibility(a: &Visibility, b: &Visibility) -> bool {
41    match (&a.kind, &b.kind) {
42        (
43            VisibilityKind::Restricted { path: p, .. },
44            VisibilityKind::Restricted { path: q, .. },
45        ) => pprust::path_to_string(p) == pprust::path_to_string(q),
46        (VisibilityKind::Public, VisibilityKind::Public)
47        | (VisibilityKind::Inherited, VisibilityKind::Inherited) => true,
48        _ => false,
49    }
50}
51
52// Uses Cow to avoid allocating in the common cases.
53pub(crate) fn format_visibility(
54    context: &RewriteContext<'_>,
55    vis: &Visibility,
56) -> Cow<'static, str> {
57    match vis.kind {
58        VisibilityKind::Public => Cow::from("pub "),
59        VisibilityKind::Inherited => Cow::from(""),
60        VisibilityKind::Restricted { ref path, .. } => {
61            let Path { ref segments, .. } = **path;
62            let mut segments_iter = segments.iter().map(|seg| rewrite_ident(context, seg.ident));
63            if path.is_global() {
64                segments_iter
65                    .next()
66                    .expect("Non-global path in pub(restricted)?");
67            }
68            let is_keyword = |s: &str| s == "crate" || s == "self" || s == "super";
69            let path = segments_iter.collect::<Vec<_>>().join("::");
70            let in_str = if is_keyword(&path) { "" } else { "in " };
71
72            Cow::from(format!("pub({in_str}{path}) "))
73        }
74    }
75}
76
77#[inline]
78pub(crate) fn format_coro(coroutine_kind: &ast::CoroutineKind) -> &'static str {
79    match coroutine_kind {
80        ast::CoroutineKind::Async { .. } => "async ",
81        ast::CoroutineKind::Gen { .. } => "gen ",
82        ast::CoroutineKind::AsyncGen { .. } => "async gen ",
83    }
84}
85
86#[inline]
87pub(crate) fn format_constness(constness: ast::Const) -> &'static str {
88    match constness {
89        ast::Const::Yes(..) => "const ",
90        ast::Const::No => "",
91    }
92}
93
94#[inline]
95pub(crate) fn format_constness_right(constness: ast::Const) -> &'static str {
96    match constness {
97        ast::Const::Yes(..) => " const",
98        ast::Const::No => "",
99    }
100}
101
102#[inline]
103pub(crate) fn format_defaultness(defaultness: ast::Defaultness) -> &'static str {
104    match defaultness {
105        ast::Defaultness::Default(..) => "default ",
106        ast::Defaultness::Final => "",
107    }
108}
109
110#[inline]
111pub(crate) fn format_safety(unsafety: ast::Safety) -> &'static str {
112    match unsafety {
113        ast::Safety::Unsafe(..) => "unsafe ",
114        ast::Safety::Safe(..) => "safe ",
115        ast::Safety::Default => "",
116    }
117}
118
119#[inline]
120pub(crate) fn format_auto(is_auto: ast::IsAuto) -> &'static str {
121    match is_auto {
122        ast::IsAuto::Yes => "auto ",
123        ast::IsAuto::No => "",
124    }
125}
126
127#[inline]
128pub(crate) fn format_mutability(mutability: ast::Mutability) -> &'static str {
129    match mutability {
130        ast::Mutability::Mut => "mut ",
131        ast::Mutability::Not => "",
132    }
133}
134
135#[inline]
136pub(crate) fn format_extern(ext: ast::Extern, explicit_abi: bool) -> Cow<'static, str> {
137    match ext {
138        ast::Extern::None => Cow::from(""),
139        ast::Extern::Implicit(_) if explicit_abi => Cow::from("extern \"C\" "),
140        ast::Extern::Implicit(_) => Cow::from("extern "),
141        // turn `extern "C"` into `extern` when `explicit_abi` is set to false
142        ast::Extern::Explicit(abi, _) if abi.symbol_unescaped == sym::C && !explicit_abi => {
143            Cow::from("extern ")
144        }
145        ast::Extern::Explicit(abi, _) => {
146            Cow::from(format!(r#"extern "{}" "#, abi.symbol_unescaped))
147        }
148    }
149}
150
151#[inline]
152// Transform `Vec<rustc_ast::ptr::P<T>>` into `Vec<&T>`
153pub(crate) fn ptr_vec_to_ref_vec<T>(vec: &[ptr::P<T>]) -> Vec<&T> {
154    vec.iter().map(|x| &**x).collect::<Vec<_>>()
155}
156
157#[inline]
158pub(crate) fn filter_attributes(
159    attrs: &[ast::Attribute],
160    style: ast::AttrStyle,
161) -> Vec<ast::Attribute> {
162    attrs
163        .iter()
164        .filter(|a| a.style == style)
165        .cloned()
166        .collect::<Vec<_>>()
167}
168
169#[inline]
170pub(crate) fn inner_attributes(attrs: &[ast::Attribute]) -> Vec<ast::Attribute> {
171    filter_attributes(attrs, ast::AttrStyle::Inner)
172}
173
174#[inline]
175pub(crate) fn outer_attributes(attrs: &[ast::Attribute]) -> Vec<ast::Attribute> {
176    filter_attributes(attrs, ast::AttrStyle::Outer)
177}
178
179#[inline]
180pub(crate) fn is_single_line(s: &str) -> bool {
181    !s.chars().any(|c| c == '\n')
182}
183
184#[inline]
185pub(crate) fn first_line_contains_single_line_comment(s: &str) -> bool {
186    s.lines().next().map_or(false, |l| l.contains("//"))
187}
188
189#[inline]
190pub(crate) fn last_line_contains_single_line_comment(s: &str) -> bool {
191    s.lines().last().map_or(false, |l| l.contains("//"))
192}
193
194#[inline]
195pub(crate) fn is_attributes_extendable(attrs_str: &str) -> bool {
196    !attrs_str.contains('\n') && !last_line_contains_single_line_comment(attrs_str)
197}
198
199/// The width of the first line in s.
200#[inline]
201pub(crate) fn first_line_width(s: &str) -> usize {
202    unicode_str_width(s.splitn(2, '\n').next().unwrap_or(""))
203}
204
205/// The width of the last line in s.
206#[inline]
207pub(crate) fn last_line_width(s: &str) -> usize {
208    unicode_str_width(s.rsplitn(2, '\n').next().unwrap_or(""))
209}
210
211/// The total used width of the last line.
212#[inline]
213pub(crate) fn last_line_used_width(s: &str, offset: usize) -> usize {
214    if s.contains('\n') {
215        last_line_width(s)
216    } else {
217        offset + unicode_str_width(s)
218    }
219}
220
221#[inline]
222pub(crate) fn trimmed_last_line_width(s: &str) -> usize {
223    unicode_str_width(match s.rfind('\n') {
224        Some(n) => s[(n + 1)..].trim(),
225        None => s.trim(),
226    })
227}
228
229#[inline]
230pub(crate) fn last_line_extendable(s: &str) -> bool {
231    if s.ends_with("\"#") {
232        return true;
233    }
234    for c in s.chars().rev() {
235        match c {
236            '(' | ')' | ']' | '}' | '?' | '>' => continue,
237            '\n' => break,
238            _ if c.is_whitespace() => continue,
239            _ => return false,
240        }
241    }
242    true
243}
244
245#[inline]
246fn is_skip(meta_item: &MetaItem) -> bool {
247    match meta_item.kind {
248        MetaItemKind::Word => {
249            let path_str = pprust::path_to_string(&meta_item.path);
250            path_str == skip_annotation().as_str() || path_str == depr_skip_annotation().as_str()
251        }
252        MetaItemKind::List(ref l) => {
253            meta_item.has_name(sym::cfg_attr) && l.len() == 2 && is_skip_nested(&l[1])
254        }
255        _ => false,
256    }
257}
258
259#[inline]
260fn is_skip_nested(meta_item: &MetaItemInner) -> bool {
261    match meta_item {
262        MetaItemInner::MetaItem(ref mi) => is_skip(mi),
263        MetaItemInner::Lit(_) => false,
264    }
265}
266
267#[inline]
268pub(crate) fn contains_skip(attrs: &[Attribute]) -> bool {
269    attrs
270        .iter()
271        .any(|a| a.meta().map_or(false, |a| is_skip(&a)))
272}
273
274#[inline]
275pub(crate) fn semicolon_for_expr(context: &RewriteContext<'_>, expr: &ast::Expr) -> bool {
276    // Never try to insert semicolons on expressions when we're inside
277    // a macro definition - this can prevent the macro from compiling
278    // when used in expression position
279    if context.is_macro_def {
280        return false;
281    }
282
283    match expr.kind {
284        ast::ExprKind::Ret(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Break(..) => {
285            context.config.trailing_semicolon()
286        }
287        _ => false,
288    }
289}
290
291#[inline]
292pub(crate) fn semicolon_for_stmt(
293    context: &RewriteContext<'_>,
294    stmt: &ast::Stmt,
295    is_last_expr: bool,
296) -> bool {
297    match stmt.kind {
298        ast::StmtKind::Semi(ref expr) => match expr.kind {
299            ast::ExprKind::While(..) | ast::ExprKind::Loop(..) | ast::ExprKind::ForLoop { .. } => {
300                false
301            }
302            ast::ExprKind::Break(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Ret(..) => {
303                // The only time we can skip the semi-colon is if the config option is set to false
304                // **and** this is the last expr (even though any following exprs are unreachable)
305                context.config.trailing_semicolon() || !is_last_expr
306            }
307            _ => true,
308        },
309        ast::StmtKind::Expr(..) => false,
310        _ => true,
311    }
312}
313
314#[inline]
315pub(crate) fn stmt_expr(stmt: &ast::Stmt) -> Option<&ast::Expr> {
316    match stmt.kind {
317        ast::StmtKind::Expr(ref expr) => Some(expr),
318        _ => None,
319    }
320}
321
322/// Returns the number of LF and CRLF respectively.
323pub(crate) fn count_lf_crlf(input: &str) -> (usize, usize) {
324    let mut lf = 0;
325    let mut crlf = 0;
326    let mut is_crlf = false;
327    for c in input.as_bytes() {
328        match c {
329            b'\r' => is_crlf = true,
330            b'\n' if is_crlf => crlf += 1,
331            b'\n' => lf += 1,
332            _ => is_crlf = false,
333        }
334    }
335    (lf, crlf)
336}
337
338pub(crate) fn count_newlines(input: &str) -> usize {
339    // Using bytes to omit UTF-8 decoding
340    bytecount::count(input.as_bytes(), b'\n')
341}
342
343// For format_missing and last_pos, need to use the source callsite (if applicable).
344// Required as generated code spans aren't guaranteed to follow on from the last span.
345macro_rules! source {
346    ($this:ident, $sp:expr) => {
347        $sp.source_callsite()
348    };
349}
350
351pub(crate) fn mk_sp(lo: BytePos, hi: BytePos) -> Span {
352    Span::new(lo, hi, SyntaxContext::root(), None)
353}
354
355pub(crate) fn mk_sp_lo_plus_one(lo: BytePos) -> Span {
356    Span::new(lo, lo + BytePos(1), SyntaxContext::root(), None)
357}
358
359// Returns `true` if the given span does not intersect with file lines.
360macro_rules! out_of_file_lines_range {
361    ($self:ident, $span:expr) => {
362        !$self.config.file_lines().is_all()
363            && !$self
364                .config
365                .file_lines()
366                .intersects(&$self.psess.lookup_line_range($span))
367    };
368}
369
370macro_rules! skip_out_of_file_lines_range_err {
371    ($self:ident, $span:expr) => {
372        if out_of_file_lines_range!($self, $span) {
373            return Err(RewriteError::SkipFormatting);
374        }
375    };
376}
377
378macro_rules! skip_out_of_file_lines_range_visitor {
379    ($self:ident, $span:expr) => {
380        if out_of_file_lines_range!($self, $span) {
381            $self.push_rewrite($span, None);
382            return;
383        }
384    };
385}
386
387// Wraps String in an Option. Returns Some when the string adheres to the
388// Rewrite constraints defined for the Rewrite trait and None otherwise.
389pub(crate) fn wrap_str(s: String, max_width: usize, shape: Shape) -> Option<String> {
390    if filtered_str_fits(&s, max_width, shape) {
391        Some(s)
392    } else {
393        None
394    }
395}
396
397pub(crate) fn filtered_str_fits(snippet: &str, max_width: usize, shape: Shape) -> bool {
398    let snippet = &filter_normal_code(snippet);
399    if !snippet.is_empty() {
400        // First line must fits with `shape.width`.
401        if first_line_width(snippet) > shape.width {
402            return false;
403        }
404        // If the snippet does not include newline, we are done.
405        if is_single_line(snippet) {
406            return true;
407        }
408        // The other lines must fit within the maximum width.
409        if snippet
410            .lines()
411            .skip(1)
412            .any(|line| unicode_str_width(line) > max_width)
413        {
414            return false;
415        }
416        // A special check for the last line, since the caller may
417        // place trailing characters on this line.
418        if last_line_width(snippet) > shape.used_width() + shape.width {
419            return false;
420        }
421    }
422    true
423}
424
425#[inline]
426pub(crate) fn colon_spaces(config: &Config) -> &'static str {
427    let before = config.space_before_colon();
428    let after = config.space_after_colon();
429    match (before, after) {
430        (true, true) => " : ",
431        (true, false) => " :",
432        (false, true) => ": ",
433        (false, false) => ":",
434    }
435}
436
437#[inline]
438pub(crate) fn left_most_sub_expr(e: &ast::Expr) -> &ast::Expr {
439    match e.kind {
440        ast::ExprKind::Call(ref e, _)
441        | ast::ExprKind::Binary(_, ref e, _)
442        | ast::ExprKind::Cast(ref e, _)
443        | ast::ExprKind::Type(ref e, _)
444        | ast::ExprKind::Assign(ref e, _, _)
445        | ast::ExprKind::AssignOp(_, ref e, _)
446        | ast::ExprKind::Field(ref e, _)
447        | ast::ExprKind::Index(ref e, _, _)
448        | ast::ExprKind::Range(Some(ref e), _, _)
449        | ast::ExprKind::Try(ref e) => left_most_sub_expr(e),
450        _ => e,
451    }
452}
453
454#[inline]
455pub(crate) fn starts_with_newline(s: &str) -> bool {
456    s.starts_with('\n') || s.starts_with("\r\n")
457}
458
459#[inline]
460pub(crate) fn first_line_ends_with(s: &str, c: char) -> bool {
461    s.lines().next().map_or(false, |l| l.ends_with(c))
462}
463
464// States whether an expression's last line exclusively consists of closing
465// parens, braces, and brackets in its idiomatic formatting.
466pub(crate) fn is_block_expr(context: &RewriteContext<'_>, expr: &ast::Expr, repr: &str) -> bool {
467    match expr.kind {
468        ast::ExprKind::MacCall(..)
469        | ast::ExprKind::FormatArgs(..)
470        | ast::ExprKind::Call(..)
471        | ast::ExprKind::MethodCall(..)
472        | ast::ExprKind::Array(..)
473        | ast::ExprKind::Struct(..)
474        | ast::ExprKind::While(..)
475        | ast::ExprKind::If(..)
476        | ast::ExprKind::Block(..)
477        | ast::ExprKind::ConstBlock(..)
478        | ast::ExprKind::Gen(..)
479        | ast::ExprKind::Loop(..)
480        | ast::ExprKind::ForLoop { .. }
481        | ast::ExprKind::TryBlock(..)
482        | ast::ExprKind::Match(..) => repr.contains('\n'),
483        ast::ExprKind::Paren(ref expr)
484        | ast::ExprKind::Binary(_, _, ref expr)
485        | ast::ExprKind::Index(_, ref expr, _)
486        | ast::ExprKind::Unary(_, ref expr)
487        | ast::ExprKind::Try(ref expr)
488        | ast::ExprKind::Yield(Some(ref expr)) => is_block_expr(context, expr, repr),
489        ast::ExprKind::Closure(ref closure) => is_block_expr(context, &closure.body, repr),
490        // This can only be a string lit
491        ast::ExprKind::Lit(_) => {
492            repr.contains('\n') && trimmed_last_line_width(repr) <= context.config.tab_spaces()
493        }
494        ast::ExprKind::AddrOf(..)
495        | ast::ExprKind::Assign(..)
496        | ast::ExprKind::AssignOp(..)
497        | ast::ExprKind::Await(..)
498        | ast::ExprKind::Break(..)
499        | ast::ExprKind::Cast(..)
500        | ast::ExprKind::Continue(..)
501        | ast::ExprKind::Dummy
502        | ast::ExprKind::Err(_)
503        | ast::ExprKind::Field(..)
504        | ast::ExprKind::IncludedBytes(..)
505        | ast::ExprKind::InlineAsm(..)
506        | ast::ExprKind::OffsetOf(..)
507        | ast::ExprKind::UnsafeBinderCast(..)
508        | ast::ExprKind::Let(..)
509        | ast::ExprKind::Path(..)
510        | ast::ExprKind::Range(..)
511        | ast::ExprKind::Repeat(..)
512        | ast::ExprKind::Ret(..)
513        | ast::ExprKind::Become(..)
514        | ast::ExprKind::Yeet(..)
515        | ast::ExprKind::Tup(..)
516        | ast::ExprKind::Type(..)
517        | ast::ExprKind::Yield(None)
518        | ast::ExprKind::Underscore => false,
519    }
520}
521
522/// Removes trailing spaces from the specified snippet. We do not remove spaces
523/// inside strings or comments.
524pub(crate) fn remove_trailing_white_spaces(text: &str) -> String {
525    let mut buffer = String::with_capacity(text.len());
526    let mut space_buffer = String::with_capacity(128);
527    for (char_kind, c) in CharClasses::new(text.chars()) {
528        match c {
529            '\n' => {
530                if char_kind == FullCodeCharKind::InString {
531                    buffer.push_str(&space_buffer);
532                }
533                space_buffer.clear();
534                buffer.push('\n');
535            }
536            _ if c.is_whitespace() => {
537                space_buffer.push(c);
538            }
539            _ => {
540                if !space_buffer.is_empty() {
541                    buffer.push_str(&space_buffer);
542                    space_buffer.clear();
543                }
544                buffer.push(c);
545            }
546        }
547    }
548    buffer
549}
550
551/// Indent each line according to the specified `indent`.
552/// e.g.
553///
554/// ```rust,compile_fail
555/// foo!{
556/// x,
557/// y,
558/// foo(
559///     a,
560///     b,
561///     c,
562/// ),
563/// }
564/// ```
565///
566/// will become
567///
568/// ```rust,compile_fail
569/// foo!{
570///     x,
571///     y,
572///     foo(
573///         a,
574///         b,
575///         c,
576///     ),
577/// }
578/// ```
579pub(crate) fn trim_left_preserve_layout(
580    orig: &str,
581    indent: Indent,
582    config: &Config,
583) -> Option<String> {
584    let mut lines = LineClasses::new(orig);
585    let first_line = lines.next().map(|(_, s)| s.trim_end().to_owned())?;
586    let mut trimmed_lines = Vec::with_capacity(16);
587
588    let mut veto_trim = false;
589    let min_prefix_space_width = lines
590        .filter_map(|(kind, line)| {
591            let mut trimmed = true;
592            let prefix_space_width = if is_empty_line(&line) {
593                None
594            } else {
595                Some(get_prefix_space_width(config, &line))
596            };
597
598            // just InString{Commented} in order to allow the start of a string to be indented
599            let new_veto_trim_value = (kind == FullCodeCharKind::InString
600                || (config.style_edition() >= StyleEdition::Edition2024
601                    && kind == FullCodeCharKind::InStringCommented))
602                && !line.ends_with('\\');
603            let line = if veto_trim || new_veto_trim_value {
604                veto_trim = new_veto_trim_value;
605                trimmed = false;
606                line
607            } else {
608                line.trim().to_owned()
609            };
610            trimmed_lines.push((trimmed, line, prefix_space_width));
611
612            // Because there is a veto against trimming and indenting lines within a string,
613            // such lines should not be taken into account when computing the minimum.
614            match kind {
615                FullCodeCharKind::InStringCommented | FullCodeCharKind::EndStringCommented
616                    if config.style_edition() >= StyleEdition::Edition2024 =>
617                {
618                    None
619                }
620                FullCodeCharKind::InString | FullCodeCharKind::EndString => None,
621                _ => prefix_space_width,
622            }
623        })
624        .min()?;
625
626    Some(
627        first_line
628            + "\n"
629            + &trimmed_lines
630                .iter()
631                .map(
632                    |&(trimmed, ref line, prefix_space_width)| match prefix_space_width {
633                        _ if !trimmed => line.to_owned(),
634                        Some(original_indent_width) => {
635                            let new_indent_width = indent.width()
636                                + original_indent_width.saturating_sub(min_prefix_space_width);
637                            let new_indent = Indent::from_width(config, new_indent_width);
638                            format!("{}{}", new_indent.to_string(config), line)
639                        }
640                        None => String::new(),
641                    },
642                )
643                .collect::<Vec<_>>()
644                .join("\n"),
645    )
646}
647
648/// Based on the given line, determine if the next line can be indented or not.
649/// This allows to preserve the indentation of multi-line literals when
650/// re-inserted a code block that has been formatted separately from the rest
651/// of the code, such as code in macro defs or code blocks doc comments.
652pub(crate) fn indent_next_line(kind: FullCodeCharKind, line: &str, config: &Config) -> bool {
653    if kind.is_string() {
654        // If the string ends with '\', the string has been wrapped over
655        // multiple lines. If `format_strings = true`, then the indentation of
656        // strings wrapped over multiple lines will have been adjusted while
657        // formatting the code block, therefore the string's indentation needs
658        // to be adjusted for the code surrounding the code block.
659        config.format_strings() && line.ends_with('\\')
660    } else if config.style_edition() >= StyleEdition::Edition2024 {
661        !kind.is_commented_string()
662    } else {
663        true
664    }
665}
666
667pub(crate) fn is_empty_line(s: &str) -> bool {
668    s.is_empty() || s.chars().all(char::is_whitespace)
669}
670
671fn get_prefix_space_width(config: &Config, s: &str) -> usize {
672    let mut width = 0;
673    for c in s.chars() {
674        match c {
675            ' ' => width += 1,
676            '\t' => width += config.tab_spaces(),
677            _ => return width,
678        }
679    }
680    width
681}
682
683pub(crate) trait NodeIdExt {
684    fn root() -> Self;
685}
686
687impl NodeIdExt for NodeId {
688    fn root() -> NodeId {
689        NodeId::placeholder_from_expn_id(LocalExpnId::ROOT)
690    }
691}
692
693pub(crate) fn unicode_str_width(s: &str) -> usize {
694    s.width()
695}
696
697#[cfg(test)]
698mod test {
699    use super::*;
700
701    #[test]
702    fn test_remove_trailing_white_spaces() {
703        let s = "    r#\"\n        test\n    \"#";
704        assert_eq!(remove_trailing_white_spaces(s), s);
705    }
706
707    #[test]
708    fn test_trim_left_preserve_layout() {
709        let s = "aaa\n\tbbb\n    ccc";
710        let config = Config::default();
711        let indent = Indent::new(4, 0);
712        assert_eq!(
713            trim_left_preserve_layout(s, indent, &config),
714            Some("aaa\n    bbb\n    ccc".to_string())
715        );
716    }
717}