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_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, 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 {
24 #[must_use]
25 fn source_map(self) -> &'sm SourceMap;
26}
27impl<'sm> HasSourceMap<'sm> for &'sm SourceMap {
28 #[inline]
29 fn source_map(self) -> &'sm SourceMap {
30 self
31 }
32}
33impl<'sm> HasSourceMap<'sm> for &'sm Session {
34 #[inline]
35 fn source_map(self) -> &'sm SourceMap {
36 self.source_map()
37 }
38}
39impl<'sm> HasSourceMap<'sm> for TyCtxt<'sm> {
40 #[inline]
41 fn source_map(self) -> &'sm SourceMap {
42 self.sess.source_map()
43 }
44}
45impl<'sm> HasSourceMap<'sm> for &'sm EarlyContext<'_> {
46 #[inline]
47 fn source_map(self) -> &'sm SourceMap {
48 ::rustc_lint::LintContext::sess(self).source_map()
49 }
50}
51impl<'sm> HasSourceMap<'sm> for &LateContext<'sm> {
52 #[inline]
53 fn source_map(self) -> &'sm SourceMap {
54 self.tcx.sess.source_map()
55 }
56}
57
58pub trait SpanRange: Sized {
60 fn into_range(self) -> Range<BytePos>;
61}
62impl SpanRange for Span {
63 fn into_range(self) -> Range<BytePos> {
64 let data = self.data();
65 data.lo..data.hi
66 }
67}
68impl SpanRange for SpanData {
69 fn into_range(self) -> Range<BytePos> {
70 self.lo..self.hi
71 }
72}
73impl SpanRange for Range<BytePos> {
74 fn into_range(self) -> Range<BytePos> {
75 self
76 }
77}
78
79pub trait IntoSpan: Sized {
81 fn into_span(self) -> Span;
82 fn with_ctxt(self, ctxt: SyntaxContext) -> Span;
83}
84impl IntoSpan for Span {
85 fn into_span(self) -> Span {
86 self
87 }
88 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
89 self.with_ctxt(ctxt)
90 }
91}
92impl IntoSpan for SpanData {
93 fn into_span(self) -> Span {
94 self.span()
95 }
96 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
97 Span::new(self.lo, self.hi, ctxt, self.parent)
98 }
99}
100impl IntoSpan for Range<BytePos> {
101 fn into_span(self) -> Span {
102 Span::with_root_ctxt(self.start, self.end)
103 }
104 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
105 Span::new(self.start, self.end, ctxt, None)
106 }
107}
108
109pub trait SpanExt: SpanRange {
110 fn get_text<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<SourceText> {
113 get_source_range(sm.source_map(), self.into_range()).and_then(SourceText::new)
114 }
115
116 fn get_source_range<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<SourceFileRange> {
119 get_source_range(sm.source_map(), self.into_range())
120 }
121
122 fn with_source_text<'sm, T>(self, sm: impl HasSourceMap<'sm>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
125 with_source_text(sm.source_map(), self.into_range(), f)
126 }
127
128 fn check_text<'sm>(self, sm: impl HasSourceMap<'sm>, pred: impl for<'a> FnOnce(&'a str) -> bool) -> bool {
131 self.with_source_text(sm, pred).unwrap_or(false)
132 }
133
134 fn with_source_text_and_range<'sm, T>(
137 self,
138 sm: impl HasSourceMap<'sm>,
139 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
140 ) -> Option<T> {
141 with_source_text_and_range(sm.source_map(), self.into_range(), f)
142 }
143
144 fn map_range<'sm>(
150 self,
151 sm: impl HasSourceMap<'sm>,
152 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
153 ) -> Option<Range<BytePos>> {
154 map_range(sm.source_map(), self.into_range(), f)
155 }
156
157 fn with_leading_whitespace<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range<BytePos> {
171 with_leading_whitespace(sm.source_map(), self.into_range())
172 }
173
174 fn trim_start<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range<BytePos> {
176 trim_start(sm.source_map(), self.into_range())
177 }
178}
179impl<T: SpanRange> SpanExt for T {}
180
181pub struct SourceText(SourceFileRange);
183impl SourceText {
184 pub fn new(text: SourceFileRange) -> Option<Self> {
186 if text.as_str().is_some() {
187 Some(Self(text))
188 } else {
189 None
190 }
191 }
192
193 pub fn as_str(&self) -> &str {
195 self.0.as_str().unwrap()
196 }
197
198 pub fn to_owned(&self) -> String {
200 self.as_str().to_owned()
201 }
202}
203impl Deref for SourceText {
204 type Target = str;
205 fn deref(&self) -> &Self::Target {
206 self.as_str()
207 }
208}
209impl AsRef<str> for SourceText {
210 fn as_ref(&self) -> &str {
211 self.as_str()
212 }
213}
214impl<T> Index<T> for SourceText
215where
216 str: Index<T>,
217{
218 type Output = <str as Index<T>>::Output;
219 fn index(&self, idx: T) -> &Self::Output {
220 &self.as_str()[idx]
221 }
222}
223impl fmt::Display for SourceText {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 self.as_str().fmt(f)
226 }
227}
228impl fmt::Debug for SourceText {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 self.as_str().fmt(f)
231 }
232}
233
234fn get_source_range(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
235 let start = sm.lookup_byte_offset(sp.start);
236 let end = sm.lookup_byte_offset(sp.end);
237 if !Arc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
238 return None;
239 }
240 sm.ensure_source_file_source_present(&start.sf);
241 let range = start.pos.to_usize()..end.pos.to_usize();
242 Some(SourceFileRange { sf: start.sf, range })
243}
244
245fn with_source_text<T>(sm: &SourceMap, sp: Range<BytePos>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
246 if let Some(src) = get_source_range(sm, sp)
247 && let Some(src) = src.as_str()
248 {
249 Some(f(src))
250 } else {
251 None
252 }
253}
254
255fn with_source_text_and_range<T>(
256 sm: &SourceMap,
257 sp: Range<BytePos>,
258 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
259) -> Option<T> {
260 if let Some(src) = get_source_range(sm, sp)
261 && let Some(text) = &src.sf.src
262 {
263 Some(f(text, src.range))
264 } else {
265 None
266 }
267}
268
269#[expect(clippy::cast_possible_truncation)]
270fn map_range(
271 sm: &SourceMap,
272 sp: Range<BytePos>,
273 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
274) -> Option<Range<BytePos>> {
275 if let Some(src) = get_source_range(sm, sp.clone())
276 && let Some(text) = &src.sf.src
277 && let Some(range) = f(&src.sf, text, src.range.clone())
278 {
279 debug_assert!(
280 range.start <= text.len() && range.end <= text.len(),
281 "Range `{range:?}` is outside the source file (file `{}`, length `{}`)",
282 src.sf.name.prefer_local_unconditionally(),
283 text.len(),
284 );
285 debug_assert!(range.start <= range.end, "Range `{range:?}` has overlapping bounds");
286 let dstart = (range.start as u32).wrapping_sub(src.range.start as u32);
287 let dend = (range.end as u32).wrapping_sub(src.range.start as u32);
288 Some(BytePos(sp.start.0.wrapping_add(dstart))..BytePos(sp.start.0.wrapping_add(dend)))
289 } else {
290 None
291 }
292}
293
294fn ends_with_line_comment_or_broken(text: &str) -> bool {
295 let Some(last) = tokenize(text, FrontmatterAllowed::No).last() else {
296 return false;
297 };
298 match last.kind {
299 TokenKind::LineComment { .. } | TokenKind::BlockComment { terminated: false, .. } => true,
303 TokenKind::Literal { kind, .. } => matches!(
304 kind,
305 LiteralKind::Byte { terminated: false }
306 | LiteralKind::ByteStr { terminated: false }
307 | LiteralKind::CStr { terminated: false }
308 | LiteralKind::Char { terminated: false }
309 | LiteralKind::RawByteStr { n_hashes: None }
310 | LiteralKind::RawCStr { n_hashes: None }
311 | LiteralKind::RawStr { n_hashes: None }
312 ),
313 _ => false,
314 }
315}
316
317fn with_leading_whitespace_inner(lines: &[RelativeBytePos], src: &str, range: Range<usize>) -> Option<usize> {
318 debug_assert!(lines.is_empty() || lines[0].to_u32() == 0);
319
320 let start = src.get(..range.start)?.trim_end();
321 let next_line = lines.partition_point(|&pos| pos.to_usize() <= start.len());
322 if let Some(line_end) = lines.get(next_line)
323 && line_end.to_usize() <= range.start
324 && let prev_start = lines.get(next_line - 1).map_or(0, |&x| x.to_usize())
325 && ends_with_line_comment_or_broken(&start[prev_start..])
326 && let next_line = lines.partition_point(|&pos| pos.to_usize() < range.end)
327 && let next_start = lines.get(next_line).map_or(src.len(), |&x| x.to_usize())
328 && tokenize(src.get(range.end..next_start)?, FrontmatterAllowed::No)
329 .any(|t| !matches!(t.kind, TokenKind::Whitespace))
330 {
331 Some(range.start)
332 } else {
333 Some(start.len())
334 }
335}
336
337fn with_leading_whitespace(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
338 map_range(sm, sp.clone(), |sf, src, range| {
339 Some(with_leading_whitespace_inner(sf.lines(), src, range.clone())?..range.end)
340 })
341 .unwrap_or(sp)
342}
343
344fn trim_start(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
345 map_range(sm, sp.clone(), |_, src, range| {
346 let src = src.get(range.clone())?;
347 Some(range.start + (src.len() - src.trim_start().len())..range.end)
348 })
349 .unwrap_or(sp)
350}
351
352pub struct SourceFileRange {
353 pub sf: Arc<SourceFile>,
354 pub range: Range<usize>,
355}
356impl SourceFileRange {
357 pub fn as_str(&self) -> Option<&str> {
360 (self.sf.src.as_ref().map(|src| src.as_str()))
361 .or_else(|| self.sf.external_src.get()?.get_source())
362 .and_then(|x| x.get(self.range.clone()))
363 }
364}
365
366pub fn expr_block<'sm>(
368 sm: impl HasSourceMap<'sm>,
369 expr: &Expr<'_>,
370 outer: SyntaxContext,
371 default: &str,
372 indent_relative_to: Option<Span>,
373 app: &mut Applicability,
374) -> String {
375 let (code, from_macro) = snippet_block_with_context(sm, expr.span, outer, default, indent_relative_to, app);
376 if !from_macro
377 && let ExprKind::Block(block, None) = expr.kind
378 && block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
379 {
380 code
381 } else {
382 format!("{{ {code} }}")
387 }
388}
389
390pub fn first_line_of_span<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
401 first_char_in_first_line(sm, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
402}
403
404fn first_char_in_first_line<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<BytePos> {
405 let line_span = line_span(sm, span);
406 snippet_opt(sm, line_span).and_then(|snip| {
407 snip.find(|c: char| !c.is_whitespace())
408 .map(|pos| line_span.lo() + BytePos::from_usize(pos))
409 })
410}
411
412fn line_span<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
422 let span = original_sp(span, DUMMY_SP);
423 let SourceFileAndLine { sf, line } = sm.source_map().lookup_line(span.lo()).unwrap();
424 let line_start = sf.lines()[line];
425 let line_start = sf.absolute_position(line_start);
426 span.with_lo(line_start)
427}
428
429pub fn indent_of<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<usize> {
438 snippet_opt(sm, line_span(sm, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
439}
440
441pub fn snippet_indent<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<String> {
443 snippet_opt(sm, line_span(sm, span)).map(|mut s| {
444 let len = s.len() - s.trim_start().len();
445 s.truncate(len);
446 s
447 })
448}
449
450pub fn is_present_in_source<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> bool {
456 if let Some(snippet) = snippet_opt(sm, span)
457 && snippet.is_empty()
458 {
459 return false;
460 }
461 true
462}
463
464pub fn position_before_rarrow(s: &str) -> Option<usize> {
476 s.rfind("->").map(|rpos| {
477 let mut rpos = rpos;
478 let chars: Vec<char> = s.chars().collect();
479 while rpos > 1 {
480 if let Some(c) = chars.get(rpos - 1)
481 && c.is_whitespace()
482 {
483 rpos -= 1;
484 continue;
485 }
486 break;
487 }
488 rpos
489 })
490}
491
492pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
494 let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');
495 let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
496 reindent_multiline_inner(&s_tab, ignore_first, indent, ' ')
497}
498
499fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
500 let x = s
501 .lines()
502 .skip(usize::from(ignore_first))
503 .filter_map(|l| {
504 if l.is_empty() {
505 None
506 } else {
507 Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
509 }
510 })
511 .min()
512 .unwrap_or(0);
513 let indent = indent.unwrap_or(0);
514 s.lines()
515 .enumerate()
516 .map(|(i, l)| {
517 if (ignore_first && i == 0) || l.is_empty() {
518 l.to_owned()
519 } else if x > indent {
520 l.split_at(x - indent).1.to_owned()
521 } else {
522 " ".repeat(indent - x) + l
523 }
524 })
525 .collect::<Vec<String>>()
526 .join("\n")
527}
528
529pub fn snippet<'a, 'sm>(sm: impl HasSourceMap<'sm>, span: Span, default: &'a str) -> Cow<'a, str> {
549 snippet_opt(sm, span).map_or_else(|| Cow::Borrowed(default), From::from)
550}
551
552pub fn snippet_with_applicability<'a, 'sm>(
562 sm: impl HasSourceMap<'sm>,
563 span: Span,
564 default: &'a str,
565 applicability: &mut Applicability,
566) -> Cow<'a, str> {
567 snippet_with_applicability_sm(sm.source_map(), span, default, applicability)
568}
569
570fn snippet_with_applicability_sm<'a>(
571 sm: &SourceMap,
572 span: Span,
573 default: &'a str,
574 applicability: &mut Applicability,
575) -> Cow<'a, str> {
576 if *applicability != Applicability::Unspecified && span.from_expansion() {
577 *applicability = Applicability::MaybeIncorrect;
578 }
579 if let Some(t) = snippet_opt(sm, span) {
580 Cow::Owned(t)
581 } else {
582 if *applicability == Applicability::MachineApplicable {
583 *applicability = Applicability::HasPlaceholders;
584 }
585 Cow::Borrowed(default)
586 }
587}
588
589pub fn snippet_opt<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Option<String> {
591 sm.source_map().span_to_snippet(span).ok()
592}
593
594pub fn snippet_block<'sm>(
629 sm: impl HasSourceMap<'sm>,
630 span: Span,
631 default: &str,
632 indent_relative_to: Option<Span>,
633) -> String {
634 let snip = snippet(sm, span, default);
635 let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
636 reindent_multiline(&snip, true, indent)
637}
638
639pub fn snippet_block_with_applicability<'sm>(
642 sm: impl HasSourceMap<'sm>,
643 span: Span,
644 default: &str,
645 indent_relative_to: Option<Span>,
646 applicability: &mut Applicability,
647) -> String {
648 let snip = snippet_with_applicability(sm, span, default, applicability);
649 let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
650 reindent_multiline(&snip, true, indent)
651}
652
653pub fn snippet_block_with_context<'sm>(
655 sm: impl HasSourceMap<'sm>,
656 span: Span,
657 outer: SyntaxContext,
658 default: &str,
659 indent_relative_to: Option<Span>,
660 app: &mut Applicability,
661) -> (String, bool) {
662 let (snip, from_macro) = snippet_with_context(sm, span, outer, default, app);
663 let indent = indent_relative_to.and_then(|s| indent_of(sm, s));
664 (reindent_multiline(&snip, true, indent), from_macro)
665}
666
667pub fn snippet_with_context<'a, 'sm>(
678 sm: impl HasSourceMap<'sm>,
679 span: Span,
680 outer: SyntaxContext,
681 default: &'a str,
682 applicability: &mut Applicability,
683) -> (Cow<'a, str>, bool) {
684 snippet_with_context_sm(sm.source_map(), span, outer, default, applicability)
685}
686
687fn snippet_with_context_sm<'a>(
688 sm: &SourceMap,
689 span: Span,
690 outer: SyntaxContext,
691 default: &'a str,
692 applicability: &mut Applicability,
693) -> (Cow<'a, str>, bool) {
694 if span.desugaring_kind() == Some(DesugaringKind::RangeExpr) && span.parent_callsite().unwrap().ctxt() == outer {
696 return (snippet_with_applicability_sm(sm, span, default, applicability), false);
697 }
698
699 let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
700 || {
701 if *applicability != Applicability::Unspecified {
703 *applicability = Applicability::MaybeIncorrect;
704 }
705 (span, false)
707 },
708 |outer_span| (outer_span, span.ctxt() != outer),
709 );
710
711 (
712 snippet_with_applicability_sm(sm, span, default, applicability),
713 is_macro_call,
714 )
715}
716
717pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
745 let outer_span = hygiene::walk_chain(span, outer);
746 (outer_span.ctxt() == outer).then_some(outer_span)
747}
748
749pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
751 let data = span.data();
752 let sf: &_ = &sm.lookup_source_file(data.lo);
753 let Some(src) = sf.src.as_deref() else {
754 return span;
755 };
756 let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
757 return span;
758 };
759 let trim_start = snip.len() - snip.trim_start().len();
760 let trim_end = snip.len() - snip.trim_end().len();
761 SpanData {
762 lo: data.lo + BytePos::from_usize(trim_start),
763 hi: data.hi - BytePos::from_usize(trim_end),
764 ctxt: data.ctxt,
765 parent: data.parent,
766 }
767 .span()
768}
769
770pub fn expand_past_previous_comma<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Span {
776 let extended = sm.source_map().span_extend_to_prev_char(span, ',', true);
777 extended.with_lo(extended.lo() - BytePos(1))
778}
779
780pub fn str_literal_to_char_literal<'sm>(
783 sm: impl HasSourceMap<'sm>,
784 expr: &Expr<'_>,
785 applicability: &mut Applicability,
786 ascii_only: bool,
787) -> Option<String> {
788 if let ExprKind::Lit(lit) = &expr.kind
789 && let LitKind::Str(r, style) = lit.node
790 && let string = r.as_str()
791 && let len = if ascii_only {
792 string.len()
793 } else {
794 string.chars().count()
795 }
796 && len == 1
797 {
798 let snip = snippet_with_applicability(sm, expr.span, string, applicability);
799 let ch = if let StrStyle::Raw(nhash) = style {
800 let nhash = nhash as usize;
801 &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
803 } else {
804 &snip[1..(snip.len() - 1)]
806 };
807
808 let hint = format!(
809 "'{}'",
810 match ch {
811 "'" => "\\'",
812 r"\" => "\\\\",
813 "\\\"" => "\"", _ => ch,
815 }
816 );
817
818 Some(hint)
819 } else {
820 None
821 }
822}
823
824#[cfg(test)]
825mod test {
826 use super::reindent_multiline;
827
828 #[test]
829 fn test_reindent_multiline_single_line() {
830 assert_eq!("", reindent_multiline("", false, None));
831 assert_eq!("...", reindent_multiline("...", false, None));
832 assert_eq!("...", reindent_multiline(" ...", false, None));
833 assert_eq!("...", reindent_multiline("\t...", false, None));
834 assert_eq!("...", reindent_multiline("\t\t...", false, None));
835 }
836
837 #[test]
838 #[rustfmt::skip]
839 fn test_reindent_multiline_block() {
840 assert_eq!("\
841 if x {
842 y
843 } else {
844 z
845 }", reindent_multiline(" if x {
846 y
847 } else {
848 z
849 }", false, None));
850 assert_eq!("\
851 if x {
852 \ty
853 } else {
854 \tz
855 }", reindent_multiline(" if x {
856 \ty
857 } else {
858 \tz
859 }", false, None));
860 }
861
862 #[test]
863 #[rustfmt::skip]
864 fn test_reindent_multiline_empty_line() {
865 assert_eq!("\
866 if x {
867 y
868
869 } else {
870 z
871 }", reindent_multiline(" if x {
872 y
873
874 } else {
875 z
876 }", false, None));
877 }
878
879 #[test]
880 #[rustfmt::skip]
881 fn test_reindent_multiline_lines_deeper() {
882 assert_eq!("\
883 if x {
884 y
885 } else {
886 z
887 }", reindent_multiline("\
888 if x {
889 y
890 } else {
891 z
892 }", true, Some(8)));
893 }
894}