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