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 #[allow(rustdoc::invalid_rust_codeblocks, reason = "The codeblock is intentionally broken")]
146 fn with_leading_whitespace(self, cx: &impl HasSession) -> Range<BytePos> {
160 with_leading_whitespace(cx.sess().source_map(), self.into_range())
161 }
162
163 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
170pub struct SourceText(SourceFileRange);
172impl SourceText {
173 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 pub fn as_str(&self) -> &str {
184 self.0.as_str().unwrap()
185 }
186
187 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 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
315pub 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 format!("{{ {code} }}")
336 }
337}
338
339pub 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
361fn 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
378pub 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
390pub 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
399pub 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
413pub 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
441pub 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 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
478pub 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
499pub 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
534pub fn snippet_opt(sess: &impl HasSession, span: Span) -> Option<String> {
536 sess.sess().source_map().span_to_snippet(span).ok()
537}
538
539pub 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
579pub 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
606pub 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 if *applicability != Applicability::Unspecified {
637 *applicability = Applicability::MaybeIncorrect;
638 }
639 (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
651pub 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
683pub 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
704pub 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
714pub 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 &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
737 } else {
738 &snip[1..(snip.len() - 1)]
740 };
741
742 let hint = format!(
743 "'{}'",
744 match ch {
745 "'" => "\\'",
746 r"\" => "\\\\",
747 "\\\"" => "\"", _ => 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}