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, FileNameDisplayPreference, 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 HasSession {
24 fn sess(&self) -> &Session;
25}
26impl HasSession for Session {
27 fn sess(&self) -> &Session {
28 self
29 }
30}
31impl HasSession for TyCtxt<'_> {
32 fn sess(&self) -> &Session {
33 self.sess
34 }
35}
36impl HasSession for EarlyContext<'_> {
37 fn sess(&self) -> &Session {
38 ::rustc_lint::LintContext::sess(self)
39 }
40}
41impl HasSession for LateContext<'_> {
42 fn sess(&self) -> &Session {
43 self.tcx.sess()
44 }
45}
46
47pub trait SpanRange: Sized {
49 fn into_range(self) -> Range<BytePos>;
50}
51impl SpanRange for Span {
52 fn into_range(self) -> Range<BytePos> {
53 let data = self.data();
54 data.lo..data.hi
55 }
56}
57impl SpanRange for SpanData {
58 fn into_range(self) -> Range<BytePos> {
59 self.lo..self.hi
60 }
61}
62impl SpanRange for Range<BytePos> {
63 fn into_range(self) -> Range<BytePos> {
64 self
65 }
66}
67
68pub trait IntoSpan: Sized {
70 fn into_span(self) -> Span;
71 fn with_ctxt(self, ctxt: SyntaxContext) -> Span;
72}
73impl IntoSpan for Span {
74 fn into_span(self) -> Span {
75 self
76 }
77 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
78 self.with_ctxt(ctxt)
79 }
80}
81impl IntoSpan for SpanData {
82 fn into_span(self) -> Span {
83 self.span()
84 }
85 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
86 Span::new(self.lo, self.hi, ctxt, self.parent)
87 }
88}
89impl IntoSpan for Range<BytePos> {
90 fn into_span(self) -> Span {
91 Span::with_root_ctxt(self.start, self.end)
92 }
93 fn with_ctxt(self, ctxt: SyntaxContext) -> Span {
94 Span::new(self.start, self.end, ctxt, None)
95 }
96}
97
98pub trait SpanRangeExt: SpanRange {
99 fn get_source_text(self, cx: &impl HasSession) -> Option<SourceText> {
102 get_source_range(cx.sess().source_map(), self.into_range()).and_then(SourceText::new)
103 }
104
105 fn get_source_range(self, cx: &impl HasSession) -> Option<SourceFileRange> {
108 get_source_range(cx.sess().source_map(), self.into_range())
109 }
110
111 fn with_source_text<T>(self, cx: &impl HasSession, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
114 with_source_text(cx.sess().source_map(), self.into_range(), f)
115 }
116
117 fn check_source_text(self, cx: &impl HasSession, pred: impl for<'a> FnOnce(&'a str) -> bool) -> bool {
120 self.with_source_text(cx, pred).unwrap_or(false)
121 }
122
123 fn with_source_text_and_range<T>(
126 self,
127 cx: &impl HasSession,
128 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
129 ) -> Option<T> {
130 with_source_text_and_range(cx.sess().source_map(), self.into_range(), f)
131 }
132
133 fn map_range(
139 self,
140 cx: &impl HasSession,
141 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
142 ) -> Option<Range<BytePos>> {
143 map_range(cx.sess().source_map(), self.into_range(), f)
144 }
145
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}
217impl fmt::Debug for SourceText {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 self.as_str().fmt(f)
220 }
221}
222
223fn get_source_range(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
224 let start = sm.lookup_byte_offset(sp.start);
225 let end = sm.lookup_byte_offset(sp.end);
226 if !Arc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
227 return None;
228 }
229 sm.ensure_source_file_source_present(&start.sf);
230 let range = start.pos.to_usize()..end.pos.to_usize();
231 Some(SourceFileRange { sf: start.sf, range })
232}
233
234fn with_source_text<T>(sm: &SourceMap, sp: Range<BytePos>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
235 if let Some(src) = get_source_range(sm, sp)
236 && let Some(src) = src.as_str()
237 {
238 Some(f(src))
239 } else {
240 None
241 }
242}
243
244fn with_source_text_and_range<T>(
245 sm: &SourceMap,
246 sp: Range<BytePos>,
247 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
248) -> Option<T> {
249 if let Some(src) = get_source_range(sm, sp)
250 && let Some(text) = &src.sf.src
251 {
252 Some(f(text, src.range))
253 } else {
254 None
255 }
256}
257
258#[expect(clippy::cast_possible_truncation)]
259fn map_range(
260 sm: &SourceMap,
261 sp: Range<BytePos>,
262 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
263) -> Option<Range<BytePos>> {
264 if let Some(src) = get_source_range(sm, sp.clone())
265 && let Some(text) = &src.sf.src
266 && let Some(range) = f(&src.sf, text, src.range.clone())
267 {
268 debug_assert!(
269 range.start <= text.len() && range.end <= text.len(),
270 "Range `{range:?}` is outside the source file (file `{}`, length `{}`)",
271 src.sf.name.display(FileNameDisplayPreference::Local),
272 text.len(),
273 );
274 debug_assert!(range.start <= range.end, "Range `{range:?}` has overlapping bounds");
275 let dstart = (range.start as u32).wrapping_sub(src.range.start as u32);
276 let dend = (range.end as u32).wrapping_sub(src.range.start as u32);
277 Some(BytePos(sp.start.0.wrapping_add(dstart))..BytePos(sp.start.0.wrapping_add(dend)))
278 } else {
279 None
280 }
281}
282
283fn ends_with_line_comment_or_broken(text: &str) -> bool {
284 let Some(last) = tokenize(text, FrontmatterAllowed::No).last() else {
285 return false;
286 };
287 match last.kind {
288 TokenKind::LineComment { .. } | TokenKind::BlockComment { terminated: false, .. } => true,
292 TokenKind::Literal { kind, .. } => matches!(
293 kind,
294 LiteralKind::Byte { terminated: false }
295 | LiteralKind::ByteStr { terminated: false }
296 | LiteralKind::CStr { terminated: false }
297 | LiteralKind::Char { terminated: false }
298 | LiteralKind::RawByteStr { n_hashes: None }
299 | LiteralKind::RawCStr { n_hashes: None }
300 | LiteralKind::RawStr { n_hashes: None }
301 ),
302 _ => false,
303 }
304}
305
306fn with_leading_whitespace_inner(lines: &[RelativeBytePos], src: &str, range: Range<usize>) -> Option<usize> {
307 debug_assert!(lines.is_empty() || lines[0].to_u32() == 0);
308
309 let start = src.get(..range.start)?.trim_end();
310 let next_line = lines.partition_point(|&pos| pos.to_usize() <= start.len());
311 if let Some(line_end) = lines.get(next_line)
312 && line_end.to_usize() <= range.start
313 && let prev_start = lines.get(next_line - 1).map_or(0, |&x| x.to_usize())
314 && ends_with_line_comment_or_broken(&start[prev_start..])
315 && let next_line = lines.partition_point(|&pos| pos.to_usize() < range.end)
316 && let next_start = lines.get(next_line).map_or(src.len(), |&x| x.to_usize())
317 && tokenize(src.get(range.end..next_start)?, FrontmatterAllowed::No)
318 .any(|t| !matches!(t.kind, TokenKind::Whitespace))
319 {
320 Some(range.start)
321 } else {
322 Some(start.len())
323 }
324}
325
326fn with_leading_whitespace(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
327 map_range(sm, sp.clone(), |sf, src, range| {
328 Some(with_leading_whitespace_inner(sf.lines(), src, range.clone())?..range.end)
329 })
330 .unwrap_or(sp)
331}
332
333fn trim_start(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
334 map_range(sm, sp.clone(), |_, src, range| {
335 let src = src.get(range.clone())?;
336 Some(range.start + (src.len() - src.trim_start().len())..range.end)
337 })
338 .unwrap_or(sp)
339}
340
341pub struct SourceFileRange {
342 pub sf: Arc<SourceFile>,
343 pub range: Range<usize>,
344}
345impl SourceFileRange {
346 pub fn as_str(&self) -> Option<&str> {
349 (self.sf.src.as_ref().map(|src| src.as_str()))
350 .or_else(|| self.sf.external_src.get()?.get_source())
351 .and_then(|x| x.get(self.range.clone()))
352 }
353}
354
355pub fn expr_block(
357 sess: &impl HasSession,
358 expr: &Expr<'_>,
359 outer: SyntaxContext,
360 default: &str,
361 indent_relative_to: Option<Span>,
362 app: &mut Applicability,
363) -> String {
364 let (code, from_macro) = snippet_block_with_context(sess, expr.span, outer, default, indent_relative_to, app);
365 if !from_macro
366 && let ExprKind::Block(block, None) = expr.kind
367 && block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
368 {
369 code
370 } else {
371 format!("{{ {code} }}")
376 }
377}
378
379pub fn first_line_of_span(sess: &impl HasSession, span: Span) -> Span {
390 first_char_in_first_line(sess, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
391}
392
393fn first_char_in_first_line(sess: &impl HasSession, span: Span) -> Option<BytePos> {
394 let line_span = line_span(sess, span);
395 snippet_opt(sess, line_span).and_then(|snip| {
396 snip.find(|c: char| !c.is_whitespace())
397 .map(|pos| line_span.lo() + BytePos::from_usize(pos))
398 })
399}
400
401fn line_span(sess: &impl HasSession, span: Span) -> Span {
411 let span = original_sp(span, DUMMY_SP);
412 let SourceFileAndLine { sf, line } = sess.sess().source_map().lookup_line(span.lo()).unwrap();
413 let line_start = sf.lines()[line];
414 let line_start = sf.absolute_position(line_start);
415 span.with_lo(line_start)
416}
417
418pub fn indent_of(sess: &impl HasSession, span: Span) -> Option<usize> {
427 snippet_opt(sess, line_span(sess, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
428}
429
430pub fn snippet_indent(sess: &impl HasSession, span: Span) -> Option<String> {
432 snippet_opt(sess, line_span(sess, span)).map(|mut s| {
433 let len = s.len() - s.trim_start().len();
434 s.truncate(len);
435 s
436 })
437}
438
439pub fn is_present_in_source(sess: &impl HasSession, span: Span) -> bool {
445 if let Some(snippet) = snippet_opt(sess, span)
446 && snippet.is_empty()
447 {
448 return false;
449 }
450 true
451}
452
453pub fn position_before_rarrow(s: &str) -> Option<usize> {
465 s.rfind("->").map(|rpos| {
466 let mut rpos = rpos;
467 let chars: Vec<char> = s.chars().collect();
468 while rpos > 1 {
469 if let Some(c) = chars.get(rpos - 1)
470 && c.is_whitespace()
471 {
472 rpos -= 1;
473 continue;
474 }
475 break;
476 }
477 rpos
478 })
479}
480
481pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
483 let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');
484 let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
485 reindent_multiline_inner(&s_tab, ignore_first, indent, ' ')
486}
487
488fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
489 let x = s
490 .lines()
491 .skip(usize::from(ignore_first))
492 .filter_map(|l| {
493 if l.is_empty() {
494 None
495 } else {
496 Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
498 }
499 })
500 .min()
501 .unwrap_or(0);
502 let indent = indent.unwrap_or(0);
503 s.lines()
504 .enumerate()
505 .map(|(i, l)| {
506 if (ignore_first && i == 0) || l.is_empty() {
507 l.to_owned()
508 } else if x > indent {
509 l.split_at(x - indent).1.to_owned()
510 } else {
511 " ".repeat(indent - x) + l
512 }
513 })
514 .collect::<Vec<String>>()
515 .join("\n")
516}
517
518pub fn snippet<'a>(sess: &impl HasSession, span: Span, default: &'a str) -> Cow<'a, str> {
536 snippet_opt(sess, span).map_or_else(|| Cow::Borrowed(default), From::from)
537}
538
539pub fn snippet_with_applicability<'a>(
546 sess: &impl HasSession,
547 span: Span,
548 default: &'a str,
549 applicability: &mut Applicability,
550) -> Cow<'a, str> {
551 snippet_with_applicability_sess(sess.sess(), span, default, applicability)
552}
553
554fn snippet_with_applicability_sess<'a>(
555 sess: &Session,
556 span: Span,
557 default: &'a str,
558 applicability: &mut Applicability,
559) -> Cow<'a, str> {
560 if *applicability != Applicability::Unspecified && span.from_expansion() {
561 *applicability = Applicability::MaybeIncorrect;
562 }
563 snippet_opt(sess, span).map_or_else(
564 || {
565 if *applicability == Applicability::MachineApplicable {
566 *applicability = Applicability::HasPlaceholders;
567 }
568 Cow::Borrowed(default)
569 },
570 From::from,
571 )
572}
573
574pub fn snippet_opt(sess: &impl HasSession, span: Span) -> Option<String> {
576 sess.sess().source_map().span_to_snippet(span).ok()
577}
578
579pub fn snippet_block(sess: &impl HasSession, span: Span, default: &str, indent_relative_to: Option<Span>) -> String {
614 let snip = snippet(sess, span, default);
615 let indent = indent_relative_to.and_then(|s| indent_of(sess, s));
616 reindent_multiline(&snip, true, indent)
617}
618
619pub fn snippet_block_with_applicability(
622 sess: &impl HasSession,
623 span: Span,
624 default: &str,
625 indent_relative_to: Option<Span>,
626 applicability: &mut Applicability,
627) -> String {
628 let snip = snippet_with_applicability(sess, span, default, applicability);
629 let indent = indent_relative_to.and_then(|s| indent_of(sess, s));
630 reindent_multiline(&snip, true, indent)
631}
632
633pub fn snippet_block_with_context(
634 sess: &impl HasSession,
635 span: Span,
636 outer: SyntaxContext,
637 default: &str,
638 indent_relative_to: Option<Span>,
639 app: &mut Applicability,
640) -> (String, bool) {
641 let (snip, from_macro) = snippet_with_context(sess, span, outer, default, app);
642 let indent = indent_relative_to.and_then(|s| indent_of(sess, s));
643 (reindent_multiline(&snip, true, indent), from_macro)
644}
645
646pub fn snippet_with_context<'a>(
657 sess: &impl HasSession,
658 span: Span,
659 outer: SyntaxContext,
660 default: &'a str,
661 applicability: &mut Applicability,
662) -> (Cow<'a, str>, bool) {
663 snippet_with_context_sess(sess.sess(), span, outer, default, applicability)
664}
665
666fn snippet_with_context_sess<'a>(
667 sess: &Session,
668 span: Span,
669 outer: SyntaxContext,
670 default: &'a str,
671 applicability: &mut Applicability,
672) -> (Cow<'a, str>, bool) {
673 let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
674 || {
675 if *applicability != Applicability::Unspecified {
677 *applicability = Applicability::MaybeIncorrect;
678 }
679 (span, false)
681 },
682 |outer_span| (outer_span, span.ctxt() != outer),
683 );
684
685 (
686 snippet_with_applicability_sess(sess, span, default, applicability),
687 is_macro_call,
688 )
689}
690
691pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
719 let outer_span = hygiene::walk_chain(span, outer);
720 (outer_span.ctxt() == outer).then_some(outer_span)
721}
722
723pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
725 let data = span.data();
726 let sf: &_ = &sm.lookup_source_file(data.lo);
727 let Some(src) = sf.src.as_deref() else {
728 return span;
729 };
730 let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
731 return span;
732 };
733 let trim_start = snip.len() - snip.trim_start().len();
734 let trim_end = snip.len() - snip.trim_end().len();
735 SpanData {
736 lo: data.lo + BytePos::from_usize(trim_start),
737 hi: data.hi - BytePos::from_usize(trim_end),
738 ctxt: data.ctxt,
739 parent: data.parent,
740 }
741 .span()
742}
743
744pub fn expand_past_previous_comma(sess: &impl HasSession, span: Span) -> Span {
750 let extended = sess.sess().source_map().span_extend_to_prev_char(span, ',', true);
751 extended.with_lo(extended.lo() - BytePos(1))
752}
753
754pub fn str_literal_to_char_literal(
757 sess: &impl HasSession,
758 expr: &Expr<'_>,
759 applicability: &mut Applicability,
760 ascii_only: bool,
761) -> Option<String> {
762 if let ExprKind::Lit(lit) = &expr.kind
763 && let LitKind::Str(r, style) = lit.node
764 && let string = r.as_str()
765 && let len = if ascii_only {
766 string.len()
767 } else {
768 string.chars().count()
769 }
770 && len == 1
771 {
772 let snip = snippet_with_applicability(sess, expr.span, string, applicability);
773 let ch = if let StrStyle::Raw(nhash) = style {
774 let nhash = nhash as usize;
775 &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
777 } else {
778 &snip[1..(snip.len() - 1)]
780 };
781
782 let hint = format!(
783 "'{}'",
784 match ch {
785 "'" => "\\'",
786 r"\" => "\\\\",
787 "\\\"" => "\"", _ => ch,
789 }
790 );
791
792 Some(hint)
793 } else {
794 None
795 }
796}
797
798#[cfg(test)]
799mod test {
800 use super::reindent_multiline;
801
802 #[test]
803 fn test_reindent_multiline_single_line() {
804 assert_eq!("", reindent_multiline("", false, None));
805 assert_eq!("...", reindent_multiline("...", false, None));
806 assert_eq!("...", reindent_multiline(" ...", false, None));
807 assert_eq!("...", reindent_multiline("\t...", false, None));
808 assert_eq!("...", reindent_multiline("\t\t...", false, None));
809 }
810
811 #[test]
812 #[rustfmt::skip]
813 fn test_reindent_multiline_block() {
814 assert_eq!("\
815 if x {
816 y
817 } else {
818 z
819 }", reindent_multiline(" if x {
820 y
821 } else {
822 z
823 }", false, None));
824 assert_eq!("\
825 if x {
826 \ty
827 } else {
828 \tz
829 }", reindent_multiline(" if x {
830 \ty
831 } else {
832 \tz
833 }", false, None));
834 }
835
836 #[test]
837 #[rustfmt::skip]
838 fn test_reindent_multiline_empty_line() {
839 assert_eq!("\
840 if x {
841 y
842
843 } else {
844 z
845 }", reindent_multiline(" if x {
846 y
847
848 } else {
849 z
850 }", false, None));
851 }
852
853 #[test]
854 #[rustfmt::skip]
855 fn test_reindent_multiline_lines_deeper() {
856 assert_eq!("\
857 if x {
858 y
859 } else {
860 z
861 }", reindent_multiline("\
862 if x {
863 y
864 } else {
865 z
866 }", true, Some(8)));
867 }
868}