rust_format/
replace.rs

1#![cfg(feature = "post_process")]
2
3use std::borrow::Cow;
4use std::{cmp, slice};
5
6use crate::Error;
7
8const BLANK_START: &[&[u8]] = &[b"lank_", b"!", b"("];
9const BLANK_END: &[&[u8]] = &[b";"];
10const COMMENT_START: &[&[u8]] = &[b"omment_", b"!", b"("];
11const COMMENT_END: &[&[u8]] = &[b")", b";"];
12const COMMENT_END2: &[&[u8]] = &[b";"];
13const DOC_BLOCK_START: &[&[u8]] = &[b"[", b"doc", b"="];
14const DOC_BLOCK_END: &[&[u8]] = &[b"]"];
15
16const EMPTY_COMMENT: &str = "//";
17const COMMENT: &str = "// ";
18const DOC_COMMENT: &str = "///";
19const LF_STR: &str = "\n";
20const CRLF_STR: &str = "\r\n";
21
22const CR: u8 = b'\r';
23const LF: u8 = b'\n';
24
25const MIN_BUFF_SIZE: usize = 128;
26
27// In order to replace the markers there were a few options:
28// 1. Create a full special purpose Rust lexer, replace the tokens we want as we go, write it back
29// 2. Find the markers via regular string search, copy everything up to that point, replace, repeat
30// 3. A hybrid of 1 and 2
31//
32// The problem with #1 is it is hugely overkill - we are only interested in 3 markers
33// The problem with #2 is that it would find markers in strings and comments - likely not an issue, but it bothered me
34// (and also we generalize the marker replacement code also for doc blocks, which someone could have commented out)
35// #3 is what is below - it does basic lexing of Rust comments and strings for the purposes of skipping them only. It
36// understands just enough to do the job. The weird part is it literally searches inside all other constructs, but the
37// probability of a false positive while low in comments and strings, is likely very close to zero anywhere else, so
38// I think this is a good compromise. Regardless, the user should be advised to not use `_comment_!(` or `_blank_!(`
39// anywhere in the source file other than where they want markers.
40
41struct CopyingCursor<'a> {
42    start_idx: usize,
43    curr_idx: usize,
44    curr: u8,
45
46    // We can iterate as if this were raw bytes since we are only matching ASCII. We preserve
47    // any unicode, however, and copy it verbatim
48    iter: slice::Iter<'a, u8>,
49    source: &'a str,
50    buffer: String,
51}
52
53impl<'a> CopyingCursor<'a> {
54    fn new(source: &'a str) -> Option<Self> {
55        // Better to be too large than not large enough
56        let buffer = String::with_capacity(cmp::max(source.len() * 2, MIN_BUFF_SIZE));
57        let mut iter = source.as_bytes().iter();
58
59        iter.next().map(|&ch| Self {
60            start_idx: 0,
61            curr_idx: 0,
62            curr: ch,
63            iter,
64            source,
65            buffer,
66        })
67    }
68
69    #[inline]
70    fn next(&mut self) -> Option<u8> {
71        self.iter.next().map(|&ch| {
72            self.curr_idx += 1;
73            self.curr = ch;
74            ch
75        })
76    }
77
78    #[inline]
79    fn copy_to_marker(&mut self, marker: usize, new_start_idx: usize) {
80        if marker > self.start_idx {
81            // Copy exclusive of marker position
82            self.buffer.push_str(&self.source[self.start_idx..marker]);
83        }
84        self.start_idx = new_start_idx;
85    }
86
87    fn into_buffer(mut self) -> Cow<'a, str> {
88        // We have done some work
89        if self.start_idx > 0 {
90            // Last write to ensure everything is copied
91            self.copy_to_marker(self.curr_idx + 1, self.curr_idx + 1);
92
93            self.buffer.shrink_to_fit();
94            Cow::Owned(self.buffer)
95        // We have done nothing - just return original str
96        } else {
97            Cow::Borrowed(self.source)
98        }
99    }
100
101    fn skip_block_comment(&mut self) {
102        enum State {
103            InComment,
104            MaybeStarting,
105            MaybeEnding,
106        }
107
108        let mut nest_level = 1;
109        let mut state = State::InComment;
110
111        while let Some(ch) = self.next() {
112            match (ch, state) {
113                (b'*', State::InComment) => {
114                    state = State::MaybeEnding;
115                }
116                (b'/', State::MaybeEnding) => {
117                    nest_level -= 1;
118                    if nest_level == 0 {
119                        break;
120                    }
121                    state = State::InComment;
122                }
123                (b'*', State::MaybeStarting) => {
124                    nest_level += 1;
125                    state = State::InComment;
126                }
127                (b'/', State::InComment) => {
128                    state = State::MaybeStarting;
129                }
130                (_, _) => {
131                    state = State::InComment;
132                }
133            }
134        }
135    }
136
137    fn try_skip_comment(&mut self) -> bool {
138        match self.next() {
139            // Line comment of some form (we don't care which)
140            Some(b'/') => {
141                while let Some(ch) = self.next() {
142                    if ch == b'\n' {
143                        break;
144                    }
145                }
146
147                true
148            }
149            // Block comment of some form (we don't care which)
150            Some(b'*') => {
151                self.skip_block_comment();
152                true
153            }
154            // Not a comment or EOF, etc. - should be impossible in valid code
155            _ => false,
156        }
157    }
158
159    fn skip_string(&mut self) {
160        let mut in_escape = false;
161
162        while let Some(ch) = self.next() {
163            match ch {
164                b'"' if !in_escape => break,
165                b'\\' if !in_escape => in_escape = true,
166                _ if in_escape => in_escape = false,
167                _ => {}
168            }
169        }
170    }
171
172    fn try_skip_raw_string(&mut self) -> bool {
173        // First, match the entry sequence to the raw string and collect # of pads present
174        let pads = match self.next() {
175            Some(b'#') => {
176                let mut pads = 1;
177
178                while let Some(ch) = self.next() {
179                    match ch {
180                        b'#' => {
181                            pads += 1;
182                        }
183                        b'"' => break,
184                        // Not a raw string
185                        _ => return false,
186                    }
187                }
188
189                pads
190            }
191            Some(b'"') => 0,
192            _ => return false,
193        };
194
195        #[derive(Clone, Copy)]
196        enum State {
197            InRawComment,
198            MaybeEndingComment(i32),
199        }
200
201        let mut state = State::InRawComment;
202
203        // Loop over the raw string looking for ending sequence and count pads until we have
204        // the correct # of them
205        while let Some(ch) = self.next() {
206            match (ch, state) {
207                (b'"', State::InRawComment) if pads == 0 => break,
208                (b'"', State::InRawComment) => state = State::MaybeEndingComment(0),
209                (b'#', State::MaybeEndingComment(pads_seen)) => {
210                    let pads_seen = pads_seen + 1;
211                    if pads_seen == pads {
212                        break;
213                    }
214                    state = State::MaybeEndingComment(pads_seen);
215                }
216                (_, _) => {
217                    state = State::InRawComment;
218                }
219            }
220        }
221
222        true
223    }
224
225    #[inline]
226    fn skip_blank_param(&mut self) -> Result<(), Error> {
227        while let Some(ch) = self.next() {
228            if ch == b')' {
229                return Ok(());
230            }
231        }
232
233        // EOF
234        Err(Error::BadSourceCode("Unexpected end of input".to_string()))
235    }
236
237    fn try_skip_string(&mut self) -> Result<Option<u8>, Error> {
238        while let Some(ch) = self.next() {
239            if Self::is_whitespace(ch) {
240                continue;
241            }
242
243            return match ch {
244                // Regular string
245                b'"' => {
246                    self.skip_string();
247                    Ok(None)
248                }
249                // Raw string
250                b'r' => {
251                    if self.try_skip_raw_string() {
252                        Ok(None)
253                    } else {
254                        Err(Error::BadSourceCode("Bad raw string".to_string()))
255                    }
256                }
257                // Something else
258                ch => Ok(Some(ch)),
259            };
260        }
261
262        // EOF
263        Err(Error::BadSourceCode("Unexpected end of input".to_string()))
264    }
265
266    // TODO: Was planning to match values here (but we only recognize ASCII atm):
267    // https://github.com/rust-lang/rust/blob/38e0ae590caab982a4305da58a0a62385c2dd880/compiler/rustc_lexer/src/lib.rs#L245
268    // We could switch back to UTF8 since we have been matching valid ASCII up to this point, but atm
269    // any unicode whitespace will make it not match (not sure any code formatter preserves non-ASCII whitespace?)
270    // For now, users should use NO whitespace and let the code formatters add any, if needed. I suspect
271    // they will not add any non-ASCII whitespace on their own at min, but likely just ' ', '\n', and '\r'
272    //
273    // Code points we don't handle that we should (for future ref):
274    // Code point 0x0085 == 0xC285
275    // Code point 0x200E == 0xE2808E
276    // Code point 0x200F == 0xE2808F
277    // Code point 0x2028 == 0xE280A8
278    // Code point 0x2029 == 0xE280A9
279    #[inline]
280    fn is_whitespace(ch: u8) -> bool {
281        matches!(ch, b' ' | b'\n' | b'\r' | b'\t' | b'\x0b' | b'\x0c')
282    }
283
284    fn try_ws_matches(&mut self, slices: &[&[u8]], allow_whitespace_first: bool) -> bool {
285        let mut allow_whitespace = allow_whitespace_first;
286
287        'top: for &sl in slices {
288            // Panic safety: it is pointless for us to pass in a blank slice, don't do that
289            let first_ch = sl[0];
290
291            while let Some(ch) = self.next() {
292                // This is what we were looking for, now match the rest (if needed)
293                if ch == first_ch {
294                    // Panic safety: it is pointless for us to pass in a blank slice, don't do that
295                    let remainder = &sl[1..];
296
297                    if !remainder.is_empty() && !self.try_match(remainder) {
298                        return false;
299                    }
300                    allow_whitespace = true;
301                    continue 'top;
302                } else if allow_whitespace && Self::is_whitespace(ch) {
303                    // no op
304                } else {
305                    return false;
306                }
307            }
308
309            // Premature EOF
310            return false;
311        }
312
313        // If we can exhaust the iterator then they all must have matched
314        true
315    }
316
317    fn try_match(&mut self, sl: &[u8]) -> bool {
318        let iter = sl.iter();
319
320        for &ch in iter {
321            if self.next().is_none() {
322                // This isn't great as it will reevaluate the last char - 'b' or 'c' in the main loop,
323                // but since those aren't top level it will exit at the bottom of the main loop gracefully
324                return false;
325            }
326
327            if self.curr != ch {
328                return false;
329            }
330        }
331
332        // If we can exhaust the iterator then it must have matched
333        true
334    }
335
336    #[inline]
337    fn detect_line_ending(&mut self) -> Option<&'static str> {
338        match self.next() {
339            Some(CR) => match self.next() {
340                Some(LF) => Some(CRLF_STR),
341                _ => None,
342            },
343            Some(LF) => Some(LF_STR),
344            _ => None,
345        }
346    }
347
348    #[inline]
349    fn push_spaces(spaces: usize, buffer: &mut String) {
350        for _ in 0..spaces {
351            buffer.push(' ');
352        }
353    }
354
355    fn process_blanks(
356        _spaces: usize,
357        buffer: &mut String,
358        num: &str,
359        ending: &str,
360    ) -> Result<(), Error> {
361        // Single blank line
362        if num.is_empty() {
363            buffer.push_str(ending);
364        // Multiple blank lines
365        } else {
366            let num: syn::LitInt = syn::parse_str(num)?;
367            let blanks: u32 = num.base10_parse()?;
368
369            for _ in 0..blanks {
370                buffer.push_str(ending);
371            }
372        }
373
374        Ok(())
375    }
376
377    fn process_comments(
378        spaces: usize,
379        buffer: &mut String,
380        s: &str,
381        ending: &str,
382    ) -> Result<(), Error> {
383        // Single blank comment
384        if s.is_empty() {
385            Self::push_spaces(spaces, buffer);
386            buffer.push_str(EMPTY_COMMENT);
387            buffer.push_str(ending);
388        // Multiple comments
389        } else {
390            let s: syn::LitStr = syn::parse_str(s)?;
391            let comment = s.value();
392
393            // Blank comment after parsing
394            if comment.is_empty() {
395                Self::push_spaces(spaces, buffer);
396                buffer.push_str(EMPTY_COMMENT);
397                buffer.push_str(ending);
398            } else {
399                for line in comment.lines() {
400                    Self::push_spaces(spaces, buffer);
401
402                    if line.is_empty() {
403                        buffer.push_str(EMPTY_COMMENT);
404                    } else {
405                        buffer.push_str(COMMENT);
406                        buffer.push_str(line);
407                    }
408
409                    buffer.push_str(ending);
410                }
411            }
412        }
413
414        Ok(())
415    }
416
417    // This is slightly different than comment in that we don't prepend a space but need to translate
418    // the doc block literally (#[doc = "test"] == ///test <-- no prepended space)
419    fn process_doc_block(
420        spaces: usize,
421        buffer: &mut String,
422        s: &str,
423        ending: &str,
424    ) -> Result<(), Error> {
425        // Single blank comment
426        if s.is_empty() {
427            Self::push_spaces(spaces, buffer);
428            buffer.push_str(DOC_COMMENT);
429            buffer.push_str(ending);
430        // Multiple comments
431        } else {
432            let s: syn::LitStr = syn::parse_str(s)?;
433            let comment = s.value();
434
435            // Blank comment after parsing
436            if comment.is_empty() {
437                Self::push_spaces(spaces, buffer);
438                buffer.push_str(DOC_COMMENT);
439                buffer.push_str(ending);
440            } else {
441                for line in comment.lines() {
442                    Self::push_spaces(spaces, buffer);
443                    buffer.push_str(DOC_COMMENT);
444                    buffer.push_str(line);
445                    buffer.push_str(ending);
446                }
447            }
448        }
449
450        Ok(())
451    }
452
453    fn try_match_prefixes(
454        &mut self,
455        indent: usize,
456        chars_matched: usize,
457        prefixes: &[&[u8]],
458        allow_ws_first: bool,
459    ) -> Option<(usize, usize)> {
460        // We already matched X chars before we got here (but didn't 'next()' after last match so minus 1)
461        let mark_start_ident = self.curr_idx - ((chars_matched + indent) - 1);
462
463        if self.try_ws_matches(prefixes, allow_ws_first) {
464            let mark_start_value = self.curr_idx + 1;
465            Some((mark_start_ident, mark_start_value))
466        } else {
467            None
468        }
469    }
470
471    fn try_replace<F>(
472        &mut self,
473        spaces: usize,
474        chars_matched: usize,
475        suffixes: &[&[u8]],
476        mark_start_ident: usize,
477        mark_start_value: usize,
478        f: F,
479    ) -> Result<(), Error>
480    where
481        F: FnOnce(usize, &mut String, &str, &str) -> Result<(), Error>,
482    {
483        // End of value (exclusive)
484        let mark_end_value = self.curr_idx + (1 - chars_matched);
485
486        if !self.try_ws_matches(suffixes, true) {
487            return Err(Error::BadSourceCode(
488                "Unable to match suffix on doc block or marker.".to_string(),
489            ));
490        }
491
492        if let Some(ending) = self.detect_line_ending() {
493            // Mark end of ident here (inclusive)
494            let mark_end_ident = self.curr_idx + 1;
495
496            // Copy everything up until this marker
497            self.copy_to_marker(mark_start_ident, mark_end_ident);
498
499            // Parse and output
500            f(
501                spaces,
502                &mut self.buffer,
503                &self.source[mark_start_value..mark_end_value],
504                ending,
505            )?;
506            Ok(())
507        } else {
508            Err(Error::BadSourceCode("Expected CR or LF".to_string()))
509        }
510    }
511
512    fn try_replace_blank_marker(&mut self, spaces: usize) -> Result<bool, Error> {
513        // 6 or 7 sections to match: _blank_ ! ( [int] ) ; CRLF|LF
514
515        match self.try_match_prefixes(spaces, 2, BLANK_START, false) {
516            Some((ident_start, value_start)) => {
517                self.skip_blank_param()?;
518
519                self.try_replace(
520                    spaces,
521                    1,
522                    BLANK_END,
523                    ident_start,
524                    value_start,
525                    CopyingCursor::process_blanks,
526                )?;
527                Ok(true)
528            }
529            None => Ok(false),
530        }
531    }
532
533    fn try_replace_comment_marker(&mut self, spaces: usize) -> Result<bool, Error> {
534        // 6 or 7 sections to match: _comment_ ! ( [string] ) ; CRLF|LF
535
536        match self.try_match_prefixes(spaces, 2, COMMENT_START, false) {
537            Some((ident_start, value_start)) => {
538                // Make sure it is empty or a string
539                let (matched, suffix) = match self.try_skip_string()? {
540                    // String
541                    None => (0, COMMENT_END),
542                    // Empty
543                    Some(b')') => (1, COMMENT_END2),
544                    Some(ch) => {
545                        return Err(Error::BadSourceCode(format!(
546                            "Expected ')' or string, but got: {}",
547                            ch as char
548                        )))
549                    }
550                };
551
552                self.try_replace(
553                    spaces,
554                    matched,
555                    suffix,
556                    ident_start,
557                    value_start,
558                    CopyingCursor::process_comments,
559                )?;
560                Ok(true)
561            }
562            None => Ok(false),
563        }
564    }
565
566    fn try_replace_doc_block(&mut self, spaces: usize) -> Result<bool, Error> {
567        // 7 sections to match: # [ doc = <string> ] CRLF|LF
568
569        match self.try_match_prefixes(spaces, 1, DOC_BLOCK_START, true) {
570            Some((ident_start, value_start)) => {
571                // Make sure it is a string
572                match self.try_skip_string()? {
573                    // String
574                    None => {
575                        self.try_replace(
576                            spaces,
577                            0,
578                            DOC_BLOCK_END,
579                            ident_start,
580                            value_start,
581                            CopyingCursor::process_doc_block,
582                        )?;
583                        Ok(true)
584                    }
585                    Some(ch) => Err(Error::BadSourceCode(format!(
586                        "Expected string, but got: {}",
587                        ch as char
588                    ))),
589                }
590            }
591            None => Ok(false),
592        }
593    }
594}
595
596pub(crate) fn replace_markers(s: &str, replace_doc_blocks: bool) -> Result<Cow<str>, Error> {
597    match CopyingCursor::new(s) {
598        Some(mut cursor) => {
599            let mut indent = 0;
600
601            loop {
602                match cursor.curr {
603                    // Possible raw string
604                    b'r' => {
605                        indent = 0;
606                        if !cursor.try_skip_raw_string() {
607                            continue;
608                        }
609                    }
610                    // Regular string
611                    b'\"' => {
612                        indent = 0;
613                        cursor.skip_string()
614                    }
615                    // Possible comment
616                    b'/' => {
617                        indent = 0;
618                        if !cursor.try_skip_comment() {
619                            continue;
620                        }
621                    }
622                    // Possible special ident (_comment!_ or _blank!_)
623                    b'_' => {
624                        if cursor.next().is_none() {
625                            break;
626                        }
627
628                        match cursor.curr {
629                            // Possible blank marker
630                            b'b' => {
631                                if !cursor.try_replace_blank_marker(indent)? {
632                                    indent = 0;
633                                    continue;
634                                }
635                            }
636                            // Possible comment marker
637                            b'c' => {
638                                if !cursor.try_replace_comment_marker(indent)? {
639                                    indent = 0;
640                                    continue;
641                                }
642                            }
643                            // Nothing we are interested in
644                            _ => {
645                                indent = 0;
646                                continue;
647                            }
648                        }
649
650                        indent = 0;
651                    }
652                    // Possible doc block
653                    b'#' if replace_doc_blocks => {
654                        if !cursor.try_replace_doc_block(indent)? {
655                            indent = 0;
656                            continue;
657                        }
658
659                        indent = 0;
660                    }
661                    // Count spaces in front of our three special replacements
662                    b' ' => {
663                        indent += 1;
664                    }
665                    // Anything else
666                    _ => {
667                        indent = 0;
668                    }
669                }
670
671                if cursor.next().is_none() {
672                    break;
673                }
674            }
675
676            Ok(cursor.into_buffer())
677        }
678        // Empty file
679        None => Ok(Cow::Borrowed(s)),
680    }
681}
682
683// *** Tests ***
684
685#[cfg(test)]
686mod tests {
687    use std::borrow::Cow;
688
689    use pretty_assertions::assert_eq;
690
691    use crate::replace::replace_markers;
692    use crate::Error;
693
694    #[test]
695    fn blank() {
696        let source = "";
697
698        let actual = replace_markers(source, false).unwrap();
699        let expected = source;
700
701        assert_eq!(expected, actual);
702        assert!(matches!(actual, Cow::Borrowed(_)));
703    }
704
705    #[test]
706    fn no_replacements() {
707        let source = r####"// _comment!_("comment");
708
709/* /* nested comment */ */
710        
711/// This is a main function
712fn main() {
713    println!("hello world");
714    println!(r##"hello raw world!"##);
715}
716_blank!_;
717"####;
718
719        let actual = replace_markers(source, false).unwrap();
720        let expected = source;
721
722        assert_eq!(expected, actual);
723        assert!(matches!(actual, Cow::Borrowed(_)));
724    }
725
726    #[test]
727    fn replace_comments() {
728        let source = r####"// _comment!_("comment");
729
730/* /* nested comment */ */
731_comment_!("comment 1\n\ncomment 2");
732_comment_!("test");
733_comment!("skip this");
734/// This is a main function
735fn main() {
736    println!(r##"hello raw world!"##);
737    _comment_!(r"");
738    _comment_!();
739    println!("hello \nworld");
740}
741
742   _comment_ !
743( r#"This is two
744comments"# )
745;
746_blank!_;
747"####;
748
749        let actual = replace_markers(source, false).unwrap();
750        let expected = r####"// _comment!_("comment");
751
752/* /* nested comment */ */
753// comment 1
754//
755// comment 2
756// test
757_comment!("skip this");
758/// This is a main function
759fn main() {
760    println!(r##"hello raw world!"##);
761    //
762    //
763    println!("hello \nworld");
764}
765
766   // This is two
767   // comments
768_blank!_;
769"####;
770
771        assert_eq!(expected, actual);
772    }
773
774    #[test]
775    fn replace_blanks() {
776        let source = r####"// _blank!_(5);
777
778/* /* nested comment */ */
779_blank_!(2);
780_blank!_("skip this");
781#[doc = "This is a main function"]
782fn main() {
783    let r#test = "hello";
784    println!(r"hello raw world!");
785    _blank_!();
786    println!("hello \nworld");
787}
788
789      _blank_
790!(
7912
792);
793_blank!_;
794"####;
795
796        let actual = replace_markers(source, false).unwrap();
797        let expected = r####"// _blank!_(5);
798
799/* /* nested comment */ */
800
801
802_blank!_("skip this");
803#[doc = "This is a main function"]
804fn main() {
805    let r#test = "hello";
806    println!(r"hello raw world!");
807
808    println!("hello \nworld");
809}
810
811
812
813_blank!_;
814"####;
815
816        assert_eq!(expected, actual);
817    }
818
819    #[test]
820    fn replace_doc_blocks() {
821        let source = r####"// _blank!_(5);
822
823/* not a nested comment */
824#[doc = r#" This is a main function"#]
825#[doc = r#" This is two doc
826 comments"#]
827#[cfg(feature = "main")]
828#[doc(hidden)]
829fn main() {
830    println!(r##"hello raw world!"##);
831    #[doc = ""]
832    println!("hello \nworld");
833}
834
835#    [
836doc
837 = 
838 " this is\n\n three doc comments"
839 
840 ]
841fn test() {
842}
843_blank!_;
844"####;
845
846        let actual = replace_markers(source, true).unwrap();
847        let expected = r####"// _blank!_(5);
848
849/* not a nested comment */
850/// This is a main function
851/// This is two doc
852/// comments
853#[cfg(feature = "main")]
854#[doc(hidden)]
855fn main() {
856    println!(r##"hello raw world!"##);
857    ///
858    println!("hello \nworld");
859}
860
861/// this is
862///
863/// three doc comments
864fn test() {
865}
866_blank!_;
867"####;
868
869        assert_eq!(expected, actual);
870    }
871
872    #[test]
873    fn replace_crlf() {
874        let source = "_blank_!(2);\r\n";
875        let actual = replace_markers(source, false).unwrap();
876
877        let expected = "\r\n\r\n";
878        assert_eq!(expected, actual);
879    }
880
881    #[test]
882    fn marker_end_after_prefix() {
883        assert!(matches!(
884            replace_markers("_blank_!(", false),
885            Err(Error::BadSourceCode(_))
886        ));
887    }
888
889    #[test]
890    fn marker_param_not_string() {
891        assert!(matches!(
892            replace_markers("_comment_!(blah);\n", false),
893            Err(Error::BadSourceCode(_))
894        ));
895    }
896
897    #[test]
898    fn marker_bad_suffix() {
899        assert!(matches!(
900            replace_markers("_comment_!(\"blah\"];\n", false),
901            Err(Error::BadSourceCode(_))
902        ));
903    }
904
905    #[test]
906    fn doc_block_string_not_closed() {
907        assert!(matches!(
908            replace_markers("#[doc = \"test]\n", true),
909            Err(Error::BadSourceCode(_))
910        ));
911    }
912}