1#![expect(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, DesugaringKind, Pos as _, 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 HasSourceMap<'sm>: Copy {
25 #[must_use]
26 fn source_map(self) -> &'sm SourceMap;
27}
28impl<'sm> HasSourceMap<'sm> for &'sm SourceMap {
29 #[inline]
30 fn source_map(self) -> &'sm SourceMap {
31 self
32 }
33}
34impl<'sm> HasSourceMap<'sm> for &'sm Session {
35 #[inline]
36 fn source_map(self) -> &'sm SourceMap {
37 self.early_sess.source_map()
38 }
39}
40impl<'sm> HasSourceMap<'sm> for TyCtxt<'sm> {
41 #[inline]
42 fn source_map(self) -> &'sm SourceMap {
43 self.sess.source_map()
44 }
45}
46impl<'sm> HasSourceMap<'sm> for &'sm EarlyContext<'_> {
47 #[inline]
48 fn source_map(self) -> &'sm SourceMap {
49 ::rustc_lint::LintContext::sess(self).source_map()
50 }
51}
52impl<'sm> HasSourceMap<'sm> for &LateContext<'sm> {
53 #[inline]
54 fn source_map(self) -> &'sm SourceMap {
55 self.tcx.sess.source_map()
56 }
57}
58
59pub trait SpanRange: Sized {
61 fn into_range(self) -> Range<BytePos>;
63}
64impl SpanRange for Span {
65 fn into_range(self) -> Range<BytePos> {
66 let (lo, hi) = self.lo_hi();
67 lo..hi
68 }
69}
70impl SpanRange for SpanData {
71 fn into_range(self) -> Range<BytePos> {
72 self.lo..self.hi
73 }
74}
75impl SpanRange for Range<BytePos> {
76 fn into_range(self) -> Range<BytePos> {
77 self
78 }
79}
80
81pub trait IntoSpan: Sized {
83 fn into_span(self) -> Span;
85 fn with_ctxt(self, ctxt: SyntaxContext) -> Span;
87}
88impl IntoSpan for Span {
89 fn into_span(self) -> Span {
90 self
91 }
92 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
93 self.with_ctxt(ctxt)
94 }
95}
96impl IntoSpan for SpanData {
97 fn into_span(self) -> Span {
98 self.span()
99 }
100 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
101 Span::new(self.lo, self.hi, ctxt, self.parent)
102 }
103}
104impl IntoSpan for Range<BytePos> {
105 fn into_span(self) -> Span {
106 Span::with_root_ctxt(self.start, self.end)
107 }
108 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
109 Span::new(self.start, self.end, ctxt, None)
110 }
111}
112
113pub trait SpanExt: SpanRange {
116 fn get_text<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<SourceText> {
119 get_source_range(sm.source_map(), self.into_range()).and_then(SourceText::new)
120 }
121
122 fn get_source_range<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<SourceFileRange> {
125 get_source_range(sm.source_map(), self.into_range())
126 }
127
128 fn with_source_text<'sm, T>(self, sm: impl HasSourceMap<'sm>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
131 with_source_text(sm.source_map(), self.into_range(), f)
132 }
133
134 fn check_text<'sm>(self, sm: impl HasSourceMap<'sm>, pred: impl for<'a> FnOnce(&'a str) -> bool) -> bool {
137 self.with_source_text(sm, pred).unwrap_or(false)
138 }
139
140 fn with_source_text_and_range<'sm, T>(
143 self,
144 sm: impl HasSourceMap<'sm>,
145 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
146 ) -> Option<T> {
147 with_source_text_and_range(sm.source_map(), self.into_range(), f)
148 }
149
150 fn map_range<'sm>(
156 self,
157 sm: impl HasSourceMap<'sm>,
158 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
159 ) -> Option<Range<BytePos>> {
160 map_range(sm.source_map(), self.into_range(), f)
161 }
162
163 fn with_leading_whitespace<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range<BytePos> {
177 with_leading_whitespace(sm.source_map(), self.into_range())
178 }
179
180 fn trim_start<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range<BytePos> {
182 trim_start(sm.source_map(), self.into_range())
183 }
184}
185impl<T: SpanRange> SpanExt for T {}
186
187pub struct SourceText(SourceFileRange);
189impl SourceText {
190 pub fn new(text: SourceFileRange) -> Option<Self> {
192 if text.as_str().is_some() {
193 Some(Self(text))
194 } else {
195 None
196 }
197 }
198
199 pub fn as_str(&self) -> &str {
201 self.0.as_str().unwrap()
202 }
203
204 pub fn to_owned(&self) -> String {
206 self.as_str().to_owned()
207 }
208}
209impl Deref for SourceText {
210 type Target = str;
211 fn deref(&self) -> &Self::Target {
212 self.as_str()
213 }
214}
215impl AsRef<str> for SourceText {
216 fn as_ref(&self) -> &str {
217 self.as_str()
218 }
219}
220impl<T> Index<T> for SourceText
221where
222 str: Index<T>,
223{
224 type Output = <str as Index<T>>::Output;
225 fn index(&self, idx: T) -> &Self::Output {
226 &self.as_str()[idx]
227 }
228}
229impl fmt::Display for SourceText {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 self.as_str().fmt(f)
232 }
233}
234impl fmt::Debug for SourceText {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 self.as_str().fmt(f)
237 }
238}
239
240fn get_source_range(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
241 let start = sm.lookup_byte_offset(sp.start);
242 let end = sm.lookup_byte_offset(sp.end);
243 if !Arc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
244 return None;
245 }
246 sm.ensure_source_file_source_present(&start.sf);
247 let range = start.pos.to_usize()..end.pos.to_usize();
248 Some(SourceFileRange { sf: start.sf, range })
249}
250
251fn with_source_text<T>(sm: &SourceMap, sp: Range<BytePos>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
252 if let Some(src) = get_source_range(sm, sp)
253 && let Some(src) = src.as_str()
254 {
255 Some(f(src))
256 } else {
257 None
258 }
259}
260
261fn with_source_text_and_range<T>(
262 sm: &SourceMap,
263 sp: Range<BytePos>,
264 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
265) -> Option<T> {
266 if let Some(src) = get_source_range(sm, sp)
267 && let Some(text) = &src.sf.src
268 {
269 Some(f(text, src.range))
270 } else {
271 None
272 }
273}
274
275#[expect(clippy::cast_possible_truncation)]
276fn map_range(
277 sm: &SourceMap,
278 sp: Range<BytePos>,
279 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
280) -> Option<Range<BytePos>> {
281 if let Some(src) = get_source_range(sm, sp.clone())
282 && let Some(text) = &src.sf.src
283 && let Some(range) = f(&src.sf, text, src.range.clone())
284 {
285 debug_assert!(
286 range.start <= text.len() && range.end <= text.len(),
287 "Range `{range:?}` is outside the source file (file `{}`, length `{}`)",
288 src.sf.name.prefer_local_unconditionally(),
289 text.len(),
290 );
291 debug_assert!(range.start <= range.end, "Range `{range:?}` has overlapping bounds");
292 let dstart = (range.start as u32).wrapping_sub(src.range.start as u32);
293 let dend = (range.end as u32).wrapping_sub(src.range.start as u32);
294 Some(BytePos(sp.start.0.wrapping_add(dstart))..BytePos(sp.start.0.wrapping_add(dend)))
295 } else {
296 None
297 }
298}
299
300fn ends_with_line_comment_or_broken(text: &str) -> bool {
301 let Some(last) = tokenize(text, FrontmatterAllowed::No).last() else {
302 return false;
303 };
304 match last.kind {
305 TokenKind::LineComment { .. } | TokenKind::BlockComment { terminated: false, .. } => true,
309 TokenKind::Literal { kind, .. } => matches!(
310 kind,
311 LiteralKind::Byte { terminated: false }
312 | LiteralKind::ByteStr { terminated: false }
313 | LiteralKind::CStr { terminated: false }
314 | LiteralKind::Char { terminated: false }
315 | LiteralKind::RawByteStr { n_hashes: None }
316 | LiteralKind::RawCStr { n_hashes: None }
317 | LiteralKind::RawStr { n_hashes: None }
318 ),
319 _ => false,
320 }
321}
322
323fn with_leading_whitespace_inner(lines: &[RelativeBytePos], src: &str, range: Range<usize>) -> Option<usize> {
324 debug_assert!(lines.is_empty() || lines[0].to_u32() == 0);
325
326 let start = src.get(..range.start)?.trim_end();
327 let next_line = lines.partition_point(|&pos| pos.to_usize() <= start.len());
328 if let Some(line_end) = lines.get(next_line)
329 && line_end.to_usize() <= range.start
330 && let prev_start = lines.get(next_line - 1).map_or(0, |&x| x.to_usize())
331 && ends_with_line_comment_or_broken(&start[prev_start..])
332 && let next_line = lines.partition_point(|&pos| pos.to_usize() < range.end)
333 && let next_start = lines.get(next_line).map_or(src.len(), |&x| x.to_usize())
334 && tokenize(src.get(range.end..next_start)?, FrontmatterAllowed::No)
335 .any(|t| !matches!(t.kind, TokenKind::Whitespace))
336 {
337 Some(range.start)
338 } else {
339 Some(start.len())
340 }
341}
342
343fn with_leading_whitespace(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
344 map_range(sm, sp.clone(), |sf, src, range| {
345 Some(with_leading_whitespace_inner(sf.lines(), src, range.clone())?..range.end)
346 })
347 .unwrap_or(sp)
348}
349
350fn trim_start(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
351 map_range(sm, sp.clone(), |_, src, range| {
352 let src = src.get(range.clone())?;
353 Some(range.start + (src.len() - src.trim_start().len())..range.end)
354 })
355 .unwrap_or(sp)
356}
357
358pub struct SourceFileRange {
360 pub sf: Arc<SourceFile>,
362 pub range: Range<usize>,
364}
365impl SourceFileRange {
366 pub fn as_str(&self) -> Option<&str> {
369 (self.sf.src.as_ref().map(|src| src.as_str()))
370 .or_else(|| self.sf.external_src.get()?.get_source())
371 .and_then(|x| x.get(self.range.clone()))
372 }
373}
374
375pub fn expr_block<'sm>(
377 sm: impl HasSourceMap<'sm>,
378 expr: &Expr<'_>,
379 outer: SyntaxContext,
380 default: &str,
381 indent_relative_to: Option<Span>,
382 app: &mut Applicability,
383) -> String {
384 let (code, from_macro) = snippet_block_with_context(sm, expr.span, outer, default, indent_relative_to, app);
385 if !from_macro
386 && let ExprKind::Block(block, None) = expr.kind
387 && block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
388 {
389 code
390 } else {
391 format!("{{ {code} }}")
396 }
397}
398
399pub fn first_line_of_span<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
410 first_char_in_first_line(sm, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
411}
412
413fn first_char_in_first_line<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<BytePos> {
414 let line_span = line_span(sm, span);
415 snippet_opt(sm, line_span).and_then(|snip| {
416 snip.find(|c: char| !c.is_whitespace())
417 .map(|pos| line_span.lo() + BytePos::from_usize(pos))
418 })
419}
420
421fn line_span<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
431 let span = original_sp(span, DUMMY_SP);
432 let SourceFileAndLine { sf, line } = sm.source_map().lookup_line(span.lo()).unwrap();
433 let line_start = sf.lines()[line];
434 let line_start = sf.absolute_position(line_start);
435 span.with_lo(line_start)
436}
437
438pub fn indent_of<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<usize> {
447 snippet_opt(sm, line_span(sm, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
448}
449
450pub fn snippet_indent<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<String> {
452 snippet_opt(sm, line_span(sm, span)).map(|mut s| {
453 let len = s.len() - s.trim_start().len();
454 s.truncate(len);
455 s
456 })
457}
458
459pub fn is_present_in_source<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> bool {
466 if let Some(snippet) = snippet_opt(sm, span)
467 && snippet.is_empty()
468 {
469 return false;
470 }
471 true
472}
473
474pub fn position_before_rarrow(s: &str) -> Option<usize> {
486 s.rfind("->").map(|rpos| {
487 let mut rpos = rpos;
488 let chars: Vec<char> = s.chars().collect();
489 while rpos > 1 {
490 if let Some(c) = chars.get(rpos - 1)
491 && c.is_whitespace()
492 {
493 rpos -= 1;
494 continue;
495 }
496 break;
497 }
498 rpos
499 })
500}
501
502pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
504 let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');
505 let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
506 reindent_multiline_inner(&s_tab, ignore_first, indent, ' ')
507}
508
509fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
510 let x = s
511 .lines()
512 .skip(usize::from(ignore_first))
513 .filter_map(|l| {
514 if l.is_empty() {
515 None
516 } else {
517 Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
519 }
520 })
521 .min()
522 .unwrap_or(0);
523 let indent = indent.unwrap_or(0);
524 s.lines()
525 .enumerate()
526 .map(|(i, l)| {
527 if (ignore_first && i == 0) || l.is_empty() {
528 l.to_owned()
529 } else if x > indent {
530 l.split_at(x - indent).1.to_owned()
531 } else {
532 " ".repeat(indent - x) + l
533 }
534 })
535 .collect::<Vec<String>>()
536 .join("\n")
537}
538
539pub fn snippet<'a, 'sm>(sm: impl HasSourceMap<'sm>, span: Span, default: &'a str) -> Cow<'a, str> {
559 snippet_opt(sm, span).map_or_else(|| Cow::Borrowed(default), From::from)
560}
561
562pub fn snippet_with_applicability<'a, 'sm>(
571 sm: impl HasSourceMap<'sm>,
572 span: Span,
573 default: &'a str,
574 applicability: &mut Applicability,
575) -> Cow<'a, str> {
576 snippet_with_applicability_sm(sm.source_map(), span, default, applicability)
577}
578
579fn snippet_with_applicability_sm<'a>(
580 sm: &SourceMap,
581 span: Span,
582 default: &'a str,
583 applicability: &mut Applicability,
584) -> Cow<'a, str> {
585 if *applicability != Applicability::Unspecified && span.from_expansion() {
586 *applicability = Applicability::MaybeIncorrect;
587 }
588 if let Some(t) = snippet_opt(sm, span) {
589 Cow::Owned(t)
590 } else {
591 if *applicability == Applicability::MachineApplicable {
592 *applicability = Applicability::HasPlaceholders;
593 }
594 Cow::Borrowed(default)
595 }
596}
597
598pub fn snippet_opt<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<String> {
600 sm.source_map().span_to_snippet(span).ok()
601}
602
603pub fn snippet_block<'sm>(
638 sm: impl HasSourceMap<'sm>,
639 span: Span,
640 default: &str,
641 indent_relative_to: Option<Span>,
642) -> String {
643 let snip = snippet(sm, span, default);
644 let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
645 reindent_multiline(&snip, true, indent)
646}
647
648pub fn snippet_block_with_applicability<'sm>(
651 sm: impl HasSourceMap<'sm>,
652 span: Span,
653 default: &str,
654 indent_relative_to: Option<Span>,
655 applicability: &mut Applicability,
656) -> String {
657 let snip = snippet_with_applicability(sm, span, default, applicability);
658 let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
659 reindent_multiline(&snip, true, indent)
660}
661
662pub fn snippet_block_with_context<'sm>(
664 sm: impl HasSourceMap<'sm>,
665 span: Span,
666 outer: SyntaxContext,
667 default: &str,
668 indent_relative_to: Option<Span>,
669 app: &mut Applicability,
670) -> (String, bool) {
671 let (snip, from_macro) = snippet_with_context(sm, span, outer, default, app);
672 let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
673 (reindent_multiline(&snip, true, indent), from_macro)
674}
675
676pub fn snippet_with_context<'a, 'sm>(
687 sm: impl HasSourceMap<'sm>,
688 span: Span,
689 outer: SyntaxContext,
690 default: &'a str,
691 applicability: &mut Applicability,
692) -> (Cow<'a, str>, bool) {
693 snippet_with_context_sm(sm.source_map(), span, outer, default, applicability)
694}
695
696fn snippet_with_context_sm<'a>(
697 sm: &SourceMap,
698 span: Span,
699 outer: SyntaxContext,
700 default: &'a str,
701 applicability: &mut Applicability,
702) -> (Cow<'a, str>, bool) {
703 if span.desugaring_kind() == Some(DesugaringKind::RangeExpr) && span.parent_callsite().unwrap().ctxt() == outer {
705 return (snippet_with_applicability_sm(sm, span, default, applicability), false);
706 }
707
708 let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
709 || {
710 if *applicability != Applicability::Unspecified {
712 *applicability = Applicability::MaybeIncorrect;
713 }
714 (span, false)
716 },
717 |outer_span| (outer_span, span.ctxt() != outer),
718 );
719
720 (
721 snippet_with_applicability_sm(sm, span, default, applicability),
722 is_macro_call,
723 )
724}
725
726pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
754 let outer_span = hygiene::walk_chain(span, outer);
755 (outer_span.ctxt() == outer).then_some(outer_span)
756}
757
758pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
760 let data = span.data();
761 let sf: &_ = &sm.lookup_source_file(data.lo);
762 let Some(src) = sf.src.as_deref() else {
763 return span;
764 };
765 let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
766 return span;
767 };
768 let trim_start = snip.len() - snip.trim_start().len();
769 let trim_end = snip.len() - snip.trim_end().len();
770 SpanData {
771 lo: data.lo + BytePos::from_usize(trim_start),
772 hi: data.hi - BytePos::from_usize(trim_end),
773 ctxt: data.ctxt,
774 parent: data.parent,
775 }
776 .span()
777}
778
779pub fn expand_past_previous_comma<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
785 let extended = sm.source_map().span_extend_to_prev_char(span, ',', true);
786 extended.with_lo(extended.lo() - BytePos(1))
787}
788
789pub fn str_literal_to_char_literal<'sm>(
792 sm: impl HasSourceMap<'sm>,
793 expr: &Expr<'_>,
794 applicability: &mut Applicability,
795 ascii_only: bool,
796) -> Option<String> {
797 if let ExprKind::Lit(lit) = &expr.kind
798 && let LitKind::Str(r, style) = lit.node
799 && let string = r.as_str()
800 && let len = if ascii_only {
801 string.len()
802 } else {
803 string.chars().count()
804 }
805 && len == 1
806 {
807 let snip = snippet_with_applicability(sm, expr.span, string, applicability);
808 let ch = if let StrStyle::Raw(nhash) = style {
809 let nhash = nhash as usize;
810 &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
812 } else {
813 &snip[1..(snip.len() - 1)]
815 };
816
817 let hint = format!(
818 "'{}'",
819 match ch {
820 "'" => "\\'",
821 r"\" => "\\\\",
822 "\\\"" => "\"", _ => ch,
824 }
825 );
826
827 Some(hint)
828 } else {
829 None
830 }
831}
832
833#[cfg(test)]
834mod test {
835 use super::reindent_multiline;
836
837 #[test]
838 fn test_reindent_multiline_single_line() {
839 assert_eq!("", reindent_multiline("", false, None));
840 assert_eq!("...", reindent_multiline("...", false, None));
841 assert_eq!("...", reindent_multiline(" ...", false, None));
842 assert_eq!("...", reindent_multiline("\t...", false, None));
843 assert_eq!("...", reindent_multiline("\t\t...", false, None));
844 }
845
846 #[test]
847 #[rustfmt::skip]
848 fn test_reindent_multiline_block() {
849 assert_eq!("\
850 if x {
851 y
852 } else {
853 z
854 }", reindent_multiline(" if x {
855 y
856 } else {
857 z
858 }", false, None));
859 assert_eq!("\
860 if x {
861 \ty
862 } else {
863 \tz
864 }", reindent_multiline(" if x {
865 \ty
866 } else {
867 \tz
868 }", false, None));
869 }
870
871 #[test]
872 #[rustfmt::skip]
873 fn test_reindent_multiline_empty_line() {
874 assert_eq!("\
875 if x {
876 y
877
878 } else {
879 z
880 }", reindent_multiline(" if x {
881 y
882
883 } else {
884 z
885 }", false, None));
886 }
887
888 #[test]
889 #[rustfmt::skip]
890 fn test_reindent_multiline_lines_deeper() {
891 assert_eq!("\
892 if x {
893 y
894 } else {
895 z
896 }", reindent_multiline("\
897 if x {
898 y
899 } else {
900 z
901 }", true, Some(8)));
902 }
903}