clippy_utils/
source.rs

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