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 #[allow(rustdoc::invalid_rust_codeblocks, reason = "The codeblock is intentionally broken")]
147 fn with_leading_whitespace(self, cx: &impl HasSession) -> Range<BytePos> {
161 with_leading_whitespace(cx.sess().source_map(), self.into_range())
162 }
163
164 fn trim_start(self, cx: &impl HasSession) -> Range<BytePos> {
166 trim_start(cx.sess().source_map(), self.into_range())
167 }
168}
169impl<T: SpanRange> SpanRangeExt for T {}
170
171pub struct SourceText(SourceFileRange);
173impl SourceText {
174 pub fn new(text: SourceFileRange) -> Option<Self> {
176 if text.as_str().is_some() {
177 Some(Self(text))
178 } else {
179 None
180 }
181 }
182
183 pub fn as_str(&self) -> &str {
185 self.0.as_str().unwrap()
186 }
187
188 pub fn to_owned(&self) -> String {
190 self.as_str().to_owned()
191 }
192}
193impl Deref for SourceText {
194 type Target = str;
195 fn deref(&self) -> &Self::Target {
196 self.as_str()
197 }
198}
199impl AsRef<str> for SourceText {
200 fn as_ref(&self) -> &str {
201 self.as_str()
202 }
203}
204impl<T> Index<T> for SourceText
205where
206 str: Index<T>,
207{
208 type Output = <str as Index<T>>::Output;
209 fn index(&self, idx: T) -> &Self::Output {
210 &self.as_str()[idx]
211 }
212}
213impl fmt::Display for SourceText {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 self.as_str().fmt(f)
216 }
217}
218impl fmt::Debug for SourceText {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 self.as_str().fmt(f)
221 }
222}
223
224fn get_source_range(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
225 let start = sm.lookup_byte_offset(sp.start);
226 let end = sm.lookup_byte_offset(sp.end);
227 if !Arc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
228 return None;
229 }
230 sm.ensure_source_file_source_present(&start.sf);
231 let range = start.pos.to_usize()..end.pos.to_usize();
232 Some(SourceFileRange { sf: start.sf, range })
233}
234
235fn with_source_text<T>(sm: &SourceMap, sp: Range<BytePos>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option<T> {
236 if let Some(src) = get_source_range(sm, sp)
237 && let Some(src) = src.as_str()
238 {
239 Some(f(src))
240 } else {
241 None
242 }
243}
244
245fn with_source_text_and_range<T>(
246 sm: &SourceMap,
247 sp: Range<BytePos>,
248 f: impl for<'a> FnOnce(&'a str, Range<usize>) -> T,
249) -> Option<T> {
250 if let Some(src) = get_source_range(sm, sp)
251 && let Some(text) = &src.sf.src
252 {
253 Some(f(text, src.range))
254 } else {
255 None
256 }
257}
258
259#[expect(clippy::cast_possible_truncation)]
260fn map_range(
261 sm: &SourceMap,
262 sp: Range<BytePos>,
263 f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range<usize>) -> Option<Range<usize>>,
264) -> Option<Range<BytePos>> {
265 if let Some(src) = get_source_range(sm, sp.clone())
266 && let Some(text) = &src.sf.src
267 && let Some(range) = f(&src.sf, text, src.range.clone())
268 {
269 debug_assert!(
270 range.start <= text.len() && range.end <= text.len(),
271 "Range `{range:?}` is outside the source file (file `{}`, length `{}`)",
272 src.sf.name.display(FileNameDisplayPreference::Local),
273 text.len(),
274 );
275 debug_assert!(range.start <= range.end, "Range `{range:?}` has overlapping bounds");
276 let dstart = (range.start as u32).wrapping_sub(src.range.start as u32);
277 let dend = (range.end as u32).wrapping_sub(src.range.start as u32);
278 Some(BytePos(sp.start.0.wrapping_add(dstart))..BytePos(sp.start.0.wrapping_add(dend)))
279 } else {
280 None
281 }
282}
283
284fn ends_with_line_comment_or_broken(text: &str) -> bool {
285 let Some(last) = tokenize(text, FrontmatterAllowed::No).last() else {
286 return false;
287 };
288 match last.kind {
289 TokenKind::LineComment { .. } | TokenKind::BlockComment { terminated: false, .. } => true,
293 TokenKind::Literal { kind, .. } => matches!(
294 kind,
295 LiteralKind::Byte { terminated: false }
296 | LiteralKind::ByteStr { terminated: false }
297 | LiteralKind::CStr { terminated: false }
298 | LiteralKind::Char { terminated: false }
299 | LiteralKind::RawByteStr { n_hashes: None }
300 | LiteralKind::RawCStr { n_hashes: None }
301 | LiteralKind::RawStr { n_hashes: None }
302 ),
303 _ => false,
304 }
305}
306
307fn with_leading_whitespace_inner(lines: &[RelativeBytePos], src: &str, range: Range<usize>) -> Option<usize> {
308 debug_assert!(lines.is_empty() || lines[0].to_u32() == 0);
309
310 let start = src.get(..range.start)?.trim_end();
311 let next_line = lines.partition_point(|&pos| pos.to_usize() <= start.len());
312 if let Some(line_end) = lines.get(next_line)
313 && line_end.to_usize() <= range.start
314 && let prev_start = lines.get(next_line - 1).map_or(0, |&x| x.to_usize())
315 && ends_with_line_comment_or_broken(&start[prev_start..])
316 && let next_line = lines.partition_point(|&pos| pos.to_usize() < range.end)
317 && let next_start = lines.get(next_line).map_or(src.len(), |&x| x.to_usize())
318 && tokenize(src.get(range.end..next_start)?, FrontmatterAllowed::No)
319 .any(|t| !matches!(t.kind, TokenKind::Whitespace))
320 {
321 Some(range.start)
322 } else {
323 Some(start.len())
324 }
325}
326
327fn with_leading_whitespace(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
328 map_range(sm, sp.clone(), |sf, src, range| {
329 Some(with_leading_whitespace_inner(sf.lines(), src, range.clone())?..range.end)
330 })
331 .unwrap_or(sp)
332}
333
334fn trim_start(sm: &SourceMap, sp: Range<BytePos>) -> Range<BytePos> {
335 map_range(sm, sp.clone(), |_, src, range| {
336 let src = src.get(range.clone())?;
337 Some(range.start + (src.len() - src.trim_start().len())..range.end)
338 })
339 .unwrap_or(sp)
340}
341
342pub struct SourceFileRange {
343 pub sf: Arc<SourceFile>,
344 pub range: Range<usize>,
345}
346impl SourceFileRange {
347 pub fn as_str(&self) -> Option<&str> {
350 (self.sf.src.as_ref().map(|src| src.as_str()))
351 .or_else(|| self.sf.external_src.get()?.get_source())
352 .and_then(|x| x.get(self.range.clone()))
353 }
354}
355
356pub fn expr_block(
358 sess: &impl HasSession,
359 expr: &Expr<'_>,
360 outer: SyntaxContext,
361 default: &str,
362 indent_relative_to: Option<Span>,
363 app: &mut Applicability,
364) -> String {
365 let (code, from_macro) = snippet_block_with_context(sess, expr.span, outer, default, indent_relative_to, app);
366 if !from_macro
367 && let ExprKind::Block(block, None) = expr.kind
368 && block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
369 {
370 code
371 } else {
372 format!("{{ {code} }}")
377 }
378}
379
380pub fn first_line_of_span(sess: &impl HasSession, span: Span) -> Span {
391 first_char_in_first_line(sess, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
392}
393
394fn first_char_in_first_line(sess: &impl HasSession, span: Span) -> Option<BytePos> {
395 let line_span = line_span(sess, span);
396 snippet_opt(sess, line_span).and_then(|snip| {
397 snip.find(|c: char| !c.is_whitespace())
398 .map(|pos| line_span.lo() + BytePos::from_usize(pos))
399 })
400}
401
402fn line_span(sess: &impl HasSession, span: Span) -> Span {
412 let span = original_sp(span, DUMMY_SP);
413 let SourceFileAndLine { sf, line } = sess.sess().source_map().lookup_line(span.lo()).unwrap();
414 let line_start = sf.lines()[line];
415 let line_start = sf.absolute_position(line_start);
416 span.with_lo(line_start)
417}
418
419pub fn indent_of(sess: &impl HasSession, span: Span) -> Option<usize> {
428 snippet_opt(sess, line_span(sess, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
429}
430
431pub fn snippet_indent(sess: &impl HasSession, span: Span) -> Option<String> {
433 snippet_opt(sess, line_span(sess, span)).map(|mut s| {
434 let len = s.len() - s.trim_start().len();
435 s.truncate(len);
436 s
437 })
438}
439
440pub fn is_present_in_source(sess: &impl HasSession, span: Span) -> bool {
446 if let Some(snippet) = snippet_opt(sess, span)
447 && snippet.is_empty()
448 {
449 return false;
450 }
451 true
452}
453
454pub fn position_before_rarrow(s: &str) -> Option<usize> {
466 s.rfind("->").map(|rpos| {
467 let mut rpos = rpos;
468 let chars: Vec<char> = s.chars().collect();
469 while rpos > 1 {
470 if let Some(c) = chars.get(rpos - 1)
471 && c.is_whitespace()
472 {
473 rpos -= 1;
474 continue;
475 }
476 break;
477 }
478 rpos
479 })
480}
481
482pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
484 let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');
485 let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
486 reindent_multiline_inner(&s_tab, ignore_first, indent, ' ')
487}
488
489fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
490 let x = s
491 .lines()
492 .skip(usize::from(ignore_first))
493 .filter_map(|l| {
494 if l.is_empty() {
495 None
496 } else {
497 Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
499 }
500 })
501 .min()
502 .unwrap_or(0);
503 let indent = indent.unwrap_or(0);
504 s.lines()
505 .enumerate()
506 .map(|(i, l)| {
507 if (ignore_first && i == 0) || l.is_empty() {
508 l.to_owned()
509 } else if x > indent {
510 l.split_at(x - indent).1.to_owned()
511 } else {
512 " ".repeat(indent - x) + l
513 }
514 })
515 .collect::<Vec<String>>()
516 .join("\n")
517}
518
519pub fn snippet<'a>(sess: &impl HasSession, span: Span, default: &'a str) -> Cow<'a, str> {
537 snippet_opt(sess, span).map_or_else(|| Cow::Borrowed(default), From::from)
538}
539
540pub fn snippet_with_applicability<'a>(
547 sess: &impl HasSession,
548 span: Span,
549 default: &'a str,
550 applicability: &mut Applicability,
551) -> Cow<'a, str> {
552 snippet_with_applicability_sess(sess.sess(), span, default, applicability)
553}
554
555fn snippet_with_applicability_sess<'a>(
556 sess: &Session,
557 span: Span,
558 default: &'a str,
559 applicability: &mut Applicability,
560) -> Cow<'a, str> {
561 if *applicability != Applicability::Unspecified && span.from_expansion() {
562 *applicability = Applicability::MaybeIncorrect;
563 }
564 snippet_opt(sess, span).map_or_else(
565 || {
566 if *applicability == Applicability::MachineApplicable {
567 *applicability = Applicability::HasPlaceholders;
568 }
569 Cow::Borrowed(default)
570 },
571 From::from,
572 )
573}
574
575pub fn snippet_opt(sess: &impl HasSession, span: Span) -> Option<String> {
577 sess.sess().source_map().span_to_snippet(span).ok()
578}
579
580pub fn snippet_block(sess: &impl HasSession, span: Span, default: &str, indent_relative_to: Option<Span>) -> String {
615 let snip = snippet(sess, span, default);
616 let indent = indent_relative_to.and_then(|s| indent_of(sess, s));
617 reindent_multiline(&snip, true, indent)
618}
619
620pub fn snippet_block_with_applicability(
623 sess: &impl HasSession,
624 span: Span,
625 default: &str,
626 indent_relative_to: Option<Span>,
627 applicability: &mut Applicability,
628) -> String {
629 let snip = snippet_with_applicability(sess, span, default, applicability);
630 let indent = indent_relative_to.and_then(|s| indent_of(sess, s));
631 reindent_multiline(&snip, true, indent)
632}
633
634pub fn snippet_block_with_context(
635 sess: &impl HasSession,
636 span: Span,
637 outer: SyntaxContext,
638 default: &str,
639 indent_relative_to: Option<Span>,
640 app: &mut Applicability,
641) -> (String, bool) {
642 let (snip, from_macro) = snippet_with_context(sess, span, outer, default, app);
643 let indent = indent_relative_to.and_then(|s| indent_of(sess, s));
644 (reindent_multiline(&snip, true, indent), from_macro)
645}
646
647pub fn snippet_with_context<'a>(
658 sess: &impl HasSession,
659 span: Span,
660 outer: SyntaxContext,
661 default: &'a str,
662 applicability: &mut Applicability,
663) -> (Cow<'a, str>, bool) {
664 snippet_with_context_sess(sess.sess(), span, outer, default, applicability)
665}
666
667fn snippet_with_context_sess<'a>(
668 sess: &Session,
669 span: Span,
670 outer: SyntaxContext,
671 default: &'a str,
672 applicability: &mut Applicability,
673) -> (Cow<'a, str>, bool) {
674 let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
675 || {
676 if *applicability != Applicability::Unspecified {
678 *applicability = Applicability::MaybeIncorrect;
679 }
680 (span, false)
682 },
683 |outer_span| (outer_span, span.ctxt() != outer),
684 );
685
686 (
687 snippet_with_applicability_sess(sess, span, default, applicability),
688 is_macro_call,
689 )
690}
691
692pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
720 let outer_span = hygiene::walk_chain(span, outer);
721 (outer_span.ctxt() == outer).then_some(outer_span)
722}
723
724pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
726 let data = span.data();
727 let sf: &_ = &sm.lookup_source_file(data.lo);
728 let Some(src) = sf.src.as_deref() else {
729 return span;
730 };
731 let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
732 return span;
733 };
734 let trim_start = snip.len() - snip.trim_start().len();
735 let trim_end = snip.len() - snip.trim_end().len();
736 SpanData {
737 lo: data.lo + BytePos::from_usize(trim_start),
738 hi: data.hi - BytePos::from_usize(trim_end),
739 ctxt: data.ctxt,
740 parent: data.parent,
741 }
742 .span()
743}
744
745pub fn expand_past_previous_comma(sess: &impl HasSession, span: Span) -> Span {
751 let extended = sess.sess().source_map().span_extend_to_prev_char(span, ',', true);
752 extended.with_lo(extended.lo() - BytePos(1))
753}
754
755pub fn str_literal_to_char_literal(
758 sess: &impl HasSession,
759 expr: &Expr<'_>,
760 applicability: &mut Applicability,
761 ascii_only: bool,
762) -> Option<String> {
763 if let ExprKind::Lit(lit) = &expr.kind
764 && let LitKind::Str(r, style) = lit.node
765 && let string = r.as_str()
766 && let len = if ascii_only {
767 string.len()
768 } else {
769 string.chars().count()
770 }
771 && len == 1
772 {
773 let snip = snippet_with_applicability(sess, expr.span, string, applicability);
774 let ch = if let StrStyle::Raw(nhash) = style {
775 let nhash = nhash as usize;
776 &snip[(nhash + 2)..(snip.len() - 1 - nhash)]
778 } else {
779 &snip[1..(snip.len() - 1)]
781 };
782
783 let hint = format!(
784 "'{}'",
785 match ch {
786 "'" => "\\'",
787 r"\" => "\\\\",
788 "\\\"" => "\"", _ => ch,
790 }
791 );
792
793 Some(hint)
794 } else {
795 None
796 }
797}
798
799#[cfg(test)]
800mod test {
801 use super::reindent_multiline;
802
803 #[test]
804 fn test_reindent_multiline_single_line() {
805 assert_eq!("", reindent_multiline("", false, None));
806 assert_eq!("...", reindent_multiline("...", false, None));
807 assert_eq!("...", reindent_multiline(" ...", false, None));
808 assert_eq!("...", reindent_multiline("\t...", false, None));
809 assert_eq!("...", reindent_multiline("\t\t...", false, None));
810 }
811
812 #[test]
813 #[rustfmt::skip]
814 fn test_reindent_multiline_block() {
815 assert_eq!("\
816 if x {
817 y
818 } else {
819 z
820 }", reindent_multiline(" if x {
821 y
822 } else {
823 z
824 }", false, None));
825 assert_eq!("\
826 if x {
827 \ty
828 } else {
829 \tz
830 }", reindent_multiline(" if x {
831 \ty
832 } else {
833 \tz
834 }", false, None));
835 }
836
837 #[test]
838 #[rustfmt::skip]
839 fn test_reindent_multiline_empty_line() {
840 assert_eq!("\
841 if x {
842 y
843
844 } else {
845 z
846 }", reindent_multiline(" if x {
847 y
848
849 } else {
850 z
851 }", false, None));
852 }
853
854 #[test]
855 #[rustfmt::skip]
856 fn test_reindent_multiline_lines_deeper() {
857 assert_eq!("\
858 if x {
859 y
860 } else {
861 z
862 }", reindent_multiline("\
863 if x {
864 y
865 } else {
866 z
867 }", true, Some(8)));
868 }
869}