• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Utils for extracting, inspecting or transforming source code
2 
3 #![allow(clippy::module_name_repetitions)]
4 
5 use rustc_data_structures::sync::Lrc;
6 use rustc_errors::Applicability;
7 use rustc_hir::{BlockCheckMode, Expr, ExprKind, UnsafeSource};
8 use rustc_lint::{LateContext, LintContext};
9 use rustc_session::Session;
10 use rustc_span::source_map::{original_sp, SourceMap};
11 use rustc_span::{hygiene, SourceFile};
12 use rustc_span::{BytePos, Pos, Span, SpanData, SyntaxContext, DUMMY_SP};
13 use std::borrow::Cow;
14 use std::ops::Range;
15 
16 /// A type which can be converted to the range portion of a `Span`.
17 pub trait SpanRange {
into_range(self) -> Range<BytePos>18     fn into_range(self) -> Range<BytePos>;
19 }
20 impl SpanRange for Span {
into_range(self) -> Range<BytePos>21     fn into_range(self) -> Range<BytePos> {
22         let data = self.data();
23         data.lo..data.hi
24     }
25 }
26 impl SpanRange for SpanData {
into_range(self) -> Range<BytePos>27     fn into_range(self) -> Range<BytePos> {
28         self.lo..self.hi
29     }
30 }
31 impl SpanRange for Range<BytePos> {
into_range(self) -> Range<BytePos>32     fn into_range(self) -> Range<BytePos> {
33         self
34     }
35 }
36 
37 pub struct SourceFileRange {
38     pub sf: Lrc<SourceFile>,
39     pub range: Range<usize>,
40 }
41 impl SourceFileRange {
42     /// Attempts to get the text from the source file. This can fail if the source text isn't
43     /// loaded.
as_str(&self) -> Option<&str>44     pub fn as_str(&self) -> Option<&str> {
45         self.sf.src.as_ref().and_then(|x| x.get(self.range.clone()))
46     }
47 }
48 
49 /// Gets the source file, and range in the file, of the given span. Returns `None` if the span
50 /// extends through multiple files, or is malformed.
get_source_text(cx: &impl LintContext, sp: impl SpanRange) -> Option<SourceFileRange>51 pub fn get_source_text(cx: &impl LintContext, sp: impl SpanRange) -> Option<SourceFileRange> {
52     fn f(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
53         let start = sm.lookup_byte_offset(sp.start);
54         let end = sm.lookup_byte_offset(sp.end);
55         if !Lrc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
56             return None;
57         }
58         let range = start.pos.to_usize()..end.pos.to_usize();
59         Some(SourceFileRange { sf: start.sf, range })
60     }
61     f(cx.sess().source_map(), sp.into_range())
62 }
63 
64 /// Like `snippet_block`, but add braces if the expr is not an `ExprKind::Block`.
expr_block<T: LintContext>( cx: &T, expr: &Expr<'_>, outer: SyntaxContext, default: &str, indent_relative_to: Option<Span>, app: &mut Applicability, ) -> String65 pub fn expr_block<T: LintContext>(
66     cx: &T,
67     expr: &Expr<'_>,
68     outer: SyntaxContext,
69     default: &str,
70     indent_relative_to: Option<Span>,
71     app: &mut Applicability,
72 ) -> String {
73     let (code, from_macro) = snippet_block_with_context(cx, expr.span, outer, default, indent_relative_to, app);
74     if !from_macro &&
75         let ExprKind::Block(block, _) = expr.kind &&
76         block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
77     {
78         format!("{code}")
79     } else {
80         // FIXME: add extra indent for the unsafe blocks:
81         //     original code:   unsafe { ... }
82         //     result code:     { unsafe { ... } }
83         //     desired code:    {\n  unsafe { ... }\n}
84         format!("{{ {code} }}")
85     }
86 }
87 
88 /// Returns a new Span that extends the original Span to the first non-whitespace char of the first
89 /// line.
90 ///
91 /// ```rust,ignore
92 ///     let x = ();
93 /// //          ^^
94 /// // will be converted to
95 ///     let x = ();
96 /// //  ^^^^^^^^^^
97 /// ```
first_line_of_span<T: LintContext>(cx: &T, span: Span) -> Span98 pub fn first_line_of_span<T: LintContext>(cx: &T, span: Span) -> Span {
99     first_char_in_first_line(cx, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
100 }
101 
first_char_in_first_line<T: LintContext>(cx: &T, span: Span) -> Option<BytePos>102 fn first_char_in_first_line<T: LintContext>(cx: &T, span: Span) -> Option<BytePos> {
103     let line_span = line_span(cx, span);
104     snippet_opt(cx, line_span).and_then(|snip| {
105         snip.find(|c: char| !c.is_whitespace())
106             .map(|pos| line_span.lo() + BytePos::from_usize(pos))
107     })
108 }
109 
110 /// Extends the span to the beginning of the spans line, incl. whitespaces.
111 ///
112 /// ```rust
113 ///        let x = ();
114 /// //             ^^
115 /// // will be converted to
116 ///        let x = ();
117 /// // ^^^^^^^^^^^^^^
118 /// ```
line_span<T: LintContext>(cx: &T, span: Span) -> Span119 fn line_span<T: LintContext>(cx: &T, span: Span) -> Span {
120     let span = original_sp(span, DUMMY_SP);
121     let source_map_and_line = cx.sess().source_map().lookup_line(span.lo()).unwrap();
122     let line_no = source_map_and_line.line;
123     let line_start = source_map_and_line.sf.lines(|lines| lines[line_no]);
124     span.with_lo(line_start)
125 }
126 
127 /// Returns the indentation of the line of a span
128 ///
129 /// ```rust,ignore
130 /// let x = ();
131 /// //      ^^ -- will return 0
132 ///     let x = ();
133 /// //          ^^ -- will return 4
134 /// ```
indent_of<T: LintContext>(cx: &T, span: Span) -> Option<usize>135 pub fn indent_of<T: LintContext>(cx: &T, span: Span) -> Option<usize> {
136     snippet_opt(cx, line_span(cx, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
137 }
138 
139 /// Gets a snippet of the indentation of the line of a span
snippet_indent<T: LintContext>(cx: &T, span: Span) -> Option<String>140 pub fn snippet_indent<T: LintContext>(cx: &T, span: Span) -> Option<String> {
141     snippet_opt(cx, line_span(cx, span)).map(|mut s| {
142         let len = s.len() - s.trim_start().len();
143         s.truncate(len);
144         s
145     })
146 }
147 
148 // If the snippet is empty, it's an attribute that was inserted during macro
149 // expansion and we want to ignore those, because they could come from external
150 // sources that the user has no control over.
151 // For some reason these attributes don't have any expansion info on them, so
152 // we have to check it this way until there is a better way.
is_present_in_source<T: LintContext>(cx: &T, span: Span) -> bool153 pub fn is_present_in_source<T: LintContext>(cx: &T, span: Span) -> bool {
154     if let Some(snippet) = snippet_opt(cx, span) {
155         if snippet.is_empty() {
156             return false;
157         }
158     }
159     true
160 }
161 
162 /// Returns the position just before rarrow
163 ///
164 /// ```rust,ignore
165 /// fn into(self) -> () {}
166 ///              ^
167 /// // in case of unformatted code
168 /// fn into2(self)-> () {}
169 ///               ^
170 /// fn into3(self)   -> () {}
171 ///               ^
172 /// ```
position_before_rarrow(s: &str) -> Option<usize>173 pub fn position_before_rarrow(s: &str) -> Option<usize> {
174     s.rfind("->").map(|rpos| {
175         let mut rpos = rpos;
176         let chars: Vec<char> = s.chars().collect();
177         while rpos > 1 {
178             if let Some(c) = chars.get(rpos - 1) {
179                 if c.is_whitespace() {
180                     rpos -= 1;
181                     continue;
182                 }
183             }
184             break;
185         }
186         rpos
187     })
188 }
189 
190 /// Reindent a multiline string with possibility of ignoring the first line.
191 #[expect(clippy::needless_pass_by_value)]
reindent_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str>192 pub fn reindent_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str> {
193     let s_space = reindent_multiline_inner(&s, ignore_first, indent, ' ');
194     let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
195     reindent_multiline_inner(&s_tab, ignore_first, indent, ' ').into()
196 }
197 
reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String198 fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
199     let x = s
200         .lines()
201         .skip(usize::from(ignore_first))
202         .filter_map(|l| {
203             if l.is_empty() {
204                 None
205             } else {
206                 // ignore empty lines
207                 Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
208             }
209         })
210         .min()
211         .unwrap_or(0);
212     let indent = indent.unwrap_or(0);
213     s.lines()
214         .enumerate()
215         .map(|(i, l)| {
216             if (ignore_first && i == 0) || l.is_empty() {
217                 l.to_owned()
218             } else if x > indent {
219                 l.split_at(x - indent).1.to_owned()
220             } else {
221                 " ".repeat(indent - x) + l
222             }
223         })
224         .collect::<Vec<String>>()
225         .join("\n")
226 }
227 
228 /// Converts a span to a code snippet if available, otherwise returns the default.
229 ///
230 /// This is useful if you want to provide suggestions for your lint or more generally, if you want
231 /// to convert a given `Span` to a `str`. To create suggestions consider using
232 /// [`snippet_with_applicability`] to ensure that the applicability stays correct.
233 ///
234 /// # Example
235 /// ```rust,ignore
236 /// // Given two spans one for `value` and one for the `init` expression.
237 /// let value = Vec::new();
238 /// //  ^^^^^   ^^^^^^^^^^
239 /// //  span1   span2
240 ///
241 /// // The snipped call would return the corresponding code snippet
242 /// snippet(cx, span1, "..") // -> "value"
243 /// snippet(cx, span2, "..") // -> "Vec::new()"
244 /// ```
snippet<'a, T: LintContext>(cx: &T, span: Span, default: &'a str) -> Cow<'a, str>245 pub fn snippet<'a, T: LintContext>(cx: &T, span: Span, default: &'a str) -> Cow<'a, str> {
246     snippet_opt(cx, span).map_or_else(|| Cow::Borrowed(default), From::from)
247 }
248 
249 /// Same as [`snippet`], but it adapts the applicability level by following rules:
250 ///
251 /// - Applicability level `Unspecified` will never be changed.
252 /// - If the span is inside a macro, change the applicability level to `MaybeIncorrect`.
253 /// - If the default value is used and the applicability level is `MachineApplicable`, change it to
254 /// `HasPlaceholders`
snippet_with_applicability<'a, T: LintContext>( cx: &T, span: Span, default: &'a str, applicability: &mut Applicability, ) -> Cow<'a, str>255 pub fn snippet_with_applicability<'a, T: LintContext>(
256     cx: &T,
257     span: Span,
258     default: &'a str,
259     applicability: &mut Applicability,
260 ) -> Cow<'a, str> {
261     snippet_with_applicability_sess(cx.sess(), span, default, applicability)
262 }
263 
snippet_with_applicability_sess<'a>( sess: &Session, span: Span, default: &'a str, applicability: &mut Applicability, ) -> Cow<'a, str>264 fn snippet_with_applicability_sess<'a>(
265     sess: &Session,
266     span: Span,
267     default: &'a str,
268     applicability: &mut Applicability,
269 ) -> Cow<'a, str> {
270     if *applicability != Applicability::Unspecified && span.from_expansion() {
271         *applicability = Applicability::MaybeIncorrect;
272     }
273     snippet_opt_sess(sess, span).map_or_else(
274         || {
275             if *applicability == Applicability::MachineApplicable {
276                 *applicability = Applicability::HasPlaceholders;
277             }
278             Cow::Borrowed(default)
279         },
280         From::from,
281     )
282 }
283 
284 /// Converts a span to a code snippet. Returns `None` if not available.
snippet_opt(cx: &impl LintContext, span: Span) -> Option<String>285 pub fn snippet_opt(cx: &impl LintContext, span: Span) -> Option<String> {
286     snippet_opt_sess(cx.sess(), span)
287 }
288 
snippet_opt_sess(sess: &Session, span: Span) -> Option<String>289 fn snippet_opt_sess(sess: &Session, span: Span) -> Option<String> {
290     sess.source_map().span_to_snippet(span).ok()
291 }
292 
293 /// Converts a span (from a block) to a code snippet if available, otherwise use default.
294 ///
295 /// This trims the code of indentation, except for the first line. Use it for blocks or block-like
296 /// things which need to be printed as such.
297 ///
298 /// The `indent_relative_to` arg can be used, to provide a span, where the indentation of the
299 /// resulting snippet of the given span.
300 ///
301 /// # Example
302 ///
303 /// ```rust,ignore
304 /// snippet_block(cx, block.span, "..", None)
305 /// // where, `block` is the block of the if expr
306 ///     if x {
307 ///         y;
308 ///     }
309 /// // will return the snippet
310 /// {
311 ///     y;
312 /// }
313 /// ```
314 ///
315 /// ```rust,ignore
316 /// snippet_block(cx, block.span, "..", Some(if_expr.span))
317 /// // where, `block` is the block of the if expr
318 ///     if x {
319 ///         y;
320 ///     }
321 /// // will return the snippet
322 /// {
323 ///         y;
324 ///     } // aligned with `if`
325 /// ```
326 /// Note that the first line of the snippet always has 0 indentation.
snippet_block<'a, T: LintContext>( cx: &T, span: Span, default: &'a str, indent_relative_to: Option<Span>, ) -> Cow<'a, str>327 pub fn snippet_block<'a, T: LintContext>(
328     cx: &T,
329     span: Span,
330     default: &'a str,
331     indent_relative_to: Option<Span>,
332 ) -> Cow<'a, str> {
333     let snip = snippet(cx, span, default);
334     let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
335     reindent_multiline(snip, true, indent)
336 }
337 
338 /// Same as `snippet_block`, but adapts the applicability level by the rules of
339 /// `snippet_with_applicability`.
snippet_block_with_applicability<'a>( cx: &impl LintContext, span: Span, default: &'a str, indent_relative_to: Option<Span>, applicability: &mut Applicability, ) -> Cow<'a, str>340 pub fn snippet_block_with_applicability<'a>(
341     cx: &impl LintContext,
342     span: Span,
343     default: &'a str,
344     indent_relative_to: Option<Span>,
345     applicability: &mut Applicability,
346 ) -> Cow<'a, str> {
347     let snip = snippet_with_applicability(cx, span, default, applicability);
348     let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
349     reindent_multiline(snip, true, indent)
350 }
351 
snippet_block_with_context<'a>( cx: &impl LintContext, span: Span, outer: SyntaxContext, default: &'a str, indent_relative_to: Option<Span>, app: &mut Applicability, ) -> (Cow<'a, str>, bool)352 pub fn snippet_block_with_context<'a>(
353     cx: &impl LintContext,
354     span: Span,
355     outer: SyntaxContext,
356     default: &'a str,
357     indent_relative_to: Option<Span>,
358     app: &mut Applicability,
359 ) -> (Cow<'a, str>, bool) {
360     let (snip, from_macro) = snippet_with_context(cx, span, outer, default, app);
361     let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
362     (reindent_multiline(snip, true, indent), from_macro)
363 }
364 
365 /// Same as `snippet_with_applicability`, but first walks the span up to the given context. This
366 /// will result in the macro call, rather then the expansion, if the span is from a child context.
367 /// If the span is not from a child context, it will be used directly instead.
368 ///
369 /// e.g. Given the expression `&vec![]`, getting a snippet from the span for `vec![]` as a HIR node
370 /// would result in `box []`. If given the context of the address of expression, this function will
371 /// correctly get a snippet of `vec![]`.
372 ///
373 /// This will also return whether or not the snippet is a macro call.
snippet_with_context<'a>( cx: &impl LintContext, span: Span, outer: SyntaxContext, default: &'a str, applicability: &mut Applicability, ) -> (Cow<'a, str>, bool)374 pub fn snippet_with_context<'a>(
375     cx: &impl LintContext,
376     span: Span,
377     outer: SyntaxContext,
378     default: &'a str,
379     applicability: &mut Applicability,
380 ) -> (Cow<'a, str>, bool) {
381     snippet_with_context_sess(cx.sess(), span, outer, default, applicability)
382 }
383 
snippet_with_context_sess<'a>( sess: &Session, span: Span, outer: SyntaxContext, default: &'a str, applicability: &mut Applicability, ) -> (Cow<'a, str>, bool)384 fn snippet_with_context_sess<'a>(
385     sess: &Session,
386     span: Span,
387     outer: SyntaxContext,
388     default: &'a str,
389     applicability: &mut Applicability,
390 ) -> (Cow<'a, str>, bool) {
391     let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
392         || {
393             // The span is from a macro argument, and the outer context is the macro using the argument
394             if *applicability != Applicability::Unspecified {
395                 *applicability = Applicability::MaybeIncorrect;
396             }
397             // TODO: get the argument span.
398             (span, false)
399         },
400         |outer_span| (outer_span, span.ctxt() != outer),
401     );
402 
403     (
404         snippet_with_applicability_sess(sess, span, default, applicability),
405         is_macro_call,
406     )
407 }
408 
409 /// Walks the span up to the target context, thereby returning the macro call site if the span is
410 /// inside a macro expansion, or the original span if it is not. Note this will return `None` in the
411 /// case of the span being in a macro expansion, but the target context is from expanding a macro
412 /// argument.
413 ///
414 /// Given the following
415 ///
416 /// ```rust,ignore
417 /// macro_rules! m { ($e:expr) => { f($e) }; }
418 /// g(m!(0))
419 /// ```
420 ///
421 /// If called with a span of the call to `f` and a context of the call to `g` this will return a
422 /// span containing `m!(0)`. However, if called with a span of the literal `0` this will give a span
423 /// containing `0` as the context is the same as the outer context.
424 ///
425 /// This will traverse through multiple macro calls. Given the following:
426 ///
427 /// ```rust,ignore
428 /// macro_rules! m { ($e:expr) => { n!($e, 0) }; }
429 /// macro_rules! n { ($e:expr, $f:expr) => { f($e, $f) }; }
430 /// g(m!(0))
431 /// ```
432 ///
433 /// If called with a span of the call to `f` and a context of the call to `g` this will return a
434 /// span containing `m!(0)`.
walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span>435 pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
436     let outer_span = hygiene::walk_chain(span, outer);
437     (outer_span.ctxt() == outer).then_some(outer_span)
438 }
439 
440 /// Removes block comments from the given `Vec` of lines.
441 ///
442 /// # Examples
443 ///
444 /// ```rust,ignore
445 /// without_block_comments(vec!["/*", "foo", "*/"]);
446 /// // => vec![]
447 ///
448 /// without_block_comments(vec!["bar", "/*", "foo", "*/"]);
449 /// // => vec!["bar"]
450 /// ```
without_block_comments(lines: Vec<&str>) -> Vec<&str>451 pub fn without_block_comments(lines: Vec<&str>) -> Vec<&str> {
452     let mut without = vec![];
453 
454     let mut nest_level = 0;
455 
456     for line in lines {
457         if line.contains("/*") {
458             nest_level += 1;
459             continue;
460         } else if line.contains("*/") {
461             nest_level -= 1;
462             continue;
463         }
464 
465         if nest_level == 0 {
466             without.push(line);
467         }
468     }
469 
470     without
471 }
472 
473 /// Trims the whitespace from the start and the end of the span.
trim_span(sm: &SourceMap, span: Span) -> Span474 pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
475     let data = span.data();
476     let sf: &_ = &sm.lookup_source_file(data.lo);
477     let Some(src) = sf.src.as_deref() else {
478         return span;
479     };
480     let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
481         return span;
482     };
483     let trim_start = snip.len() - snip.trim_start().len();
484     let trim_end = snip.len() - snip.trim_end().len();
485     SpanData {
486         lo: data.lo + BytePos::from_usize(trim_start),
487         hi: data.hi - BytePos::from_usize(trim_end),
488         ctxt: data.ctxt,
489         parent: data.parent,
490     }
491     .span()
492 }
493 
494 /// Expand a span to include a preceding comma
495 /// ```rust,ignore
496 /// writeln!(o, "")   ->   writeln!(o, "")
497 ///             ^^                   ^^^^
498 /// ```
expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span499 pub fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
500     let extended = cx.sess().source_map().span_extend_to_prev_char(span, ',', true);
501     extended.with_lo(extended.lo() - BytePos(1))
502 }
503 
504 #[cfg(test)]
505 mod test {
506     use super::{reindent_multiline, without_block_comments};
507 
508     #[test]
test_reindent_multiline_single_line()509     fn test_reindent_multiline_single_line() {
510         assert_eq!("", reindent_multiline("".into(), false, None));
511         assert_eq!("...", reindent_multiline("...".into(), false, None));
512         assert_eq!("...", reindent_multiline("    ...".into(), false, None));
513         assert_eq!("...", reindent_multiline("\t...".into(), false, None));
514         assert_eq!("...", reindent_multiline("\t\t...".into(), false, None));
515     }
516 
517     #[test]
518     #[rustfmt::skip]
test_reindent_multiline_block()519     fn test_reindent_multiline_block() {
520         assert_eq!("\
521     if x {
522         y
523     } else {
524         z
525     }", reindent_multiline("    if x {
526             y
527         } else {
528             z
529         }".into(), false, None));
530         assert_eq!("\
531     if x {
532     \ty
533     } else {
534     \tz
535     }", reindent_multiline("    if x {
536         \ty
537         } else {
538         \tz
539         }".into(), false, None));
540     }
541 
542     #[test]
543     #[rustfmt::skip]
test_reindent_multiline_empty_line()544     fn test_reindent_multiline_empty_line() {
545         assert_eq!("\
546     if x {
547         y
548 
549     } else {
550         z
551     }", reindent_multiline("    if x {
552             y
553 
554         } else {
555             z
556         }".into(), false, None));
557     }
558 
559     #[test]
560     #[rustfmt::skip]
test_reindent_multiline_lines_deeper()561     fn test_reindent_multiline_lines_deeper() {
562         assert_eq!("\
563         if x {
564             y
565         } else {
566             z
567         }", reindent_multiline("\
568     if x {
569         y
570     } else {
571         z
572     }".into(), true, Some(8)));
573     }
574 
575     #[test]
test_without_block_comments_lines_without_block_comments()576     fn test_without_block_comments_lines_without_block_comments() {
577         let result = without_block_comments(vec!["/*", "", "*/"]);
578         println!("result: {result:?}");
579         assert!(result.is_empty());
580 
581         let result = without_block_comments(vec!["", "/*", "", "*/", "#[crate_type = \"lib\"]", "/*", "", "*/", ""]);
582         assert_eq!(result, vec!["", "#[crate_type = \"lib\"]", ""]);
583 
584         let result = without_block_comments(vec!["/* rust", "", "*/"]);
585         assert!(result.is_empty());
586 
587         let result = without_block_comments(vec!["/* one-line comment */"]);
588         assert!(result.is_empty());
589 
590         let result = without_block_comments(vec!["/* nested", "/* multi-line", "comment", "*/", "test", "*/"]);
591         assert!(result.is_empty());
592 
593         let result = without_block_comments(vec!["/* nested /* inline /* comment */ test */ */"]);
594         assert!(result.is_empty());
595 
596         let result = without_block_comments(vec!["foo", "bar", "baz"]);
597         assert_eq!(result, vec!["foo", "bar", "baz"]);
598     }
599 }
600