Skip to main content

clippy_utils/
source.rs

1//! Utils for extracting, inspecting or transforming source code
2
3#![expect(clippy::module_name_repetitions)]
4
5use std::sync::Arc;
6
7use rustc_ast::{LitKind, StrStyle};
8use rustc_errors::Applicability;
9use rustc_hir::{BlockCheckMode, Expr, ExprKind, UnsafeSource};
10use rustc_lexer::{FrontmatterAllowed, LiteralKind, TokenKind, tokenize};
11use rustc_lint::{EarlyContext, LateContext};
12use rustc_middle::ty::TyCtxt;
13use rustc_session::Session;
14use rustc_span::source_map::{SourceMap, original_sp};
15use rustc_span::{
16    BytePos, DUMMY_SP, DesugaringKind, Pos as _, RelativeBytePos, SourceFile, SourceFileAndLine, Span, SpanData,
17    SyntaxContext, hygiene,
18};
19use std::borrow::Cow;
20use std::fmt;
21use std::ops::{Deref, Index, Range};
22
23/// Types from which a `SourceMap` can be obtained.
24pub trait HasSourceMap<'sm>: Copy {
25    #[must_use]
26    fn source_map(self) -> &'sm SourceMap;
27}
28impl<'sm> HasSourceMap<'sm> for &'sm SourceMap {
29    #[inline]
30    fn source_map(self) -> &'sm SourceMap {
31        self
32    }
33}
34impl<'sm> HasSourceMap<'sm> for &'sm Session {
35    #[inline]
36    fn source_map(self) -> &'sm SourceMap {
37        self.early_sess.source_map()
38    }
39}
40impl<'sm> HasSourceMap<'sm> for TyCtxt<'sm> {
41    #[inline]
42    fn source_map(self) -> &'sm SourceMap {
43        self.sess.source_map()
44    }
45}
46impl<'sm> HasSourceMap<'sm> for &'sm EarlyContext<'_> {
47    #[inline]
48    fn source_map(self) -> &'sm SourceMap {
49        ::rustc_lint::LintContext::sess(self).source_map()
50    }
51}
52impl<'sm> HasSourceMap<'sm> for &LateContext<'sm> {
53    #[inline]
54    fn source_map(self) -> &'sm SourceMap {
55        self.tcx.sess.source_map()
56    }
57}
58
59/// Conversion of a value into the range portion of a `Span`.
60pub trait SpanRange: Sized {
61    /// Converts `self` into the range portion of a [`Span`].
62    fn into_range(self) -> Range<BytePos>;
63}
64impl SpanRange for Span {
65    fn into_range(self) -> Range<BytePos> {
66        let (lo, hi) = self.lo_hi();
67        lo..hi
68    }
69}
70impl SpanRange for SpanData {
71    fn into_range(self) -> Range<BytePos> {
72        self.lo..self.hi
73    }
74}
75impl SpanRange for Range<BytePos> {
76    fn into_range(self) -> Range<BytePos> {
77        self
78    }
79}
80
81/// Conversion of a value into a `Span`
82pub trait IntoSpan: Sized {
83    /// Converts `self` into a [`Span`].
84    fn into_span(self) -> Span;
85    /// Converts `self` into a [`Span`], with [context](SyntaxContext).
86    fn with_ctxt(self, ctxt: SyntaxContext) -> Span;
87}
88impl IntoSpan for Span {
89    fn into_span(self) -> Span {
90        self
91    }
92    fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
93        self.with_ctxt(ctxt)
94    }
95}
96impl IntoSpan for SpanData {
97    fn into_span(self) -> Span {
98        self.span()
99    }
100    fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
101        Span::new(self.lo, self.hi, ctxt, self.parent)
102    }
103}
104impl IntoSpan for Range<BytePos> {
105    fn into_span(self) -> Span {
106        Span::with_root_ctxt(self.start, self.end)
107    }
108    fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
109        Span::new(self.start, self.end, ctxt, None)
110    }
111}
112
113/// Extension methods for span-like types (anything implementing [`SpanRange`]) that need access
114/// to the underlying source text via a [`HasSourceMap`].
115pub trait SpanExt: SpanRange {
116    /// Attempts to get a handle to the source text. Returns `None` if either the span is malformed,
117    /// or the source text is not accessible.
118    fn get_text<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<SourceText> {
119        get_source_range(sm.source_map(), self.into_range()).and_then(SourceText::new)
120    }
121
122    /// Gets the source file, and range in the file, of the given span. Returns `None` if the span
123    /// extends through multiple files, or is malformed.
124    fn get_source_range<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<SourceFileRange> {
125        get_source_range(sm.source_map(), self.into_range())
126    }
127
128    /// Calls the given function with the source text referenced and returns the value. Returns
129    /// `None` if the source text cannot be retrieved.
130    fn with_source_text<'sm, T>(self, sm: impl HasSourceMap<'sm>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
131        with_source_text(sm.source_map(), self.into_range(), f)
132    }
133
134    /// Checks if the referenced source text satisfies the given predicate. Returns `false` if the
135    /// source text cannot be retrieved.
136    fn check_text<'sm>(self, sm: impl HasSourceMap<'sm>, pred: impl for<'a> FnOnce(&'a str) -> bool) -> bool {
137        self.with_source_text(sm, pred).unwrap_or(false)
138    }
139
140    /// Calls the given function with the both the text of the source file and the referenced range,
141    /// and returns the value. Returns `None` if the source text cannot be retrieved.
142    fn with_source_text_and_range<'sm, T>(
143        self,
144        sm: impl HasSourceMap<'sm>,
145        f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
146    ) -> Option<T> {
147        with_source_text_and_range(sm.source_map(), self.into_range(), f)
148    }
149
150    /// Calls the given function with the both the text of the source file and the referenced range,
151    /// and creates a new span with the returned range. Returns `None` if the source text cannot be
152    /// retrieved, or no result is returned.
153    ///
154    /// The new range must reside within the same source file.
155    fn map_range<'sm>(
156        self,
157        sm: impl HasSourceMap<'sm>,
158        f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
159    ) -> Option<Range<BytePos>> {
160        map_range(sm.source_map(), self.into_range(), f)
161    }
162
163    /// Extends the range to include all preceding whitespace characters.
164    ///
165    /// The range will not be expanded if it would cross a line boundary, the line the range would
166    /// be extended to ends with a line comment and the text after the range contains a
167    /// non-whitespace character on the same line. e.g.
168    ///
169    /// ```ignore
170    /// ( // Some comment
171    /// foo)
172    /// ```
173    ///
174    /// When the range points to `foo`, suggesting to remove the range after it's been extended will
175    /// cause the `)` to be placed inside the line comment as `( // Some comment)`.
176    fn with_leading_whitespace<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range<BytePos> {
177        with_leading_whitespace(sm.source_map(), self.into_range())
178    }
179
180    /// Trims the leading whitespace from the range.
181    fn trim_start<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range<BytePos> {
182        trim_start(sm.source_map(), self.into_range())
183    }
184}
185impl<T: SpanRange> SpanExt for T {}
186
187/// Handle to a range of text in a source file.
188pub struct SourceText(SourceFileRange);
189impl SourceText {
190    /// Takes ownership of the source file handle if the source text is accessible.
191    pub fn new(text: SourceFileRange) -> Option<Self> {
192        if text.as_str().is_some() {
193            Some(Self(text))
194        } else {
195            None
196        }
197    }
198
199    /// Gets the source text.
200    pub fn as_str(&self) -> &str {
201        self.0.as_str().unwrap()
202    }
203
204    /// Converts this into an owned string.
205    pub fn to_owned(&self) -> String {
206        self.as_str().to_owned()
207    }
208}
209impl Deref for SourceText {
210    type Target = str;
211    fn deref(&self) -> &Self::Target {
212        self.as_str()
213    }
214}
215impl AsRef<str> for SourceText {
216    fn as_ref(&self) -> &str {
217        self.as_str()
218    }
219}
220impl<T> Index<T> for SourceText
221where
222    str: Index<T>,
223{
224    type Output = <str as Index<T>>::Output;
225    fn index(&self, idx: T) -> &Self::Output {
226        &self.as_str()[idx]
227    }
228}
229impl fmt::Display for SourceText {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        self.as_str().fmt(f)
232    }
233}
234impl fmt::Debug for SourceText {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        self.as_str().fmt(f)
237    }
238}
239
240fn get_source_range(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
241    let start = sm.lookup_byte_offset(sp.start);
242    let end = sm.lookup_byte_offset(sp.end);
243    if !Arc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
244        return None;
245    }
246    sm.ensure_source_file_source_present(&start.sf);
247    let range = start.pos.to_usize()..end.pos.to_usize();
248    Some(SourceFileRange { sf: start.sf, range })
249}
250
251fn with_source_text<T>(sm: &SourceMap, sp: Range<BytePos>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
252    if let Some(src) = get_source_range(sm, sp)
253        && let Some(src) = src.as_str()
254    {
255        Some(f(src))
256    } else {
257        None
258    }
259}
260
261fn with_source_text_and_range<T>(
262    sm: &SourceMap,
263    sp: Range<BytePos>,
264    f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
265) -> Option<T> {
266    if let Some(src) = get_source_range(sm, sp)
267        && let Some(text) = &src.sf.src
268    {
269        Some(f(text, src.range))
270    } else {
271        None
272    }
273}
274
275#[expect(clippy::cast_possible_truncation)]
276fn map_range(
277    sm: &SourceMap,
278    sp: Range<BytePos>,
279    f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
280) -> Option<Range<BytePos>> {
281    if let Some(src) = get_source_range(sm, sp.clone())
282        && let Some(text) = &src.sf.src
283        && let Some(range) = f(&src.sf, text, src.range.clone())
284    {
285        debug_assert!(
286            range.start <= text.len() && range.end <= text.len(),
287            "Range `{range:?}` is outside the source file (file `{}`, length `{}`)",
288            src.sf.name.prefer_local_unconditionally(),
289            text.len(),
290        );
291        debug_assert!(range.start <= range.end, "Range `{range:?}` has overlapping bounds");
292        let dstart = (range.start as u32).wrapping_sub(src.range.start as u32);
293        let dend = (range.end as u32).wrapping_sub(src.range.start as u32);
294        Some(BytePos(sp.start.0.wrapping_add(dstart))..BytePos(sp.start.0.wrapping_add(dend)))
295    } else {
296        None
297    }
298}
299
300fn ends_with_line_comment_or_broken(text: &str) -> bool {
301    let Some(last) = tokenize(text, FrontmatterAllowed::No).last() else {
302        return false;
303    };
304    match last.kind {
305        // Will give the wrong result on text like `" // "` where the first quote ends a string
306        // started earlier. The only workaround is to lex the whole file which we don't really want
307        // to do.
308        TokenKind::LineComment { .. } | TokenKind::BlockComment { terminated: false, .. } => true,
309        TokenKind::Literal { kind, .. } => matches!(
310            kind,
311            LiteralKind::Byte { terminated: false }
312                | LiteralKind::ByteStr { terminated: false }
313                | LiteralKind::CStr { terminated: false }
314                | LiteralKind::Char { terminated: false }
315                | LiteralKind::RawByteStr { n_hashes: None }
316                | LiteralKind::RawCStr { n_hashes: None }
317                | LiteralKind::RawStr { n_hashes: None }
318        ),
319        _ => false,
320    }
321}
322
323fn with_leading_whitespace_inner(lines: &[RelativeBytePos], src: &str, range: Range<usize>) -> Option<usize> {
324    debug_assert!(lines.is_empty() || lines[0].to_u32() == 0);
325
326    let start = src.get(..range.start)?.trim_end();
327    let next_line = lines.partition_point(|&pos| pos.to_usize() <= start.len());
328    if let Some(line_end) = lines.get(next_line)
329        && line_end.to_usize() <= range.start
330        && let prev_start = lines.get(next_line - 1).map_or(0, |&x| x.to_usize())
331        && ends_with_line_comment_or_broken(&start[prev_start..])
332        && let next_line = lines.partition_point(|&pos| pos.to_usize() < range.end)
333        && let next_start = lines.get(next_line).map_or(src.len(), |&x| x.to_usize())
334        && tokenize(src.get(range.end..next_start)?, FrontmatterAllowed::No)
335            .any(|t| !matches!(t.kind, TokenKind::Whitespace))
336    {
337        Some(range.start)
338    } else {
339        Some(start.len())
340    }
341}
342
343fn with_leading_whitespace(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
344    map_range(sm, sp.clone(), |sf, src, range| {
345        Some(with_leading_whitespace_inner(sf.lines(), src, range.clone())?..range.end)
346    })
347    .unwrap_or(sp)
348}
349
350fn trim_start(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
351    map_range(sm, sp.clone(), |_, src, range| {
352        let src = src.get(range.clone())?;
353        Some(range.start + (src.len() - src.trim_start().len())..range.end)
354    })
355    .unwrap_or(sp)
356}
357
358/// A range within a specific [source file](SourceFile).
359pub struct SourceFileRange {
360    /// The [source file](SourceFile) referred to by this range
361    pub sf: Arc<SourceFile>,
362    /// The range within the associated [source file](SourceFile)
363    pub range: Range<usize>,
364}
365impl SourceFileRange {
366    /// Attempts to get the text from the source file. This can fail if the source text isn't
367    /// loaded.
368    pub fn as_str(&self) -> Option<&str> {
369        (self.sf.src.as_ref().map(|src| src.as_str()))
370            .or_else(|| self.sf.external_src.get()?.get_source())
371            .and_then(|x| x.get(self.range.clone()))
372    }
373}
374
375/// Like [`snippet_block`], but add braces if the expr is not an `ExprKind::Block` with no label.
376pub fn expr_block<'sm>(
377    sm: impl HasSourceMap<'sm>,
378    expr: &Expr<'_>,
379    outer: SyntaxContext,
380    default: &str,
381    indent_relative_to: Option<Span>,
382    app: &mut Applicability,
383) -> String {
384    let (code, from_macro) = snippet_block_with_context(sm, expr.span, outer, default, indent_relative_to, app);
385    if !from_macro
386        && let ExprKind::Block(block, None) = expr.kind
387        && block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
388    {
389        code
390    } else {
391        // FIXME: add extra indent for the unsafe blocks:
392        //     original code:   unsafe { ... }
393        //     result code:     { unsafe { ... } }
394        //     desired code:    {\n  unsafe { ... }\n}
395        format!("{{ {code} }}")
396    }
397}
398
399/// Returns a new Span that extends the original Span to the first non-whitespace char of the first
400/// line.
401///
402/// ```rust,ignore
403///     let x = ();
404/// //          ^^
405/// // will be converted to
406///     let x = ();
407/// //  ^^^^^^^^^^
408/// ```
409pub fn first_line_of_span<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
410    first_char_in_first_line(sm, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
411}
412
413fn first_char_in_first_line<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<BytePos> {
414    let line_span = line_span(sm, span);
415    snippet_opt(sm, line_span).and_then(|snip| {
416        snip.find(|c: char| !c.is_whitespace())
417            .map(|pos| line_span.lo() + BytePos::from_usize(pos))
418    })
419}
420
421/// Extends the span to the beginning of the spans line, incl. whitespaces.
422///
423/// ```no_run
424///        let x = ();
425/// //             ^^
426/// // will be converted to
427///        let x = ();
428/// // ^^^^^^^^^^^^^^
429/// ```
430fn line_span<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
431    let span = original_sp(span, DUMMY_SP);
432    let SourceFileAndLine { sf, line } = sm.source_map().lookup_line(span.lo()).unwrap();
433    let line_start = sf.lines()[line];
434    let line_start = sf.absolute_position(line_start);
435    span.with_lo(line_start)
436}
437
438/// Returns the indentation of the line of a span
439///
440/// ```rust,ignore
441/// let x = ();
442/// //      ^^ -- will return 0
443///     let x = ();
444/// //          ^^ -- will return 4
445/// ```
446pub fn indent_of<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<usize> {
447    snippet_opt(sm, line_span(sm, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
448}
449
450/// Gets a snippet of the indentation of the line of a span
451pub fn snippet_indent<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<String> {
452    snippet_opt(sm, line_span(sm, span)).map(|mut s| {
453        let len = s.len() - s.trim_start().len();
454        s.truncate(len);
455        s
456    })
457}
458
459/// Checks whether the code snippet referred to by the given [`Span`] is present in the source code.
460// If the snippet is empty, it's an attribute that was inserted during macro
461// expansion and we want to ignore those, because they could come from external
462// sources that the user has no control over.
463// For some reason these attributes don't have any expansion info on them, so
464// we have to check it this way until there is a better way.
465pub fn is_present_in_source<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> bool {
466    if let Some(snippet) = snippet_opt(sm, span)
467        && snippet.is_empty()
468    {
469        return false;
470    }
471    true
472}
473
474/// Returns the position just before rarrow
475///
476/// ```rust,ignore
477/// fn into(self) -> () {}
478///              ^
479/// // in case of unformatted code
480/// fn into2(self)-> () {}
481///               ^
482/// fn into3(self)   -> () {}
483///               ^
484/// ```
485pub fn position_before_rarrow(s: &str) -> Option<usize> {
486    s.rfind("->").map(|rpos| {
487        let mut rpos = rpos;
488        let chars: Vec<char> = s.chars().collect();
489        while rpos > 1 {
490            if let Some(c) = chars.get(rpos - 1)
491                && c.is_whitespace()
492            {
493                rpos -= 1;
494                continue;
495            }
496            break;
497        }
498        rpos
499    })
500}
501
502/// Reindent a multiline string with possibility of ignoring the first line.
503pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
504    let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');
505    let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
506    reindent_multiline_inner(&s_tab, ignore_first, indent, ' ')
507}
508
509fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
510    let x = s
511        .lines()
512        .skip(usize::from(ignore_first))
513        .filter_map(|l| {
514            if l.is_empty() {
515                None
516            } else {
517                // ignore empty lines
518                Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
519            }
520        })
521        .min()
522        .unwrap_or(0);
523    let indent = indent.unwrap_or(0);
524    s.lines()
525        .enumerate()
526        .map(|(i, l)| {
527            if (ignore_first && i == 0) || l.is_empty() {
528                l.to_owned()
529            } else if x > indent {
530                l.split_at(x - indent).1.to_owned()
531            } else {
532                " ".repeat(indent - x) + l
533            }
534        })
535        .collect::<Vec<String>>()
536        .join("\n")
537}
538
539/// Converts a span to a code snippet if available, otherwise returns the default.
540///
541/// This is useful if you want to provide suggestions for your lint or more generally, if you want
542/// to convert a given `Span` to a `str`.
543///
544/// To create suggestions consider using [`snippet_with_applicability`] to ensure that the
545/// applicability stays correct.
546///
547/// # Example
548/// ```rust,ignore
549/// // Given two spans one for `value` and one for the `init` expression.
550/// let value = Vec::new();
551/// //  ^^^^^   ^^^^^^^^^^
552/// //  span1   span2
553///
554/// // The snippet call would return the corresponding code snippet
555/// snippet(cx, span1, "..") // -> "value"
556/// snippet(cx, span2, "..") // -> "Vec::new()"
557/// ```
558pub fn snippet<'a, 'sm>(sm: impl HasSourceMap<'sm>, span: Span, default: &'a str) -> Cow<'a, str> {
559    snippet_opt(sm, span).map_or_else(|| Cow::Borrowed(default), From::from)
560}
561
562/// Same as [`snippet`], but it adapts the applicability level by following rules:
563///
564/// - Applicability level `Unspecified` will never be changed.
565/// - If the span is inside a macro, change the applicability level to `MaybeIncorrect`.
566/// - If the default value is used and the applicability level is `MachineApplicable`, change it to `HasPlaceholders`
567///
568/// If the span might realistically contain a macro call (e.g. `vec![]`), consider using
569/// [`snippet_with_context`] instead.
570pub fn snippet_with_applicability<'a, 'sm>(
571    sm: impl HasSourceMap<'sm>,
572    span: Span,
573    default: &'a str,
574    applicability: &mut Applicability,
575) -> Cow<'a, str> {
576    snippet_with_applicability_sm(sm.source_map(), span, default, applicability)
577}
578
579fn snippet_with_applicability_sm<'a>(
580    sm: &SourceMap,
581    span: Span,
582    default: &'a str,
583    applicability: &mut Applicability,
584) -> Cow<'a, str> {
585    if *applicability != Applicability::Unspecified && span.from_expansion() {
586        *applicability = Applicability::MaybeIncorrect;
587    }
588    if let Some(t) = snippet_opt(sm, span) {
589        Cow::Owned(t)
590    } else {
591        if *applicability == Applicability::MachineApplicable {
592            *applicability = Applicability::HasPlaceholders;
593        }
594        Cow::Borrowed(default)
595    }
596}
597
598/// Converts a span to a code snippet. Returns `None` if not available.
599pub fn snippet_opt<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<String> {
600    sm.source_map().span_to_snippet(span).ok()
601}
602
603/// Converts a span (from a block) to a code snippet if available, otherwise use default.
604///
605/// This trims the code of indentation, except for the first line. Use it for blocks or block-like
606/// things which need to be printed as such.
607///
608/// The `indent_relative_to` arg can be used, to provide a span, where the indentation of the
609/// resulting snippet of the given span.
610///
611/// # Example
612///
613/// ```rust,ignore
614/// snippet_block(cx, block.span, "..", None)
615/// // where, `block` is the block of the if expr
616///     if x {
617///         y;
618///     }
619/// // will return the snippet
620/// {
621///     y;
622/// }
623/// ```
624///
625/// ```rust,ignore
626/// snippet_block(cx, block.span, "..", Some(if_expr.span))
627/// // where, `block` is the block of the if expr
628///     if x {
629///         y;
630///     }
631/// // will return the snippet
632/// {
633///         y;
634///     } // aligned with `if`
635/// ```
636/// Note that the first line of the snippet always has 0 indentation.
637pub fn snippet_block<'sm>(
638    sm: impl HasSourceMap<'sm>,
639    span: Span,
640    default: &str,
641    indent_relative_to: Option<Span>,
642) -> String {
643    let snip = snippet(sm, span, default);
644    let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
645    reindent_multiline(&snip, true, indent)
646}
647
648/// Same as [`snippet_block`], but adapts the applicability level by the rules of
649/// [`snippet_with_applicability`].
650pub fn snippet_block_with_applicability<'sm>(
651    sm: impl HasSourceMap<'sm>,
652    span: Span,
653    default: &str,
654    indent_relative_to: Option<Span>,
655    applicability: &mut Applicability,
656) -> String {
657    let snip = snippet_with_applicability(sm, span, default, applicability);
658    let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
659    reindent_multiline(&snip, true, indent)
660}
661
662/// Combination of [`snippet_block`] and [`snippet_with_context`].
663pub fn snippet_block_with_context<'sm>(
664    sm: impl HasSourceMap<'sm>,
665    span: Span,
666    outer: SyntaxContext,
667    default: &str,
668    indent_relative_to: Option<Span>,
669    app: &mut Applicability,
670) -> (String, bool) {
671    let (snip, from_macro) = snippet_with_context(sm, span, outer, default, app);
672    let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
673    (reindent_multiline(&snip, true, indent), from_macro)
674}
675
676/// Same as [`snippet_with_applicability`], but first walks the span up to the given context.
677///
678/// This will result in the macro call, rather than the expansion, if the span is from a child
679/// context. If the span is not from a child context, it will be used directly instead.
680///
681/// e.g. Given the expression `&vec![]`, getting a snippet from the span for `vec![]` as a HIR node
682/// would result in `box []`. If given the context of the address of expression, this function will
683/// correctly get a snippet of `vec![]`.
684///
685/// This will also return whether or not the snippet is a macro call.
686pub fn snippet_with_context<'a, 'sm>(
687    sm: impl HasSourceMap<'sm>,
688    span: Span,
689    outer: SyntaxContext,
690    default: &'a str,
691    applicability: &mut Applicability,
692) -> (Cow<'a, str>, bool) {
693    snippet_with_context_sm(sm.source_map(), span, outer, default, applicability)
694}
695
696fn snippet_with_context_sm<'a>(
697    sm: &SourceMap,
698    span: Span,
699    outer: SyntaxContext,
700    default: &'a str,
701    applicability: &mut Applicability,
702) -> (Cow<'a, str>, bool) {
703    // If it is just range desugaring, use the desugaring span since it may include parenthesis.
704    if span.desugaring_kind() == Some(DesugaringKind::RangeExpr) && span.parent_callsite().unwrap().ctxt() == outer {
705        return (snippet_with_applicability_sm(sm, span, default, applicability), false);
706    }
707
708    let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
709        || {
710            // The span is from a macro argument, and the outer context is the macro using the argument
711            if *applicability != Applicability::Unspecified {
712                *applicability = Applicability::MaybeIncorrect;
713            }
714            // TODO: get the argument span.
715            (span, false)
716        },
717        |outer_span| (outer_span, span.ctxt() != outer),
718    );
719
720    (
721        snippet_with_applicability_sm(sm, span, default, applicability),
722        is_macro_call,
723    )
724}
725
726/// Walks the span up to the target context, thereby returning the macro call site if the span is
727/// inside a macro expansion, or the original span if it is not.
728///
729/// Note this will return `None` in the case of the span being in a macro expansion, but the target
730/// context is from expanding a macro argument.
731///
732/// Given the following
733///
734/// ```rust,ignore
735/// macro_rules! m { ($e:expr) => { f($e) }; }
736/// g(m!(0))
737/// ```
738///
739/// If called with a span of the call to `f` and a context of the call to `g` this will return a
740/// span containing `m!(0)`. However, if called with a span of the literal `0` this will give a span
741/// containing `0` as the context is the same as the outer context.
742///
743/// This will traverse through multiple macro calls. Given the following:
744///
745/// ```rust,ignore
746/// macro_rules! m { ($e:expr) => { n!($e, 0) }; }
747/// macro_rules! n { ($e:expr, $f:expr) => { f($e, $f) }; }
748/// g(m!(0))
749/// ```
750///
751/// If called with a span of the call to `f` and a context of the call to `g` this will return a
752/// span containing `m!(0)`.
753pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
754    let outer_span = hygiene::walk_chain(span, outer);
755    (outer_span.ctxt() == outer).then_some(outer_span)
756}
757
758/// Trims the whitespace from the start and the end of the span.
759pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
760    let data = span.data();
761    let sf: &_ = &sm.lookup_source_file(data.lo);
762    let Some(src) = sf.src.as_deref() else {
763        return span;
764    };
765    let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
766        return span;
767    };
768    let trim_start = snip.len() - snip.trim_start().len();
769    let trim_end = snip.len() - snip.trim_end().len();
770    SpanData {
771        lo: data.lo + BytePos::from_usize(trim_start),
772        hi: data.hi - BytePos::from_usize(trim_end),
773        ctxt: data.ctxt,
774        parent: data.parent,
775    }
776    .span()
777}
778
779/// Expand a span to include a preceding comma
780/// ```rust,ignore
781/// writeln!(o, "")   ->   writeln!(o, "")
782///             ^^                   ^^^^
783/// ```
784pub fn expand_past_previous_comma<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
785    let extended = sm.source_map().span_extend_to_prev_char(span, ',', true);
786    extended.with_lo(extended.lo() - BytePos(1))
787}
788
789/// Converts `expr` to a `char` literal if it's a `str` literal containing a single
790/// character (or a single byte with `ascii_only`)
791pub fn str_literal_to_char_literal<'sm>(
792    sm: impl HasSourceMap<'sm>,
793    expr: &Expr<'_>,
794    applicability: &mut Applicability,
795    ascii_only: bool,
796) -> Option<String> {
797    if let ExprKind::Lit(lit) = &expr.kind
798        && let LitKind::Str(r, style) = lit.node
799        && let string = r.as_str()
800        && let len = if ascii_only {
801            string.len()
802        } else {
803            string.chars().count()
804        }
805        && len == 1
806    {
807        let snip = snippet_with_applicability(sm, expr.span, string, applicability);
808        let ch = if let StrStyle::Raw(nhash) = style {
809            let nhash = nhash as usize;
810            // for raw string: r##"a"##
811            &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
812        } else {
813            // for regular string: "a"
814            &snip[1..(snip.len() - 1)]
815        };
816
817        let hint = format!(
818            "'{}'",
819            match ch {
820                "'" => "\\'",
821                r"\" => "\\\\",
822                "\\\"" => "\"", // no need to escape `"` in `'"'`
823                _ => ch,
824            }
825        );
826
827        Some(hint)
828    } else {
829        None
830    }
831}
832
833#[cfg(test)]
834mod test {
835    use super::reindent_multiline;
836
837    #[test]
838    fn test_reindent_multiline_single_line() {
839        assert_eq!("", reindent_multiline("", false, None));
840        assert_eq!("...", reindent_multiline("...", false, None));
841        assert_eq!("...", reindent_multiline("    ...", false, None));
842        assert_eq!("...", reindent_multiline("\t...", false, None));
843        assert_eq!("...", reindent_multiline("\t\t...", false, None));
844    }
845
846    #[test]
847    #[rustfmt::skip]
848    fn test_reindent_multiline_block() {
849        assert_eq!("\
850    if x {
851        y
852    } else {
853        z
854    }", reindent_multiline("    if x {
855            y
856        } else {
857            z
858        }", false, None));
859        assert_eq!("\
860    if x {
861    \ty
862    } else {
863    \tz
864    }", reindent_multiline("    if x {
865        \ty
866        } else {
867        \tz
868        }", false, None));
869    }
870
871    #[test]
872    #[rustfmt::skip]
873    fn test_reindent_multiline_empty_line() {
874        assert_eq!("\
875    if x {
876        y
877
878    } else {
879        z
880    }", reindent_multiline("    if x {
881            y
882
883        } else {
884            z
885        }", false, None));
886    }
887
888    #[test]
889    #[rustfmt::skip]
890    fn test_reindent_multiline_lines_deeper() {
891        assert_eq!("\
892        if x {
893            y
894        } else {
895            z
896        }", reindent_multiline("\
897    if x {
898        y
899    } else {
900        z
901    }", true, Some(8)));
902    }
903}