1#![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
46pub 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
67pub 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 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 fn get_source_range(self, cx: &impl HasSession) -> Option<SourceFileRange> {
107 get_source_range(cx.sess().source_map(), self.into_range())
108 }
109
110 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 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 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 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 fn with_leading_whitespace(self, cx: &impl HasSession) -> Range<BytePos> {
147 with_leading_whitespace(cx.sess().source_map(), self.into_range())
148 }
149
150 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
157pub struct SourceText(SourceFileRange);
159impl SourceText {
160 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 pub fn as_str(&self) -> &str {
171 self.0.as_str().unwrap()
172 }
173
174 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 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
297pub 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 format!("{{ {code} }}")
318 }
319}
320
321pub 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
343fn 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
360pub 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
372pub 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
381pub 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
395pub 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#[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 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
461pub 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
482pub 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
517pub fn snippet_opt(sess: &impl HasSession, span: Span) -> Option<String> {
519 sess.sess().source_map().span_to_snippet(span).ok()
520}
521
522pub 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
567pub 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
594pub 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 if *applicability != Applicability::Unspecified {
625 *applicability = Applicability::MaybeIncorrect;
626 }
627 (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
639pub 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
671pub 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
692pub 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
702pub 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 &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
725 } else {
726 &snip[1..(snip.len() - 1)]
728 };
729
730 let hint = format!(
731 "'{}'",
732 match ch {
733 "'" => "\\'",
734 r"\" => "\\\\",
735 "\\\"" => "\"", _ => 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}