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