Skip to main content

rustfmt_nightly/
comment.rs

1// Formatting and tools for comments.
2
3use std::{borrow::Cow, iter};
4
5use itertools::{Itertools as _, MultiPeek, multipeek};
6use rustc_span::Span;
7use tracing::{debug, trace};
8
9use crate::config::Config;
10use crate::rewrite::{RewriteContext, RewriteErrorExt, RewriteResult};
11use crate::shape::{Indent, Shape};
12use crate::string::{StringFormat, rewrite_string};
13use crate::utils::{
14    count_newlines, first_line_width, last_line_width, trim_left_preserve_layout,
15    trimmed_last_line_width, unicode_str_width,
16};
17use crate::{ErrorKind, FormattingError};
18
19fn is_custom_comment(comment: &str) -> bool {
20    if !comment.starts_with("//") {
21        false
22    } else if let Some(c) = comment.chars().nth(2) {
23        !c.is_alphanumeric() && !c.is_whitespace()
24    } else {
25        false
26    }
27}
28
29#[derive(Copy, Clone, PartialEq, Eq)]
30pub(crate) enum CommentStyle<'a> {
31    DoubleSlash,
32    TripleSlash,
33    Doc,
34    SingleBullet,
35    DoubleBullet,
36    Exclamation,
37    Custom(&'a str),
38}
39
40fn custom_opener(s: &str) -> &str {
41    s.lines().next().map_or("", |first_line| {
42        first_line
43            .find(' ')
44            .map_or(first_line, |space_index| &first_line[0..=space_index])
45    })
46}
47
48impl<'a> CommentStyle<'a> {
49    /// Returns `true` if the commenting style cannot span multiple lines.
50    pub(crate) fn is_line_comment(&self) -> bool {
51        matches!(
52            self,
53            CommentStyle::DoubleSlash
54                | CommentStyle::TripleSlash
55                | CommentStyle::Doc
56                | CommentStyle::Custom(_)
57        )
58    }
59
60    /// Returns `true` if the commenting style can span multiple lines.
61    pub(crate) fn is_block_comment(&self) -> bool {
62        matches!(
63            self,
64            CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation
65        )
66    }
67
68    /// Returns `true` if the commenting style is for documentation.
69    pub(crate) fn is_doc_comment(&self) -> bool {
70        matches!(*self, CommentStyle::TripleSlash | CommentStyle::Doc)
71    }
72
73    pub(crate) fn opener(&self) -> &'a str {
74        match *self {
75            CommentStyle::DoubleSlash => "// ",
76            CommentStyle::TripleSlash => "/// ",
77            CommentStyle::Doc => "//! ",
78            CommentStyle::SingleBullet => "/* ",
79            CommentStyle::DoubleBullet => "/** ",
80            CommentStyle::Exclamation => "/*! ",
81            CommentStyle::Custom(opener) => opener,
82        }
83    }
84
85    pub(crate) fn closer(&self) -> &'a str {
86        match *self {
87            CommentStyle::DoubleSlash
88            | CommentStyle::TripleSlash
89            | CommentStyle::Custom(..)
90            | CommentStyle::Doc => "",
91            CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
92                " */"
93            }
94        }
95    }
96
97    pub(crate) fn line_start(&self) -> &'a str {
98        match *self {
99            CommentStyle::DoubleSlash => "// ",
100            CommentStyle::TripleSlash => "/// ",
101            CommentStyle::Doc => "//! ",
102            CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
103                " * "
104            }
105            CommentStyle::Custom(opener) => opener,
106        }
107    }
108
109    pub(crate) fn to_str_tuplet(&self) -> (&'a str, &'a str, &'a str) {
110        (self.opener(), self.closer(), self.line_start())
111    }
112}
113
114pub(crate) fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle<'_> {
115    if !normalize_comments {
116        if orig.starts_with("/**") && !orig.starts_with("/**/") {
117            CommentStyle::DoubleBullet
118        } else if orig.starts_with("/*!") {
119            CommentStyle::Exclamation
120        } else if orig.starts_with("/*") {
121            CommentStyle::SingleBullet
122        } else if orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/') {
123            CommentStyle::TripleSlash
124        } else if orig.starts_with("//!") {
125            CommentStyle::Doc
126        } else if is_custom_comment(orig) {
127            CommentStyle::Custom(custom_opener(orig))
128        } else {
129            CommentStyle::DoubleSlash
130        }
131    } else if (orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/'))
132        || (orig.starts_with("/**") && !orig.starts_with("/**/"))
133    {
134        CommentStyle::TripleSlash
135    } else if orig.starts_with("//!") || orig.starts_with("/*!") {
136        CommentStyle::Doc
137    } else if is_custom_comment(orig) {
138        CommentStyle::Custom(custom_opener(orig))
139    } else {
140        CommentStyle::DoubleSlash
141    }
142}
143
144/// Returns true if the last line of the passed string finishes with a block-comment.
145pub(crate) fn is_last_comment_block(s: &str) -> bool {
146    s.trim_end().ends_with("*/")
147}
148
149/// Combine `prev_str` and `next_str` into a single `String`. `span` may contain
150/// comments between two strings. If there are such comments, then that will be
151/// recovered. If `allow_extend` is true and there is no comment between the two
152/// strings, then they will be put on a single line as long as doing so does not
153/// exceed max width.
154pub(crate) fn combine_strs_with_missing_comments(
155    context: &RewriteContext<'_>,
156    prev_str: &str,
157    next_str: &str,
158    span: Span,
159    shape: Shape,
160    allow_extend: bool,
161) -> RewriteResult {
162    trace!(
163        "combine_strs_with_missing_comments `{}` `{}` {:?} {:?}",
164        prev_str, next_str, span, shape
165    );
166
167    let mut result =
168        String::with_capacity(prev_str.len() + next_str.len() + shape.indent.width() + 128);
169    result.push_str(prev_str);
170    let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n');
171    let first_sep =
172        if prev_str.is_empty() || next_str.is_empty() || trimmed_last_line_width(prev_str) == 0 {
173            ""
174        } else {
175            " "
176        };
177    let mut one_line_width = last_line_width(prev_str, context.config.tab_spaces())
178        + first_line_width(next_str)
179        + first_sep.len();
180
181    let config = context.config;
182    let indent = shape.indent;
183    let missing_comment = rewrite_missing_comment(span, shape, context)?;
184
185    if missing_comment.is_empty() {
186        if allow_extend && one_line_width <= shape.width {
187            result.push_str(first_sep);
188        } else if !prev_str.is_empty() {
189            result.push_str(&indent.to_string_with_newline(config))
190        }
191        result.push_str(next_str);
192        return Ok(result);
193    }
194
195    // We have a missing comment between the first expression and the second expression.
196
197    // Peek the original source code and find out whether there is a newline between the first
198    // expression and the second expression or the missing comment. We will preserve the original
199    // layout whenever possible.
200    let original_snippet = context.snippet(span);
201    let prefer_same_line = if let Some(pos) = original_snippet.find('/') {
202        !original_snippet[..pos].contains('\n')
203    } else {
204        !original_snippet.contains('\n')
205    };
206
207    one_line_width -= first_sep.len();
208    let first_sep = if prev_str.is_empty() || missing_comment.is_empty() {
209        Cow::from("")
210    } else {
211        let one_line_width = last_line_width(prev_str, context.config.tab_spaces())
212            + first_line_width(&missing_comment)
213            + 1;
214        if prefer_same_line && one_line_width <= shape.width {
215            Cow::from(" ")
216        } else {
217            indent.to_string_with_newline(config)
218        }
219    };
220    result.push_str(&first_sep);
221    result.push_str(&missing_comment);
222
223    let second_sep = if missing_comment.is_empty() || next_str.is_empty() {
224        Cow::from("")
225    } else if missing_comment.starts_with("//") {
226        indent.to_string_with_newline(config)
227    } else {
228        one_line_width += missing_comment.len() + first_sep.len() + 1;
229        allow_one_line &= !missing_comment.starts_with("//") && !missing_comment.contains('\n');
230        if prefer_same_line && allow_one_line && one_line_width <= shape.width {
231            Cow::from(" ")
232        } else {
233            indent.to_string_with_newline(config)
234        }
235    };
236    result.push_str(&second_sep);
237    result.push_str(next_str);
238
239    Ok(result)
240}
241
242pub(crate) fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> RewriteResult {
243    identify_comment(orig, false, shape, config, true)
244}
245
246pub(crate) fn rewrite_comment(
247    orig: &str,
248    block_style: bool,
249    shape: Shape,
250    config: &Config,
251) -> RewriteResult {
252    identify_comment(orig, block_style, shape, config, false)
253}
254
255fn identify_comment(
256    orig: &str,
257    block_style: bool,
258    shape: Shape,
259    config: &Config,
260    is_doc_comment: bool,
261) -> RewriteResult {
262    let style = comment_style(orig, false);
263
264    // Computes the byte length of line taking into account a newline if the line is part of a
265    // paragraph.
266    fn compute_len(orig: &str, line: &str) -> usize {
267        if orig.len() > line.len() {
268            if orig.as_bytes()[line.len()] == b'\r' {
269                line.len() + 2
270            } else {
271                line.len() + 1
272            }
273        } else {
274            line.len()
275        }
276    }
277
278    // Get the first group of line comments having the same commenting style.
279    //
280    // Returns a tuple with:
281    // - a boolean indicating if there is a blank line
282    // - a number indicating the size of the first group of comments
283    fn consume_same_line_comments(
284        style: CommentStyle<'_>,
285        orig: &str,
286        line_start: &str,
287    ) -> (bool, usize) {
288        let mut first_group_ending = 0;
289        let mut hbl = false;
290
291        for line in orig.lines() {
292            let trimmed_line = line.trim_start();
293            if trimmed_line.is_empty() {
294                hbl = true;
295                break;
296            } else if trimmed_line.starts_with(line_start)
297                || comment_style(trimmed_line, false) == style
298            {
299                first_group_ending += compute_len(&orig[first_group_ending..], line);
300            } else {
301                break;
302            }
303        }
304        (hbl, first_group_ending)
305    }
306
307    let (has_bare_lines, first_group_ending) = match style {
308        CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => {
309            let line_start = style.line_start().trim_start();
310            consume_same_line_comments(style, orig, line_start)
311        }
312        CommentStyle::Custom(opener) => {
313            let trimmed_opener = opener.trim_end();
314            consume_same_line_comments(style, orig, trimmed_opener)
315        }
316        // for a block comment, search for the closing symbol
317        CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => {
318            let closer = style.closer().trim_start();
319            let mut count = orig.matches(closer).count();
320            let mut closing_symbol_offset = 0;
321            let mut hbl = false;
322            let mut first = true;
323            for line in orig.lines() {
324                closing_symbol_offset += compute_len(&orig[closing_symbol_offset..], line);
325                let mut trimmed_line = line.trim_start();
326                if !trimmed_line.starts_with('*')
327                    && !trimmed_line.starts_with("//")
328                    && !trimmed_line.starts_with("/*")
329                {
330                    hbl = true;
331                }
332
333                // Remove opener from consideration when searching for closer
334                if first {
335                    let opener = style.opener().trim_end();
336                    trimmed_line = &trimmed_line[opener.len()..];
337                    first = false;
338                }
339                if trimmed_line.ends_with(closer) {
340                    count -= 1;
341                    if count == 0 {
342                        break;
343                    }
344                }
345            }
346            (hbl, closing_symbol_offset)
347        }
348    };
349
350    let (first_group, rest) = orig.split_at(first_group_ending);
351    let rewritten_first_group =
352        if !config.normalize_comments() && has_bare_lines && style.is_block_comment() {
353            trim_left_preserve_layout(first_group, shape.indent, config).unknown_error()?
354        } else if !config.normalize_comments()
355            && !config.wrap_comments()
356            && !(
357                // `format_code_in_doc_comments` should only take effect on doc comments,
358                // so we only consider it when this comment block is a doc comment block.
359                is_doc_comment && config.format_code_in_doc_comments()
360            )
361        {
362            light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)
363        } else {
364            rewrite_comment_inner(
365                first_group,
366                block_style,
367                style,
368                shape,
369                config,
370                is_doc_comment || style.is_doc_comment(),
371            )?
372        };
373    if rest.is_empty() {
374        Ok(rewritten_first_group)
375    } else {
376        identify_comment(
377            rest.trim_start(),
378            block_style,
379            shape,
380            config,
381            is_doc_comment,
382        )
383        .map(|rest_str| {
384            format!(
385                "{}\n{}{}{}",
386                rewritten_first_group,
387                // insert back the blank line
388                if has_bare_lines && style.is_line_comment() {
389                    "\n"
390                } else {
391                    ""
392                },
393                shape.indent.to_string(config),
394                rest_str
395            )
396        })
397    }
398}
399
400/// Enum indicating if the code block contains rust based on attributes
401enum CodeBlockAttribute {
402    Rust,
403    NotRust,
404}
405
406impl CodeBlockAttribute {
407    /// Parse comma separated attributes list. Return rust only if all
408    /// attributes are valid rust attributes
409    /// See <https://doc.rust-lang.org/rustdoc/print.html#attributes>
410    fn new(attributes: &str) -> CodeBlockAttribute {
411        for attribute in attributes.split(',') {
412            match attribute.trim() {
413                "" | "rust" | "should_panic" | "no_run" | "edition2015" | "edition2018"
414                | "edition2021" => (),
415                "ignore" | "compile_fail" | "text" => return CodeBlockAttribute::NotRust,
416                _ => return CodeBlockAttribute::NotRust,
417            }
418        }
419        CodeBlockAttribute::Rust
420    }
421}
422
423/// Block that is formatted as an item.
424///
425/// An item starts with either a star `*`, a dash `-`, a greater-than `>`, a plus '+', or a number
426/// `12.` or `34)` (with at most 2 digits). An item represents CommonMark's ["list
427/// items"](https://spec.commonmark.org/0.30/#list-items) and/or ["block
428/// quotes"](https://spec.commonmark.org/0.30/#block-quotes), but note that only a subset of
429/// CommonMark is recognized - see the doc comment of [`ItemizedBlock::get_marker_length`] for more
430/// details.
431///
432/// Different level of indentation are handled by shrinking the shape accordingly.
433struct ItemizedBlock {
434    /// the lines that are identified as part of an itemized block
435    lines: Vec<String>,
436    /// the number of characters (typically whitespaces) up to the item marker
437    indent: usize,
438    /// the string that marks the start of an item
439    opener: String,
440    /// sequence of characters (typically whitespaces) to prefix new lines that are part of the item
441    line_start: String,
442}
443
444impl ItemizedBlock {
445    /// Checks whether the `trimmed` line includes an item marker. Returns `None` if there is no
446    /// marker. Returns the length of the marker (in bytes) if one is present. Note that the length
447    /// includes the whitespace that follows the marker, for example the marker in `"* list item"`
448    /// has the length of 2.
449    ///
450    /// This function recognizes item markers that correspond to CommonMark's
451    /// ["bullet list marker"](https://spec.commonmark.org/0.30/#bullet-list-marker),
452    /// ["block quote marker"](https://spec.commonmark.org/0.30/#block-quote-marker), and/or
453    /// ["ordered list marker"](https://spec.commonmark.org/0.30/#ordered-list-marker).
454    ///
455    /// Compared to CommonMark specification, the number of digits that are allowed in an ["ordered
456    /// list marker"](https://spec.commonmark.org/0.30/#ordered-list-marker) is more limited (to at
457    /// most 2 digits). Limiting the length of the marker helps reduce the risk of recognizing
458    /// arbitrary numbers as markers. See also
459    /// <https://talk.commonmark.org/t/blank-lines-before-lists-revisited/1990> which gives the
460    /// following example where a number (i.e. "1868") doesn't signify an ordered list:
461    /// ```md
462    /// The Captain died in
463    /// 1868. He wes buried in...
464    /// ```
465    fn get_marker_length(trimmed: &str) -> Option<usize> {
466        // https://spec.commonmark.org/0.30/#bullet-list-marker or
467        // https://spec.commonmark.org/0.30/#block-quote-marker
468        let itemized_start = ["* ", "- ", "> ", "+ "];
469        if itemized_start.iter().any(|s| trimmed.starts_with(s)) {
470            return Some(2); // All items in `itemized_start` have length 2.
471        }
472
473        // https://spec.commonmark.org/0.30/#ordered-list-marker, where at most 2 digits are
474        // allowed.
475        for suffix in [". ", ") "] {
476            if let Some((prefix, _)) = trimmed.split_once(suffix) {
477                let has_leading_digits = (1..=2).contains(&prefix.len())
478                    && prefix.chars().all(|c| char::is_ascii_digit(&c));
479                if has_leading_digits {
480                    return Some(prefix.len() + suffix.len());
481                }
482            }
483        }
484
485        None // No markers found.
486    }
487
488    /// Creates a new `ItemizedBlock` described with the given `line`.
489    /// Returns `None` if `line` doesn't start an item.
490    fn new(line: &str) -> Option<ItemizedBlock> {
491        let marker_length = ItemizedBlock::get_marker_length(line.trim_start())?;
492        let space_to_marker = line.chars().take_while(|c| c.is_whitespace()).count();
493        let mut indent = space_to_marker + marker_length;
494        let mut line_start = " ".repeat(indent);
495
496        // Markdown blockquote start with a "> "
497        if line.trim_start().starts_with('>') {
498            // remove the original +2 indent because there might be multiple nested block quotes
499            // and it's easier to reason about the final indent by just taking the length
500            // of the new line_start. We update the indent because it effects the max width
501            // of each formatted line.
502            line_start = itemized_block_quote_start(line, line_start, 2);
503            indent = line_start.len();
504        }
505        Some(ItemizedBlock {
506            lines: vec![line[indent..].to_string()],
507            indent,
508            opener: line[..indent].to_string(),
509            line_start,
510        })
511    }
512
513    /// Returns a `StringFormat` used for formatting the content of an item.
514    fn create_string_format<'a>(&'a self, fmt: &'a StringFormat<'_>) -> StringFormat<'a> {
515        StringFormat {
516            opener: "",
517            closer: "",
518            line_start: "",
519            line_end: "",
520            shape: Shape::legacy(fmt.shape.width.saturating_sub(self.indent), Indent::empty()),
521            trim_end: true,
522            config: fmt.config,
523        }
524    }
525
526    /// Returns `true` if the line is part of the current itemized block.
527    /// If it is, then it is added to the internal lines list.
528    fn add_line(&mut self, line: &str) -> bool {
529        if ItemizedBlock::get_marker_length(line.trim_start()).is_none()
530            && self.indent <= line.chars().take_while(|c| c.is_whitespace()).count()
531        {
532            self.lines.push(line.to_string());
533            return true;
534        }
535        false
536    }
537
538    /// Returns the block as a string, with each line trimmed at the start.
539    fn trimmed_block_as_string(&self) -> String {
540        self.lines.iter().fold(String::new(), |mut acc, line| {
541            acc.push_str(line.trim_start());
542            acc.push(' ');
543            acc
544        })
545    }
546
547    /// Returns the block as a string under its original form.
548    fn original_block_as_string(&self) -> String {
549        self.lines.join("\n")
550    }
551}
552
553/// Determine the line_start when formatting markdown block quotes.
554/// The original line_start likely contains indentation (whitespaces), which we'd like to
555/// replace with '> ' characters.
556fn itemized_block_quote_start(line: &str, mut line_start: String, remove_indent: usize) -> String {
557    let quote_level = line
558        .chars()
559        .take_while(|c| !c.is_alphanumeric())
560        .fold(0, |acc, c| if c == '>' { acc + 1 } else { acc });
561
562    for _ in 0..remove_indent {
563        line_start.pop();
564    }
565
566    for _ in 0..quote_level {
567        line_start.push_str("> ");
568    }
569    line_start
570}
571
572struct CommentRewrite<'a> {
573    result: String,
574    code_block_buffer: String,
575    is_prev_line_multi_line: bool,
576    code_block_attr: Option<CodeBlockAttribute>,
577    item_block: Option<ItemizedBlock>,
578    comment_line_separator: String,
579    indent_str: String,
580    max_width: usize,
581    fmt_indent: Indent,
582    fmt: StringFormat<'a>,
583
584    opener: String,
585    closer: String,
586    line_start: String,
587    style: CommentStyle<'a>,
588}
589
590impl<'a> CommentRewrite<'a> {
591    fn new(
592        orig: &'a str,
593        block_style: bool,
594        shape: Shape,
595        config: &'a Config,
596    ) -> CommentRewrite<'a> {
597        let ((opener, closer, line_start), style) = if block_style {
598            (
599                CommentStyle::SingleBullet.to_str_tuplet(),
600                CommentStyle::SingleBullet,
601            )
602        } else {
603            let style = comment_style(orig, config.normalize_comments());
604            (style.to_str_tuplet(), style)
605        };
606
607        let max_width = shape
608            .width
609            .checked_sub(closer.len() + opener.len())
610            .unwrap_or(1);
611        let indent_str = shape.indent.to_string_with_newline(config).to_string();
612
613        let mut cr = CommentRewrite {
614            result: String::with_capacity(orig.len() * 2),
615            code_block_buffer: String::with_capacity(128),
616            is_prev_line_multi_line: false,
617            code_block_attr: None,
618            item_block: None,
619            comment_line_separator: format!("{indent_str}{line_start}"),
620            max_width,
621            indent_str,
622            fmt_indent: shape.indent,
623
624            fmt: StringFormat {
625                opener: "",
626                closer: "",
627                line_start,
628                line_end: "",
629                shape: Shape::legacy(max_width, shape.indent),
630                trim_end: true,
631                config,
632            },
633
634            opener: opener.to_owned(),
635            closer: closer.to_owned(),
636            line_start: line_start.to_owned(),
637            style,
638        };
639        cr.result.push_str(opener);
640        cr
641    }
642
643    fn join_block(s: &str, sep: &str) -> String {
644        let mut result = String::with_capacity(s.len() + 128);
645        let mut iter = s.lines().peekable();
646        while let Some(line) = iter.next() {
647            result.push_str(line);
648            result.push_str(match iter.peek() {
649                Some(&"") => sep.trim_end(),
650                Some(..) => sep,
651                None => "",
652            });
653        }
654        result
655    }
656
657    /// Check if any characters were written to the result buffer after the start of the comment.
658    /// when calling [`CommentRewrite::new()`] the result buffer is initialized with the opening
659    /// characters for the comment.
660    fn buffer_contains_comment(&self) -> bool {
661        // if self.result.len() < self.opener.len() then an empty comment is in the buffer
662        // if self.result.len() > self.opener.len() then a non empty comment is in the buffer
663        self.result.len() != self.opener.len()
664    }
665
666    fn finish(mut self) -> String {
667        if !self.code_block_buffer.is_empty() {
668            // There is a code block that is not properly enclosed by backticks.
669            // We will leave them untouched.
670            self.result.push_str(&self.comment_line_separator);
671            self.result.push_str(&Self::join_block(
672                &trim_custom_comment_prefix(&self.code_block_buffer),
673                &self.comment_line_separator,
674            ));
675        }
676
677        if let Some(ref ib) = self.item_block {
678            // the last few lines are part of an itemized block
679            self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
680            let item_fmt = ib.create_string_format(&self.fmt);
681
682            // only push a comment_line_separator for ItemizedBlocks if the comment is not empty
683            if self.buffer_contains_comment() {
684                self.result.push_str(&self.comment_line_separator);
685            }
686
687            self.result.push_str(&ib.opener);
688            match rewrite_string(
689                &ib.trimmed_block_as_string(),
690                &item_fmt,
691                self.max_width.saturating_sub(ib.indent),
692            ) {
693                Some(s) => self.result.push_str(&Self::join_block(
694                    &s,
695                    &format!("{}{}", self.comment_line_separator, ib.line_start),
696                )),
697                None => self.result.push_str(&Self::join_block(
698                    &ib.original_block_as_string(),
699                    &self.comment_line_separator,
700                )),
701            };
702        }
703
704        self.result.push_str(&self.closer);
705        if self.result.ends_with(&self.opener) && self.opener.ends_with(' ') {
706            // Trailing space.
707            self.result.pop();
708        }
709
710        self.result
711    }
712
713    fn handle_line(
714        &mut self,
715        orig: &'a str,
716        i: usize,
717        line: &'a str,
718        has_leading_whitespace: bool,
719        is_doc_comment: bool,
720    ) -> bool {
721        let num_newlines = count_newlines(orig);
722        let is_last = i == num_newlines;
723        let needs_new_comment_line = if self.style.is_block_comment() {
724            num_newlines > 0 || self.buffer_contains_comment()
725        } else {
726            self.buffer_contains_comment()
727        };
728
729        if let Some(ref mut ib) = self.item_block {
730            if ib.add_line(line) {
731                return false;
732            }
733            self.is_prev_line_multi_line = false;
734            self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
735            let item_fmt = ib.create_string_format(&self.fmt);
736
737            // only push a comment_line_separator if we need to start a new comment line
738            if needs_new_comment_line {
739                self.result.push_str(&self.comment_line_separator);
740            }
741
742            self.result.push_str(&ib.opener);
743            match rewrite_string(
744                &ib.trimmed_block_as_string(),
745                &item_fmt,
746                self.max_width.saturating_sub(ib.indent),
747            ) {
748                Some(s) => self.result.push_str(&Self::join_block(
749                    &s,
750                    &format!("{}{}", self.comment_line_separator, ib.line_start),
751                )),
752                None => self.result.push_str(&Self::join_block(
753                    &ib.original_block_as_string(),
754                    &self.comment_line_separator,
755                )),
756            };
757        } else if self.code_block_attr.is_some() {
758            if line.starts_with("```") {
759                let code_block = match self.code_block_attr.as_ref().unwrap() {
760                    CodeBlockAttribute::Rust
761                        if self.fmt.config.format_code_in_doc_comments()
762                            && !self.code_block_buffer.trim().is_empty() =>
763                    {
764                        let mut config = self.fmt.config.clone();
765                        config.set().wrap_comments(false);
766                        let comment_max_width = config
767                            .doc_comment_code_block_width()
768                            .min(config.max_width());
769                        config.set().max_width(comment_max_width);
770                        if let Some(comment_use_small_heuristics) = config
771                            .doc_comment_code_block_small_heuristics()
772                            .to_heuristics()
773                        {
774                            config
775                                .set()
776                                .use_small_heuristics(comment_use_small_heuristics);
777                        }
778                        if let Some(s) =
779                            crate::format_code_block(&self.code_block_buffer, &config, false)
780                        {
781                            trim_custom_comment_prefix(&s.snippet)
782                        } else {
783                            trim_custom_comment_prefix(&self.code_block_buffer)
784                        }
785                    }
786                    _ => trim_custom_comment_prefix(&self.code_block_buffer),
787                };
788                if !code_block.is_empty() {
789                    self.result.push_str(&self.comment_line_separator);
790                    self.result
791                        .push_str(&Self::join_block(&code_block, &self.comment_line_separator));
792                }
793                self.code_block_buffer.clear();
794                self.result.push_str(&self.comment_line_separator);
795                self.result.push_str(line);
796                self.code_block_attr = None;
797            } else {
798                self.code_block_buffer
799                    .push_str(&hide_sharp_behind_comment(line));
800                self.code_block_buffer.push('\n');
801            }
802            return false;
803        }
804
805        self.code_block_attr = None;
806        self.item_block = None;
807        if let Some(stripped) = line.strip_prefix("```") {
808            self.code_block_attr = Some(CodeBlockAttribute::new(stripped))
809        } else if self.fmt.config.wrap_comments() {
810            if let Some(ib) = ItemizedBlock::new(line) {
811                self.item_block = Some(ib);
812                return false;
813            }
814        }
815
816        if self.result == self.opener {
817            let force_leading_whitespace = &self.opener == "/* " && count_newlines(orig) == 0;
818            if !has_leading_whitespace && !force_leading_whitespace && self.result.ends_with(' ') {
819                self.result.pop();
820            }
821            if line.is_empty() {
822                return false;
823            }
824        } else if self.is_prev_line_multi_line && !line.is_empty() {
825            self.result.push(' ')
826        } else if is_last && line.is_empty() {
827            // trailing blank lines are unwanted
828            if !self.closer.is_empty() {
829                self.result.push_str(&self.indent_str);
830            }
831            return true;
832        } else {
833            self.result.push_str(&self.comment_line_separator);
834            if !has_leading_whitespace && self.result.ends_with(' ') {
835                self.result.pop();
836            }
837        }
838
839        let is_markdown_header_doc_comment = is_doc_comment && line.starts_with('#');
840
841        // We only want to wrap the comment if:
842        // 1) wrap_comments = true is configured
843        // 2) The comment is not the start of a markdown header doc comment
844        // 3) The comment width exceeds the shape's width
845        // 4) No URLS were found in the comment
846        // If this changes, the documentation in ../Configurations.md#wrap_comments
847        // should be changed accordingly.
848        let should_wrap_comment = self.fmt.config.wrap_comments()
849            && !is_markdown_header_doc_comment
850            && unicode_str_width(line) > self.fmt.shape.width
851            && !has_url(line)
852            && !is_table_item(line);
853
854        if should_wrap_comment {
855            match rewrite_string(line, &self.fmt, self.max_width) {
856                Some(ref s) => {
857                    self.is_prev_line_multi_line = s.contains('\n');
858                    self.result.push_str(s);
859                }
860                None if self.is_prev_line_multi_line => {
861                    // We failed to put the current `line` next to the previous `line`.
862                    // Remove the trailing space, then start rewrite on the next line.
863                    self.result.pop();
864                    self.result.push_str(&self.comment_line_separator);
865                    self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
866                    match rewrite_string(line, &self.fmt, self.max_width) {
867                        Some(ref s) => {
868                            self.is_prev_line_multi_line = s.contains('\n');
869                            self.result.push_str(s);
870                        }
871                        None => {
872                            self.is_prev_line_multi_line = false;
873                            self.result.push_str(line);
874                        }
875                    }
876                }
877                None => {
878                    self.is_prev_line_multi_line = false;
879                    self.result.push_str(line);
880                }
881            }
882
883            self.fmt.shape = if self.is_prev_line_multi_line {
884                // 1 = " "
885                let offset = 1 + last_line_width(&self.result, self.fmt.config.tab_spaces())
886                    - self.line_start.len();
887                Shape {
888                    width: self.max_width.saturating_sub(offset),
889                    indent: self.fmt_indent,
890                    offset: self.fmt.shape.offset + offset,
891                }
892            } else {
893                Shape::legacy(self.max_width, self.fmt_indent)
894            };
895        } else {
896            if line.is_empty() && self.result.ends_with(' ') && !is_last {
897                // Remove space if this is an empty comment or a doc comment.
898                self.result.pop();
899            }
900            if self.code_block_attr.is_some() && self.is_prev_line_multi_line {
901                self.result.push_str(&self.comment_line_separator);
902            }
903            self.result.push_str(line);
904            self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
905            self.is_prev_line_multi_line = false;
906        }
907
908        false
909    }
910}
911
912fn rewrite_comment_inner(
913    orig: &str,
914    block_style: bool,
915    style: CommentStyle<'_>,
916    shape: Shape,
917    config: &Config,
918    is_doc_comment: bool,
919) -> RewriteResult {
920    let mut rewriter = CommentRewrite::new(orig, block_style, shape, config);
921
922    let line_breaks = count_newlines(orig.trim_end());
923    let lines = orig
924        .lines()
925        .enumerate()
926        .map(|(i, mut line)| {
927            line = trim_end_unless_two_whitespaces(line.trim_start(), is_doc_comment);
928            // Drop old closer.
929            if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
930                line = line[..(line.len() - 2)].trim_end();
931            }
932
933            line
934        })
935        .map(|s| left_trim_comment_line(s, &style))
936        .map(|(line, has_leading_whitespace)| {
937            if orig.starts_with("/*") && line_breaks == 0 {
938                (
939                    line.trim_start(),
940                    has_leading_whitespace || config.normalize_comments(),
941                )
942            } else {
943                (line, has_leading_whitespace || config.normalize_comments())
944            }
945        });
946
947    for (i, (line, has_leading_whitespace)) in lines.enumerate() {
948        if rewriter.handle_line(orig, i, line, has_leading_whitespace, is_doc_comment) {
949            break;
950        }
951    }
952
953    Ok(rewriter.finish())
954}
955
956const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
957
958fn hide_sharp_behind_comment(s: &str) -> Cow<'_, str> {
959    let s_trimmed = s.trim();
960    if s_trimmed.starts_with("# ") || s_trimmed == "#" {
961        Cow::from(format!("{RUSTFMT_CUSTOM_COMMENT_PREFIX}{s}"))
962    } else {
963        Cow::from(s)
964    }
965}
966
967fn trim_custom_comment_prefix(s: &str) -> String {
968    s.lines()
969        .map(|line| {
970            let left_trimmed = line.trim_start();
971            if left_trimmed.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX) {
972                left_trimmed.trim_start_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX)
973            } else {
974                line
975            }
976        })
977        .collect::<Vec<_>>()
978        .join("\n")
979}
980
981/// Returns `true` if the given string MAY include URLs or alike.
982fn has_url(s: &str) -> bool {
983    // A regex matching reference doc links.
984    //
985    // ```markdown
986    // /// An [example].
987    // ///
988    // /// [example]: this::is::a::link
989    // ```
990    let reference_link_url = static_regex!(r"^\[.+\]\s?:");
991
992    // This function may return false positive, but should get its job done in most cases.
993    s.contains("https://")
994        || s.contains("http://")
995        || s.contains("ftp://")
996        || s.contains("file://")
997        || reference_link_url.is_match(s)
998}
999
1000/// Returns true if the given string may be part of a Markdown table.
1001fn is_table_item(mut s: &str) -> bool {
1002    // This function may return false positive, but should get its job done in most cases (i.e.
1003    // markdown tables with two column delimiters).
1004    s = s.trim_start();
1005    return s.starts_with('|')
1006        && match s.rfind('|') {
1007            Some(0) | None => false,
1008            _ => true,
1009        };
1010}
1011
1012/// Given the span, rewrite the missing comment inside it if available.
1013/// Note that the given span must only include comments (or leading/trailing whitespaces).
1014pub(crate) fn rewrite_missing_comment(
1015    span: Span,
1016    shape: Shape,
1017    context: &RewriteContext<'_>,
1018) -> RewriteResult {
1019    let missing_snippet = context.snippet(span);
1020    let trimmed_snippet = missing_snippet.trim();
1021    // check the span starts with a comment
1022    let pos = trimmed_snippet.find('/');
1023    if !trimmed_snippet.is_empty() && pos.is_some() {
1024        rewrite_comment(trimmed_snippet, false, shape, context.config)
1025    } else {
1026        Ok(String::new())
1027    }
1028}
1029
1030/// Recover the missing comments in the specified span, if available.
1031/// The layout of the comments will be preserved as long as it does not break the code
1032/// and its total width does not exceed the max width.
1033pub(crate) fn recover_missing_comment_in_span(
1034    span: Span,
1035    shape: Shape,
1036    context: &RewriteContext<'_>,
1037    used_width: usize,
1038) -> RewriteResult {
1039    let missing_comment = rewrite_missing_comment(span, shape, context)?;
1040    if missing_comment.is_empty() {
1041        Ok(String::new())
1042    } else {
1043        let missing_snippet = context.snippet(span);
1044        let pos = missing_snippet.find('/').unknown_error()?;
1045        // 1 = ` `
1046        let total_width = missing_comment.len() + used_width + 1;
1047        let force_new_line_before_comment =
1048            missing_snippet[..pos].contains('\n') || total_width > context.config.max_width();
1049        let sep = if force_new_line_before_comment {
1050            shape.indent.to_string_with_newline(context.config)
1051        } else {
1052            Cow::from(" ")
1053        };
1054        Ok(format!("{sep}{missing_comment}"))
1055    }
1056}
1057
1058/// Trim trailing whitespaces unless they consist of two or more whitespaces.
1059fn trim_end_unless_two_whitespaces(s: &str, is_doc_comment: bool) -> &str {
1060    if is_doc_comment && s.ends_with("  ") {
1061        s
1062    } else {
1063        s.trim_end()
1064    }
1065}
1066
1067/// Trims whitespace and aligns to indent, but otherwise does not change comments.
1068fn light_rewrite_comment(
1069    orig: &str,
1070    offset: Indent,
1071    config: &Config,
1072    is_doc_comment: bool,
1073) -> String {
1074    orig.lines()
1075        .map(|l| {
1076            // This is basically just l.trim(), but in the case that a line starts
1077            // with `*` we want to leave one space before it, so it aligns with the
1078            // `*` in `/*`.
1079            let first_non_whitespace = l.find(|c| !char::is_whitespace(c));
1080            let left_trimmed = if let Some(fnw) = first_non_whitespace {
1081                if l.as_bytes()[fnw] == b'*' {
1082                    Cow::Owned(format!(" {}", &l[fnw..]))
1083                } else {
1084                    Cow::Borrowed(&l[fnw..])
1085                }
1086            } else {
1087                Cow::Borrowed("")
1088            };
1089
1090            // Preserve markdown's double-space line break syntax in doc comment.
1091            match left_trimmed {
1092                Cow::Borrowed(left_trimmed) => Cow::Borrowed(trim_end_unless_two_whitespaces(
1093                    left_trimmed,
1094                    is_doc_comment,
1095                )),
1096                Cow::Owned(left_trimmed) => {
1097                    let trimmed = trim_end_unless_two_whitespaces(&left_trimmed, is_doc_comment);
1098                    Cow::Owned(trimmed.to_string())
1099                }
1100            }
1101        })
1102        .join(&format!("\n{}", offset.to_string(config)))
1103}
1104
1105/// Trims comment characters and possibly a single space from the left of a string.
1106/// Does not trim all whitespace. If a single space is trimmed from the left of the string,
1107/// this function returns true.
1108fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) {
1109    if line.starts_with("//! ")
1110        || line.starts_with("/// ")
1111        || line.starts_with("/*! ")
1112        || line.starts_with("/** ")
1113    {
1114        (&line[4..], true)
1115    } else if let CommentStyle::Custom(opener) = *style {
1116        if let Some(stripped) = line.strip_prefix(opener) {
1117            (stripped, true)
1118        } else {
1119            (&line[opener.trim_end().len()..], false)
1120        }
1121    } else if line.starts_with("/* ")
1122        || line.starts_with("// ")
1123        || line.starts_with("//!")
1124        || line.starts_with("///")
1125        || line.starts_with("** ")
1126        || line.starts_with("/*!")
1127        || (line.starts_with("/**") && !line.starts_with("/**/"))
1128    {
1129        (&line[3..], line.chars().nth(2).unwrap() == ' ')
1130    } else if line.starts_with("/*")
1131        || line.starts_with("* ")
1132        || line.starts_with("//")
1133        || line.starts_with("**")
1134    {
1135        (&line[2..], line.chars().nth(1).unwrap() == ' ')
1136    } else if let Some(stripped) = line.strip_prefix('*') {
1137        (stripped, false)
1138    } else {
1139        (line, line.starts_with(' '))
1140    }
1141}
1142
1143pub(crate) trait FindUncommented {
1144    fn find_uncommented(&self, pat: &str) -> Option<usize>;
1145    fn find_last_uncommented(&self, pat: &str) -> Option<usize>;
1146}
1147
1148impl FindUncommented for str {
1149    fn find_uncommented(&self, pat: &str) -> Option<usize> {
1150        let mut needle_iter = pat.chars();
1151        for (kind, (i, b)) in CharClasses::new(self.char_indices()) {
1152            match needle_iter.next() {
1153                None => {
1154                    return Some(i - pat.len());
1155                }
1156                Some(c) => match kind {
1157                    FullCodeCharKind::Normal | FullCodeCharKind::InString if b == c => {}
1158                    _ => {
1159                        needle_iter = pat.chars();
1160                    }
1161                },
1162            }
1163        }
1164
1165        // Handle case where the pattern is a suffix of the search string
1166        match needle_iter.next() {
1167            Some(_) => None,
1168            None => Some(self.len() - pat.len()),
1169        }
1170    }
1171
1172    fn find_last_uncommented(&self, pat: &str) -> Option<usize> {
1173        if let Some(left) = self.find_uncommented(pat) {
1174            let mut result = left;
1175            // add 1 to use find_last_uncommented for &str after pat
1176            while let Some(next) = self[(result + 1)..].find_last_uncommented(pat) {
1177                result += next + 1;
1178            }
1179            Some(result)
1180        } else {
1181            None
1182        }
1183    }
1184}
1185
1186// Returns the first byte position after the first comment. The given string
1187// is expected to be prefixed by a comment, including delimiters.
1188// Good: `/* /* inner */ outer */ code();`
1189// Bad:  `code(); // hello\n world!`
1190pub(crate) fn find_comment_end(s: &str) -> Option<usize> {
1191    let mut iter = CharClasses::new(s.char_indices());
1192    for (kind, (i, _c)) in &mut iter {
1193        if kind == FullCodeCharKind::Normal || kind == FullCodeCharKind::InString {
1194            return Some(i);
1195        }
1196    }
1197
1198    // Handle case where the comment ends at the end of `s`.
1199    if iter.status == CharClassesStatus::Normal {
1200        Some(s.len())
1201    } else {
1202        None
1203    }
1204}
1205
1206/// Returns `true` if text contains any comment.
1207pub(crate) fn contains_comment(text: &str) -> bool {
1208    CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
1209}
1210
1211pub(crate) struct CharClasses<T>
1212where
1213    T: Iterator,
1214    T::Item: RichChar,
1215{
1216    base: MultiPeek<T>,
1217    status: CharClassesStatus,
1218}
1219
1220pub(crate) trait RichChar {
1221    fn get_char(&self) -> char;
1222}
1223
1224impl RichChar for char {
1225    fn get_char(&self) -> char {
1226        *self
1227    }
1228}
1229
1230impl RichChar for (usize, char) {
1231    fn get_char(&self) -> char {
1232        self.1
1233    }
1234}
1235
1236#[derive(PartialEq, Eq, Debug, Clone, Copy)]
1237enum CharClassesStatus {
1238    Normal,
1239    /// Character is within a string
1240    LitString,
1241    LitStringEscape,
1242    /// Character is within a raw string
1243    LitRawString(u32),
1244    RawStringPrefix(u32),
1245    RawStringSuffix(u32),
1246    LitChar,
1247    LitCharEscape,
1248    /// Character inside a block comment, with the integer indicating the nesting deepness of the
1249    /// comment
1250    BlockComment(u32),
1251    /// Character inside a block-commented string, with the integer indicating the nesting deepness
1252    /// of the comment
1253    StringInBlockComment(u32),
1254    /// Status when the '/' has been consumed, but not yet the '*', deepness is
1255    /// the new deepness (after the comment opening).
1256    BlockCommentOpening(u32),
1257    /// Status when the '*' has been consumed, but not yet the '/', deepness is
1258    /// the new deepness (after the comment closing).
1259    BlockCommentClosing(u32),
1260    /// Character is within a line comment
1261    LineComment,
1262}
1263
1264/// Distinguish between functional part of code and comments
1265#[derive(PartialEq, Eq, Debug, Clone, Copy)]
1266pub(crate) enum CodeCharKind {
1267    Normal,
1268    Comment,
1269}
1270
1271/// Distinguish between functional part of code and comments,
1272/// describing opening and closing of comments for ease when chunking
1273/// code from tagged characters
1274#[derive(PartialEq, Eq, Debug, Clone, Copy)]
1275pub(crate) enum FullCodeCharKind {
1276    Normal,
1277    /// The first character of a comment, there is only one for a comment (always '/')
1278    StartComment,
1279    /// Any character inside a comment including the second character of comment
1280    /// marks ("//", "/*")
1281    InComment,
1282    /// Last character of a comment, '\n' for a line comment, '/' for a block comment.
1283    EndComment,
1284    /// Start of a multiline string inside a comment
1285    StartStringCommented,
1286    /// End of a multiline string inside a comment
1287    EndStringCommented,
1288    /// Inside a commented string
1289    InStringCommented,
1290    /// Start of a multiline string
1291    StartString,
1292    /// End of a multiline string
1293    EndString,
1294    /// Inside a string.
1295    InString,
1296}
1297
1298impl FullCodeCharKind {
1299    pub(crate) fn is_comment(self) -> bool {
1300        match self {
1301            FullCodeCharKind::StartComment
1302            | FullCodeCharKind::InComment
1303            | FullCodeCharKind::EndComment
1304            | FullCodeCharKind::StartStringCommented
1305            | FullCodeCharKind::InStringCommented
1306            | FullCodeCharKind::EndStringCommented => true,
1307            _ => false,
1308        }
1309    }
1310
1311    /// Returns true if the character is inside a comment
1312    pub(crate) fn inside_comment(self) -> bool {
1313        match self {
1314            FullCodeCharKind::InComment
1315            | FullCodeCharKind::StartStringCommented
1316            | FullCodeCharKind::InStringCommented
1317            | FullCodeCharKind::EndStringCommented => true,
1318            _ => false,
1319        }
1320    }
1321
1322    pub(crate) fn is_string(self) -> bool {
1323        self == FullCodeCharKind::InString || self == FullCodeCharKind::StartString
1324    }
1325
1326    /// Returns true if the character is within a commented string
1327    pub(crate) fn is_commented_string(self) -> bool {
1328        self == FullCodeCharKind::InStringCommented
1329            || self == FullCodeCharKind::StartStringCommented
1330    }
1331
1332    fn to_codecharkind(self) -> CodeCharKind {
1333        if self.is_comment() {
1334            CodeCharKind::Comment
1335        } else {
1336            CodeCharKind::Normal
1337        }
1338    }
1339}
1340
1341impl<T> CharClasses<T>
1342where
1343    T: Iterator,
1344    T::Item: RichChar,
1345{
1346    pub(crate) fn new(base: T) -> CharClasses<T> {
1347        CharClasses {
1348            base: multipeek(base),
1349            status: CharClassesStatus::Normal,
1350        }
1351    }
1352}
1353
1354/// Returns `true` if the `r` just consumed opens a raw string literal, i.e. the run of
1355/// `#`s that follows it ends in a `"`. Peeking a single `#` is not enough to tell a raw
1356/// string apart from a raw identifier such as `r#struct`.
1357fn is_raw_string_prefix<T>(iter: &mut MultiPeek<T>) -> bool
1358where
1359    T: Iterator,
1360    T::Item: RichChar,
1361{
1362    while let Some(c) = iter.peek() {
1363        match c.get_char() {
1364            '#' => continue,
1365            '"' => return true,
1366            _ => return false,
1367        }
1368    }
1369    false
1370}
1371
1372fn is_raw_string_suffix<T>(iter: &mut MultiPeek<T>, count: u32) -> bool
1373where
1374    T: Iterator,
1375    T::Item: RichChar,
1376{
1377    for _ in 0..count {
1378        match iter.peek() {
1379            Some(c) if c.get_char() == '#' => continue,
1380            _ => return false,
1381        }
1382    }
1383    true
1384}
1385
1386impl<T> Iterator for CharClasses<T>
1387where
1388    T: Iterator,
1389    T::Item: RichChar,
1390{
1391    type Item = (FullCodeCharKind, T::Item);
1392
1393    fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> {
1394        let item = self.base.next()?;
1395        let chr = item.get_char();
1396        let mut char_kind = FullCodeCharKind::Normal;
1397        self.status = match self.status {
1398            CharClassesStatus::LitRawString(sharps) => {
1399                char_kind = FullCodeCharKind::InString;
1400                match chr {
1401                    '"' => {
1402                        if sharps == 0 {
1403                            char_kind = FullCodeCharKind::Normal;
1404                            CharClassesStatus::Normal
1405                        } else if is_raw_string_suffix(&mut self.base, sharps) {
1406                            CharClassesStatus::RawStringSuffix(sharps)
1407                        } else {
1408                            CharClassesStatus::LitRawString(sharps)
1409                        }
1410                    }
1411                    _ => CharClassesStatus::LitRawString(sharps),
1412                }
1413            }
1414            CharClassesStatus::RawStringPrefix(sharps) => {
1415                char_kind = FullCodeCharKind::InString;
1416                match chr {
1417                    '#' => CharClassesStatus::RawStringPrefix(sharps + 1),
1418                    '"' => CharClassesStatus::LitRawString(sharps),
1419                    _ => CharClassesStatus::Normal, // Unreachable.
1420                }
1421            }
1422            CharClassesStatus::RawStringSuffix(sharps) => {
1423                match chr {
1424                    '#' => {
1425                        if sharps == 1 {
1426                            CharClassesStatus::Normal
1427                        } else {
1428                            char_kind = FullCodeCharKind::InString;
1429                            CharClassesStatus::RawStringSuffix(sharps - 1)
1430                        }
1431                    }
1432                    _ => CharClassesStatus::Normal, // Unreachable
1433                }
1434            }
1435            CharClassesStatus::LitString => {
1436                char_kind = FullCodeCharKind::InString;
1437                match chr {
1438                    '"' => CharClassesStatus::Normal,
1439                    '\\' => CharClassesStatus::LitStringEscape,
1440                    _ => CharClassesStatus::LitString,
1441                }
1442            }
1443            CharClassesStatus::LitStringEscape => {
1444                char_kind = FullCodeCharKind::InString;
1445                CharClassesStatus::LitString
1446            }
1447            CharClassesStatus::LitChar => match chr {
1448                '\\' => CharClassesStatus::LitCharEscape,
1449                '\'' => CharClassesStatus::Normal,
1450                _ => CharClassesStatus::LitChar,
1451            },
1452            CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
1453            CharClassesStatus::Normal => match chr {
1454                'r' => match self.base.peek().map(RichChar::get_char) {
1455                    Some('"') => {
1456                        char_kind = FullCodeCharKind::InString;
1457                        CharClassesStatus::RawStringPrefix(0)
1458                    }
1459                    // `r#` opens a raw string only if the `#`s end in a `"`; otherwise
1460                    // this is a raw identifier like `r#struct` and stays normal code.
1461                    Some('#') if is_raw_string_prefix(&mut self.base) => {
1462                        char_kind = FullCodeCharKind::InString;
1463                        CharClassesStatus::RawStringPrefix(0)
1464                    }
1465                    _ => CharClassesStatus::Normal,
1466                },
1467                '"' => {
1468                    char_kind = FullCodeCharKind::InString;
1469                    CharClassesStatus::LitString
1470                }
1471                '\'' => {
1472                    // HACK: Work around mut borrow.
1473                    match self.base.peek() {
1474                        Some(next) if next.get_char() == '\\' => {
1475                            self.status = CharClassesStatus::LitChar;
1476                            return Some((char_kind, item));
1477                        }
1478                        _ => (),
1479                    }
1480
1481                    match self.base.peek() {
1482                        Some(next) if next.get_char() == '\'' => CharClassesStatus::LitChar,
1483                        _ => CharClassesStatus::Normal,
1484                    }
1485                }
1486                '/' => match self.base.peek() {
1487                    Some(next) if next.get_char() == '*' => {
1488                        self.status = CharClassesStatus::BlockCommentOpening(1);
1489                        return Some((FullCodeCharKind::StartComment, item));
1490                    }
1491                    Some(next) if next.get_char() == '/' => {
1492                        self.status = CharClassesStatus::LineComment;
1493                        return Some((FullCodeCharKind::StartComment, item));
1494                    }
1495                    _ => CharClassesStatus::Normal,
1496                },
1497                _ => CharClassesStatus::Normal,
1498            },
1499            CharClassesStatus::StringInBlockComment(deepness) => {
1500                char_kind = FullCodeCharKind::InStringCommented;
1501                if chr == '"' {
1502                    CharClassesStatus::BlockComment(deepness)
1503                } else if chr == '*' && self.base.peek().map(RichChar::get_char) == Some('/') {
1504                    char_kind = FullCodeCharKind::InComment;
1505                    CharClassesStatus::BlockCommentClosing(deepness - 1)
1506                } else {
1507                    CharClassesStatus::StringInBlockComment(deepness)
1508                }
1509            }
1510            CharClassesStatus::BlockComment(deepness) => {
1511                assert_ne!(deepness, 0);
1512                char_kind = FullCodeCharKind::InComment;
1513                match self.base.peek() {
1514                    Some(next) if next.get_char() == '/' && chr == '*' => {
1515                        CharClassesStatus::BlockCommentClosing(deepness - 1)
1516                    }
1517                    Some(next) if next.get_char() == '*' && chr == '/' => {
1518                        CharClassesStatus::BlockCommentOpening(deepness + 1)
1519                    }
1520                    _ if chr == '"' => CharClassesStatus::StringInBlockComment(deepness),
1521                    _ => self.status,
1522                }
1523            }
1524            CharClassesStatus::BlockCommentOpening(deepness) => {
1525                assert_eq!(chr, '*');
1526                self.status = CharClassesStatus::BlockComment(deepness);
1527                return Some((FullCodeCharKind::InComment, item));
1528            }
1529            CharClassesStatus::BlockCommentClosing(deepness) => {
1530                assert_eq!(chr, '/');
1531                if deepness == 0 {
1532                    self.status = CharClassesStatus::Normal;
1533                    return Some((FullCodeCharKind::EndComment, item));
1534                } else {
1535                    self.status = CharClassesStatus::BlockComment(deepness);
1536                    return Some((FullCodeCharKind::InComment, item));
1537                }
1538            }
1539            CharClassesStatus::LineComment => match chr {
1540                '\n' => {
1541                    self.status = CharClassesStatus::Normal;
1542                    return Some((FullCodeCharKind::EndComment, item));
1543                }
1544                _ => {
1545                    self.status = CharClassesStatus::LineComment;
1546                    return Some((FullCodeCharKind::InComment, item));
1547                }
1548            },
1549        };
1550        Some((char_kind, item))
1551    }
1552}
1553
1554/// An iterator over the lines of a string, paired with the char kind at the
1555/// end of the line.
1556pub(crate) struct LineClasses<'a> {
1557    base: iter::Peekable<CharClasses<std::str::Chars<'a>>>,
1558    kind: FullCodeCharKind,
1559}
1560
1561impl<'a> LineClasses<'a> {
1562    pub(crate) fn new(s: &'a str) -> Self {
1563        LineClasses {
1564            base: CharClasses::new(s.chars()).peekable(),
1565            kind: FullCodeCharKind::Normal,
1566        }
1567    }
1568}
1569
1570impl<'a> Iterator for LineClasses<'a> {
1571    type Item = (FullCodeCharKind, String);
1572
1573    fn next(&mut self) -> Option<Self::Item> {
1574        self.base.peek()?;
1575
1576        let mut line = String::new();
1577
1578        let start_kind = match self.base.peek() {
1579            Some((kind, _)) => *kind,
1580            None => unreachable!(),
1581        };
1582
1583        for (kind, c) in self.base.by_ref() {
1584            // needed to set the kind of the ending character on the last line
1585            self.kind = kind;
1586            if c == '\n' {
1587                self.kind = match (start_kind, kind) {
1588                    (FullCodeCharKind::Normal, FullCodeCharKind::InString) => {
1589                        FullCodeCharKind::StartString
1590                    }
1591                    (FullCodeCharKind::InString, FullCodeCharKind::Normal) => {
1592                        FullCodeCharKind::EndString
1593                    }
1594                    (FullCodeCharKind::InComment, FullCodeCharKind::InStringCommented) => {
1595                        FullCodeCharKind::StartStringCommented
1596                    }
1597                    (FullCodeCharKind::InStringCommented, FullCodeCharKind::InComment) => {
1598                        FullCodeCharKind::EndStringCommented
1599                    }
1600                    _ => kind,
1601                };
1602                break;
1603            }
1604            line.push(c);
1605        }
1606
1607        // Workaround for CRLF newline.
1608        if line.ends_with('\r') {
1609            line.pop();
1610        }
1611
1612        Some((self.kind, line))
1613    }
1614}
1615
1616/// Iterator over functional and commented parts of a string. Any part of a string is either
1617/// functional code, either *one* block comment, either *one* line comment. Whitespace between
1618/// comments is functional code. Line comments contain their ending newlines.
1619struct UngroupedCommentCodeSlices<'a> {
1620    slice: &'a str,
1621    iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
1622}
1623
1624impl<'a> UngroupedCommentCodeSlices<'a> {
1625    fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
1626        UngroupedCommentCodeSlices {
1627            slice: code,
1628            iter: CharClasses::new(code.char_indices()).peekable(),
1629        }
1630    }
1631}
1632
1633impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
1634    type Item = (CodeCharKind, usize, &'a str);
1635
1636    fn next(&mut self) -> Option<Self::Item> {
1637        let (kind, (start_idx, _)) = self.iter.next()?;
1638        match kind {
1639            FullCodeCharKind::Normal | FullCodeCharKind::InString => {
1640                // Consume all the Normal code
1641                while let Some(&(char_kind, _)) = self.iter.peek() {
1642                    if char_kind.is_comment() {
1643                        break;
1644                    }
1645                    let _ = self.iter.next();
1646                }
1647            }
1648            FullCodeCharKind::StartComment => {
1649                // Consume the whole comment
1650                loop {
1651                    match self.iter.next() {
1652                        Some((kind, ..)) if kind.inside_comment() => continue,
1653                        _ => break,
1654                    }
1655                }
1656            }
1657            _ => panic!(),
1658        }
1659        let slice = match self.iter.peek() {
1660            Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
1661            None => &self.slice[start_idx..],
1662        };
1663        Some((
1664            if kind.is_comment() {
1665                CodeCharKind::Comment
1666            } else {
1667                CodeCharKind::Normal
1668            },
1669            start_idx,
1670            slice,
1671        ))
1672    }
1673}
1674
1675/// Iterator over an alternating sequence of functional and commented parts of
1676/// a string. The first item is always a, possibly zero length, subslice of
1677/// functional text. Line style comments contain their ending newlines.
1678pub(crate) struct CommentCodeSlices<'a> {
1679    slice: &'a str,
1680    last_slice_kind: CodeCharKind,
1681    last_slice_end: usize,
1682}
1683
1684impl<'a> CommentCodeSlices<'a> {
1685    pub(crate) fn new(slice: &'a str) -> CommentCodeSlices<'a> {
1686        CommentCodeSlices {
1687            slice,
1688            last_slice_kind: CodeCharKind::Comment,
1689            last_slice_end: 0,
1690        }
1691    }
1692}
1693
1694impl<'a> Iterator for CommentCodeSlices<'a> {
1695    type Item = (CodeCharKind, usize, &'a str);
1696
1697    fn next(&mut self) -> Option<Self::Item> {
1698        if self.last_slice_end == self.slice.len() {
1699            return None;
1700        }
1701
1702        let mut sub_slice_end = self.last_slice_end;
1703        let mut first_whitespace = None;
1704        let subslice = &self.slice[self.last_slice_end..];
1705        let mut iter = CharClasses::new(subslice.char_indices());
1706
1707        for (kind, (i, c)) in &mut iter {
1708            let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal
1709                && &subslice[..2] == "//"
1710                && [' ', '\t'].contains(&c);
1711
1712            if is_comment_connector && first_whitespace.is_none() {
1713                first_whitespace = Some(i);
1714            }
1715
1716            if kind.to_codecharkind() == self.last_slice_kind && !is_comment_connector {
1717                let last_index = match first_whitespace {
1718                    Some(j) => j,
1719                    None => i,
1720                };
1721                sub_slice_end = self.last_slice_end + last_index;
1722                break;
1723            }
1724
1725            if !is_comment_connector {
1726                first_whitespace = None;
1727            }
1728        }
1729
1730        if let (None, true) = (iter.next(), sub_slice_end == self.last_slice_end) {
1731            // This was the last subslice.
1732            sub_slice_end = match first_whitespace {
1733                Some(i) => self.last_slice_end + i,
1734                None => self.slice.len(),
1735            };
1736        }
1737
1738        let kind = match self.last_slice_kind {
1739            CodeCharKind::Comment => CodeCharKind::Normal,
1740            CodeCharKind::Normal => CodeCharKind::Comment,
1741        };
1742        let res = (
1743            kind,
1744            self.last_slice_end,
1745            &self.slice[self.last_slice_end..sub_slice_end],
1746        );
1747        self.last_slice_end = sub_slice_end;
1748        self.last_slice_kind = kind;
1749
1750        Some(res)
1751    }
1752}
1753
1754/// Checks is `new` didn't miss any comment from `span`, if it removed any, return previous text
1755pub(crate) fn recover_comment_removed(
1756    new: String,
1757    span: Span,
1758    context: &RewriteContext<'_>,
1759) -> String {
1760    let snippet = context.snippet(span);
1761    if snippet != new && changed_comment_content(snippet, &new) {
1762        // We missed some comments. Warn and keep the original text.
1763        if context.config.error_on_unformatted() {
1764            context.report.append(
1765                context.psess.span_to_filename(span),
1766                vec![FormattingError::from_span(
1767                    span,
1768                    context.psess,
1769                    ErrorKind::LostComment,
1770                )],
1771            );
1772        }
1773        snippet.to_owned()
1774    } else {
1775        new
1776    }
1777}
1778
1779pub(crate) fn filter_normal_code(code: &str) -> String {
1780    let mut buffer = String::with_capacity(code.len());
1781    LineClasses::new(code).for_each(|(kind, line)| match kind {
1782        FullCodeCharKind::Normal
1783        | FullCodeCharKind::StartString
1784        | FullCodeCharKind::InString
1785        | FullCodeCharKind::EndString => {
1786            buffer.push_str(&line);
1787            buffer.push('\n');
1788        }
1789        _ => (),
1790    });
1791    if !code.ends_with('\n') && buffer.ends_with('\n') {
1792        buffer.pop();
1793    }
1794    buffer
1795}
1796
1797/// Returns `true` if the two strings of code have the same payload of comments.
1798/// The payload of comments is everything in the string except:
1799/// - actual code (not comments),
1800/// - comment start/end marks,
1801/// - whitespace,
1802/// - '*' at the beginning of lines in block comments.
1803fn changed_comment_content(orig: &str, new: &str) -> bool {
1804    // Cannot write this as a fn since we cannot return types containing closures.
1805    let code_comment_content = |code| {
1806        let slices = UngroupedCommentCodeSlices::new(code);
1807        slices
1808            .filter(|(kind, _, _)| *kind == CodeCharKind::Comment)
1809            .flat_map(|(_, _, s)| CommentReducer::new(s))
1810    };
1811    let res = code_comment_content(orig).ne(code_comment_content(new));
1812    debug!(
1813        "comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
1814        res,
1815        orig,
1816        new,
1817        code_comment_content(orig).collect::<String>(),
1818        code_comment_content(new).collect::<String>()
1819    );
1820    res
1821}
1822
1823/// Iterator over the 'payload' characters of a comment.
1824/// It skips whitespace, comment start/end marks, and '*' at the beginning of lines.
1825/// The comment must be one comment, ie not more than one start mark (no multiple line comments,
1826/// for example).
1827struct CommentReducer<'a> {
1828    is_block: bool,
1829    at_start_line: bool,
1830    iter: std::str::Chars<'a>,
1831}
1832
1833impl<'a> CommentReducer<'a> {
1834    fn new(comment: &'a str) -> CommentReducer<'a> {
1835        let is_block = comment.starts_with("/*");
1836        let comment = remove_comment_header(comment);
1837        CommentReducer {
1838            is_block,
1839            // There are no supplementary '*' on the first line.
1840            at_start_line: false,
1841            iter: comment.chars(),
1842        }
1843    }
1844}
1845
1846impl<'a> Iterator for CommentReducer<'a> {
1847    type Item = char;
1848
1849    fn next(&mut self) -> Option<Self::Item> {
1850        loop {
1851            let mut c = self.iter.next()?;
1852            if self.is_block && self.at_start_line {
1853                while c.is_whitespace() {
1854                    c = self.iter.next()?;
1855                }
1856                // Ignore leading '*'.
1857                if c == '*' {
1858                    c = self.iter.next()?;
1859                }
1860            } else if c == '\n' {
1861                self.at_start_line = true;
1862            }
1863            if !c.is_whitespace() {
1864                return Some(c);
1865            }
1866        }
1867    }
1868}
1869
1870fn remove_comment_header(comment: &str) -> &str {
1871    if comment.starts_with("///") || comment.starts_with("//!") {
1872        &comment[3..]
1873    } else if let Some(stripped) = comment.strip_prefix("//") {
1874        stripped
1875    } else if (comment.starts_with("/**") && !comment.starts_with("/**/"))
1876        || comment.starts_with("/*!")
1877    {
1878        &comment[3..comment.len() - 2]
1879    } else {
1880        assert!(
1881            comment.starts_with("/*"),
1882            "string '{comment}' is not a comment"
1883        );
1884        &comment[2..comment.len() - 2]
1885    }
1886}
1887
1888#[cfg(test)]
1889mod test {
1890    use super::*;
1891
1892    #[test]
1893    fn char_classes() {
1894        let mut iter = CharClasses::new("//\n\n".chars());
1895
1896        assert_eq!((FullCodeCharKind::StartComment, '/'), iter.next().unwrap());
1897        assert_eq!((FullCodeCharKind::InComment, '/'), iter.next().unwrap());
1898        assert_eq!((FullCodeCharKind::EndComment, '\n'), iter.next().unwrap());
1899        assert_eq!((FullCodeCharKind::Normal, '\n'), iter.next().unwrap());
1900        assert_eq!(None, iter.next());
1901    }
1902
1903    #[test]
1904    fn comment_code_slices() {
1905        let input = "code(); /* test */ 1 + 1";
1906        let mut iter = CommentCodeSlices::new(input);
1907
1908        assert_eq!((CodeCharKind::Normal, 0, "code(); "), iter.next().unwrap());
1909        assert_eq!(
1910            (CodeCharKind::Comment, 8, "/* test */"),
1911            iter.next().unwrap()
1912        );
1913        assert_eq!((CodeCharKind::Normal, 18, " 1 + 1"), iter.next().unwrap());
1914        assert_eq!(None, iter.next());
1915    }
1916
1917    #[test]
1918    fn comment_code_slices_two() {
1919        let input = "// comment\n    test();";
1920        let mut iter = CommentCodeSlices::new(input);
1921
1922        assert_eq!((CodeCharKind::Normal, 0, ""), iter.next().unwrap());
1923        assert_eq!(
1924            (CodeCharKind::Comment, 0, "// comment\n"),
1925            iter.next().unwrap()
1926        );
1927        assert_eq!(
1928            (CodeCharKind::Normal, 11, "    test();"),
1929            iter.next().unwrap()
1930        );
1931        assert_eq!(None, iter.next());
1932    }
1933
1934    #[test]
1935    fn comment_code_slices_three() {
1936        let input = "1 // comment\n    // comment2\n\n";
1937        let mut iter = CommentCodeSlices::new(input);
1938
1939        assert_eq!((CodeCharKind::Normal, 0, "1 "), iter.next().unwrap());
1940        assert_eq!(
1941            (CodeCharKind::Comment, 2, "// comment\n    // comment2\n"),
1942            iter.next().unwrap()
1943        );
1944        assert_eq!((CodeCharKind::Normal, 29, "\n"), iter.next().unwrap());
1945        assert_eq!(None, iter.next());
1946    }
1947
1948    #[test]
1949    #[rustfmt::skip]
1950    fn format_doc_comments() {
1951        let mut wrap_normalize_config: crate::config::Config = Default::default();
1952        wrap_normalize_config.set().wrap_comments(true);
1953        wrap_normalize_config.set().normalize_comments(true);
1954
1955        let mut wrap_config: crate::config::Config = Default::default();
1956        wrap_config.set().wrap_comments(true);
1957
1958        let comment = rewrite_comment(" //test",
1959                                      true,
1960                                      Shape::legacy(100, Indent::new(0, 100)),
1961                                      &wrap_normalize_config).unwrap();
1962        assert_eq!("/* test */", comment);
1963
1964        let comment = rewrite_comment("// comment on a",
1965                                      false,
1966                                      Shape::legacy(10, Indent::empty()),
1967                                      &wrap_normalize_config).unwrap();
1968        assert_eq!("// comment\n// on a", comment);
1969
1970        let comment = rewrite_comment("//  A multi line comment\n             // between args.",
1971                                      false,
1972                                      Shape::legacy(60, Indent::new(0, 12)),
1973                                      &wrap_normalize_config).unwrap();
1974        assert_eq!("//  A multi line comment\n            // between args.", comment);
1975
1976        let input = "// comment";
1977        let expected =
1978            "/* comment */";
1979        let comment = rewrite_comment(input,
1980                                      true,
1981                                      Shape::legacy(9, Indent::new(0, 69)),
1982                                      &wrap_normalize_config).unwrap();
1983        assert_eq!(expected, comment);
1984
1985        let comment = rewrite_comment("/*   trimmed    */",
1986                                      true,
1987                                      Shape::legacy(100, Indent::new(0, 100)),
1988                                      &wrap_normalize_config).unwrap();
1989        assert_eq!("/* trimmed */", comment);
1990
1991        // Check that different comment style are properly recognised.
1992        let comment = rewrite_comment(r#"/// test1
1993                                         /// test2
1994                                         /*
1995                                          * test3
1996                                          */"#,
1997                                      false,
1998                                      Shape::legacy(100, Indent::new(0, 0)),
1999                                      &wrap_normalize_config).unwrap();
2000        assert_eq!("/// test1\n/// test2\n// test3", comment);
2001
2002        // Check that the blank line marks the end of a commented paragraph.
2003        let comment = rewrite_comment(r#"// test1
2004
2005                                         // test2"#,
2006                                      false,
2007                                      Shape::legacy(100, Indent::new(0, 0)),
2008                                      &wrap_normalize_config).unwrap();
2009        assert_eq!("// test1\n\n// test2", comment);
2010
2011        // Check that the blank line marks the end of a custom-commented paragraph.
2012        let comment = rewrite_comment(r#"//@ test1
2013
2014                                         //@ test2"#,
2015                                      false,
2016                                      Shape::legacy(100, Indent::new(0, 0)),
2017                                      &wrap_normalize_config).unwrap();
2018        assert_eq!("//@ test1\n\n//@ test2", comment);
2019
2020        // Check that bare lines are just indented but otherwise left unchanged.
2021        let comment = rewrite_comment(r#"// test1
2022                                         /*
2023                                           a bare line!
2024
2025                                                another bare line!
2026                                          */"#,
2027                                      false,
2028                                      Shape::legacy(100, Indent::new(0, 0)),
2029                                      &wrap_config).unwrap();
2030        assert_eq!("// test1\n/*\n a bare line!\n\n      another bare line!\n*/", comment);
2031    }
2032
2033    // This is probably intended to be a non-test fn, but it is not used.
2034    // We should keep this around unless it helps us test stuff to remove it.
2035    fn uncommented(text: &str) -> String {
2036        CharClasses::new(text.chars())
2037            .filter_map(|(s, c)| match s {
2038                FullCodeCharKind::Normal | FullCodeCharKind::InString => Some(c),
2039                _ => None,
2040            })
2041            .collect()
2042    }
2043
2044    #[test]
2045    fn test_uncommented() {
2046        assert_eq!(&uncommented("abc/*...*/"), "abc");
2047        assert_eq!(
2048            &uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
2049            "..ac\n"
2050        );
2051        assert_eq!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
2052    }
2053
2054    #[test]
2055    fn test_contains_comment() {
2056        assert_eq!(contains_comment("abc"), false);
2057        assert_eq!(contains_comment("abc // qsdf"), true);
2058        assert_eq!(contains_comment("abc /* kqsdf"), true);
2059        assert_eq!(contains_comment("abc \" /* */\" qsdf"), false);
2060    }
2061
2062    #[test]
2063    fn test_find_uncommented() {
2064        fn check(haystack: &str, needle: &str, expected: Option<usize>) {
2065            assert_eq!(expected, haystack.find_uncommented(needle));
2066        }
2067
2068        check("/*/ */test", "test", Some(6));
2069        check("//test\ntest", "test", Some(7));
2070        check("/* comment only */", "whatever", None);
2071        check(
2072            "/* comment */ some text /* more commentary */ result",
2073            "result",
2074            Some(46),
2075        );
2076        check("sup // sup", "p", Some(2));
2077        check("sup", "x", None);
2078        check(r#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
2079        check("/*sup yo? \n sup*/ sup", "p", Some(20));
2080        check("hel/*lohello*/lo", "hello", None);
2081        check("acb", "ab", None);
2082        check(",/*A*/ ", ",", Some(0));
2083        check("abc", "abc", Some(0));
2084        check("/* abc */", "abc", None);
2085        check("/**/abc/* */", "abc", Some(4));
2086        check("\"/* abc */\"", "abc", Some(4));
2087        check("\"/* abc", "abc", Some(4));
2088    }
2089
2090    #[test]
2091    fn test_filter_normal_code() {
2092        let s = r#"
2093fn main() {
2094    println!("hello, world");
2095}
2096"#;
2097        assert_eq!(s, filter_normal_code(s));
2098        let s_with_comment = r#"
2099fn main() {
2100    // hello, world
2101    println!("hello, world");
2102}
2103"#;
2104        assert_eq!(s, filter_normal_code(s_with_comment));
2105    }
2106
2107    #[test]
2108    fn test_itemized_block_first_line_handling() {
2109        fn run_test(
2110            test_input: &str,
2111            expected_line: &str,
2112            expected_indent: usize,
2113            expected_opener: &str,
2114            expected_line_start: &str,
2115        ) {
2116            let block = ItemizedBlock::new(test_input).unwrap();
2117            assert_eq!(1, block.lines.len(), "test_input: {test_input:?}");
2118            assert_eq!(expected_line, &block.lines[0], "test_input: {test_input:?}");
2119            assert_eq!(expected_indent, block.indent, "test_input: {test_input:?}");
2120            assert_eq!(expected_opener, &block.opener, "test_input: {test_input:?}");
2121            assert_eq!(
2122                expected_line_start, &block.line_start,
2123                "test_input: {test_input:?}"
2124            );
2125        }
2126
2127        run_test("- foo", "foo", 2, "- ", "  ");
2128        run_test("* foo", "foo", 2, "* ", "  ");
2129        run_test("> foo", "foo", 2, "> ", "> ");
2130
2131        run_test("1. foo", "foo", 3, "1. ", "   ");
2132        run_test("12. foo", "foo", 4, "12. ", "    ");
2133        run_test("1) foo", "foo", 3, "1) ", "   ");
2134        run_test("12) foo", "foo", 4, "12) ", "    ");
2135
2136        run_test("    - foo", "foo", 6, "    - ", "      ");
2137
2138        // https://spec.commonmark.org/0.30 says: "A start number may begin with 0s":
2139        run_test("0. foo", "foo", 3, "0. ", "   ");
2140        run_test("01. foo", "foo", 4, "01. ", "    ");
2141    }
2142
2143    #[test]
2144    fn test_itemized_block_nonobvious_markers_are_rejected() {
2145        let test_inputs = vec![
2146            // Non-numeric item markers (e.g. `a.` or `iv.`) are not allowed by
2147            // https://spec.commonmark.org/0.30/#ordered-list-marker. We also note that allowing
2148            // them would risk misidentifying regular words as item markers. See also the
2149            // discussion in https://talk.commonmark.org/t/blank-lines-before-lists-revisited/1990
2150            "word.  rest of the paragraph.",
2151            "a.  maybe this is a list item?  maybe not?",
2152            "iv.  maybe this is a list item?  maybe not?",
2153            // Numbers with 3 or more digits are not recognized as item markers, to avoid
2154            // formatting the following example as a list:
2155            //
2156            // ```
2157            // The Captain died in
2158            // 1868. He was buried in...
2159            // ```
2160            "123.  only 2-digit numbers are recognized as item markers.",
2161            // Parens:
2162            "123)  giving some coverage to parens as well.",
2163            "a)  giving some coverage to parens as well.",
2164            // https://spec.commonmark.org/0.30 says that "at least one space or tab is needed
2165            // between the list marker and any following content":
2166            "1.Not a list item.",
2167            "1.2.3. Not a list item.",
2168            "1)Not a list item.",
2169            "-Not a list item.",
2170            "+Not a list item.",
2171            "+1 not a list item.",
2172            // https://spec.commonmark.org/0.30 says: "A start number may not be negative":
2173            "-1. Not a list item.",
2174            "-1 Not a list item.",
2175            // Marker without prefix are not recognized as item markers:
2176            ".   Not a list item.",
2177            ")   Not a list item.",
2178        ];
2179        for line in test_inputs.iter() {
2180            let maybe_block = ItemizedBlock::new(line);
2181            assert!(
2182                maybe_block.is_none(),
2183                "The following line shouldn't be classified as a list item: {line}"
2184            );
2185        }
2186    }
2187}