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