1 //! Utils for extracting, inspecting or transforming source code
2
3 #![allow(clippy::module_name_repetitions)]
4
5 use rustc_data_structures::sync::Lrc;
6 use rustc_errors::Applicability;
7 use rustc_hir::{BlockCheckMode, Expr, ExprKind, UnsafeSource};
8 use rustc_lint::{LateContext, LintContext};
9 use rustc_session::Session;
10 use rustc_span::source_map::{original_sp, SourceMap};
11 use rustc_span::{hygiene, SourceFile};
12 use rustc_span::{BytePos, Pos, Span, SpanData, SyntaxContext, DUMMY_SP};
13 use std::borrow::Cow;
14 use std::ops::Range;
15
16 /// A type which can be converted to the range portion of a `Span`.
17 pub trait SpanRange {
into_range(self) -> Range<BytePos>18 fn into_range(self) -> Range<BytePos>;
19 }
20 impl SpanRange for Span {
into_range(self) -> Range<BytePos>21 fn into_range(self) -> Range<BytePos> {
22 let data = self.data();
23 data.lo..data.hi
24 }
25 }
26 impl SpanRange for SpanData {
into_range(self) -> Range<BytePos>27 fn into_range(self) -> Range<BytePos> {
28 self.lo..self.hi
29 }
30 }
31 impl SpanRange for Range<BytePos> {
into_range(self) -> Range<BytePos>32 fn into_range(self) -> Range<BytePos> {
33 self
34 }
35 }
36
37 pub struct SourceFileRange {
38 pub sf: Lrc<SourceFile>,
39 pub range: Range<usize>,
40 }
41 impl SourceFileRange {
42 /// Attempts to get the text from the source file. This can fail if the source text isn't
43 /// loaded.
as_str(&self) -> Option<&str>44 pub fn as_str(&self) -> Option<&str> {
45 self.sf.src.as_ref().and_then(|x| x.get(self.range.clone()))
46 }
47 }
48
49 /// Gets the source file, and range in the file, of the given span. Returns `None` if the span
50 /// extends through multiple files, or is malformed.
get_source_text(cx: &impl LintContext, sp: impl SpanRange) -> Option<SourceFileRange>51 pub fn get_source_text(cx: &impl LintContext, sp: impl SpanRange) -> Option<SourceFileRange> {
52 fn f(sm: &SourceMap, sp: Range<BytePos>) -> Option<SourceFileRange> {
53 let start = sm.lookup_byte_offset(sp.start);
54 let end = sm.lookup_byte_offset(sp.end);
55 if !Lrc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos {
56 return None;
57 }
58 let range = start.pos.to_usize()..end.pos.to_usize();
59 Some(SourceFileRange { sf: start.sf, range })
60 }
61 f(cx.sess().source_map(), sp.into_range())
62 }
63
64 /// Like `snippet_block`, but add braces if the expr is not an `ExprKind::Block`.
expr_block<T: LintContext>( cx: &T, expr: &Expr<'_>, outer: SyntaxContext, default: &str, indent_relative_to: Option<Span>, app: &mut Applicability, ) -> String65 pub fn expr_block<T: LintContext>(
66 cx: &T,
67 expr: &Expr<'_>,
68 outer: SyntaxContext,
69 default: &str,
70 indent_relative_to: Option<Span>,
71 app: &mut Applicability,
72 ) -> String {
73 let (code, from_macro) = snippet_block_with_context(cx, expr.span, outer, default, indent_relative_to, app);
74 if !from_macro &&
75 let ExprKind::Block(block, _) = expr.kind &&
76 block.rules != BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
77 {
78 format!("{code}")
79 } else {
80 // FIXME: add extra indent for the unsafe blocks:
81 // original code: unsafe { ... }
82 // result code: { unsafe { ... } }
83 // desired code: {\n unsafe { ... }\n}
84 format!("{{ {code} }}")
85 }
86 }
87
88 /// Returns a new Span that extends the original Span to the first non-whitespace char of the first
89 /// line.
90 ///
91 /// ```rust,ignore
92 /// let x = ();
93 /// // ^^
94 /// // will be converted to
95 /// let x = ();
96 /// // ^^^^^^^^^^
97 /// ```
first_line_of_span<T: LintContext>(cx: &T, span: Span) -> Span98 pub fn first_line_of_span<T: LintContext>(cx: &T, span: Span) -> Span {
99 first_char_in_first_line(cx, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
100 }
101
first_char_in_first_line<T: LintContext>(cx: &T, span: Span) -> Option<BytePos>102 fn first_char_in_first_line<T: LintContext>(cx: &T, span: Span) -> Option<BytePos> {
103 let line_span = line_span(cx, span);
104 snippet_opt(cx, line_span).and_then(|snip| {
105 snip.find(|c: char| !c.is_whitespace())
106 .map(|pos| line_span.lo() + BytePos::from_usize(pos))
107 })
108 }
109
110 /// Extends the span to the beginning of the spans line, incl. whitespaces.
111 ///
112 /// ```rust
113 /// let x = ();
114 /// // ^^
115 /// // will be converted to
116 /// let x = ();
117 /// // ^^^^^^^^^^^^^^
118 /// ```
line_span<T: LintContext>(cx: &T, span: Span) -> Span119 fn line_span<T: LintContext>(cx: &T, span: Span) -> Span {
120 let span = original_sp(span, DUMMY_SP);
121 let source_map_and_line = cx.sess().source_map().lookup_line(span.lo()).unwrap();
122 let line_no = source_map_and_line.line;
123 let line_start = source_map_and_line.sf.lines(|lines| lines[line_no]);
124 span.with_lo(line_start)
125 }
126
127 /// Returns the indentation of the line of a span
128 ///
129 /// ```rust,ignore
130 /// let x = ();
131 /// // ^^ -- will return 0
132 /// let x = ();
133 /// // ^^ -- will return 4
134 /// ```
indent_of<T: LintContext>(cx: &T, span: Span) -> Option<usize>135 pub fn indent_of<T: LintContext>(cx: &T, span: Span) -> Option<usize> {
136 snippet_opt(cx, line_span(cx, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
137 }
138
139 /// Gets a snippet of the indentation of the line of a span
snippet_indent<T: LintContext>(cx: &T, span: Span) -> Option<String>140 pub fn snippet_indent<T: LintContext>(cx: &T, span: Span) -> Option<String> {
141 snippet_opt(cx, line_span(cx, span)).map(|mut s| {
142 let len = s.len() - s.trim_start().len();
143 s.truncate(len);
144 s
145 })
146 }
147
148 // If the snippet is empty, it's an attribute that was inserted during macro
149 // expansion and we want to ignore those, because they could come from external
150 // sources that the user has no control over.
151 // For some reason these attributes don't have any expansion info on them, so
152 // we have to check it this way until there is a better way.
is_present_in_source<T: LintContext>(cx: &T, span: Span) -> bool153 pub fn is_present_in_source<T: LintContext>(cx: &T, span: Span) -> bool {
154 if let Some(snippet) = snippet_opt(cx, span) {
155 if snippet.is_empty() {
156 return false;
157 }
158 }
159 true
160 }
161
162 /// Returns the position just before rarrow
163 ///
164 /// ```rust,ignore
165 /// fn into(self) -> () {}
166 /// ^
167 /// // in case of unformatted code
168 /// fn into2(self)-> () {}
169 /// ^
170 /// fn into3(self) -> () {}
171 /// ^
172 /// ```
position_before_rarrow(s: &str) -> Option<usize>173 pub fn position_before_rarrow(s: &str) -> Option<usize> {
174 s.rfind("->").map(|rpos| {
175 let mut rpos = rpos;
176 let chars: Vec<char> = s.chars().collect();
177 while rpos > 1 {
178 if let Some(c) = chars.get(rpos - 1) {
179 if c.is_whitespace() {
180 rpos -= 1;
181 continue;
182 }
183 }
184 break;
185 }
186 rpos
187 })
188 }
189
190 /// Reindent a multiline string with possibility of ignoring the first line.
191 #[expect(clippy::needless_pass_by_value)]
reindent_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str>192 pub fn reindent_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str> {
193 let s_space = reindent_multiline_inner(&s, ignore_first, indent, ' ');
194 let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
195 reindent_multiline_inner(&s_tab, ignore_first, indent, ' ').into()
196 }
197
reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String198 fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
199 let x = s
200 .lines()
201 .skip(usize::from(ignore_first))
202 .filter_map(|l| {
203 if l.is_empty() {
204 None
205 } else {
206 // ignore empty lines
207 Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
208 }
209 })
210 .min()
211 .unwrap_or(0);
212 let indent = indent.unwrap_or(0);
213 s.lines()
214 .enumerate()
215 .map(|(i, l)| {
216 if (ignore_first && i == 0) || l.is_empty() {
217 l.to_owned()
218 } else if x > indent {
219 l.split_at(x - indent).1.to_owned()
220 } else {
221 " ".repeat(indent - x) + l
222 }
223 })
224 .collect::<Vec<String>>()
225 .join("\n")
226 }
227
228 /// Converts a span to a code snippet if available, otherwise returns the default.
229 ///
230 /// This is useful if you want to provide suggestions for your lint or more generally, if you want
231 /// to convert a given `Span` to a `str`. To create suggestions consider using
232 /// [`snippet_with_applicability`] to ensure that the applicability stays correct.
233 ///
234 /// # Example
235 /// ```rust,ignore
236 /// // Given two spans one for `value` and one for the `init` expression.
237 /// let value = Vec::new();
238 /// // ^^^^^ ^^^^^^^^^^
239 /// // span1 span2
240 ///
241 /// // The snipped call would return the corresponding code snippet
242 /// snippet(cx, span1, "..") // -> "value"
243 /// snippet(cx, span2, "..") // -> "Vec::new()"
244 /// ```
snippet<'a, T: LintContext>(cx: &T, span: Span, default: &'a str) -> Cow<'a, str>245 pub fn snippet<'a, T: LintContext>(cx: &T, span: Span, default: &'a str) -> Cow<'a, str> {
246 snippet_opt(cx, span).map_or_else(|| Cow::Borrowed(default), From::from)
247 }
248
249 /// Same as [`snippet`], but it adapts the applicability level by following rules:
250 ///
251 /// - Applicability level `Unspecified` will never be changed.
252 /// - If the span is inside a macro, change the applicability level to `MaybeIncorrect`.
253 /// - If the default value is used and the applicability level is `MachineApplicable`, change it to
254 /// `HasPlaceholders`
snippet_with_applicability<'a, T: LintContext>( cx: &T, span: Span, default: &'a str, applicability: &mut Applicability, ) -> Cow<'a, str>255 pub fn snippet_with_applicability<'a, T: LintContext>(
256 cx: &T,
257 span: Span,
258 default: &'a str,
259 applicability: &mut Applicability,
260 ) -> Cow<'a, str> {
261 snippet_with_applicability_sess(cx.sess(), span, default, applicability)
262 }
263
snippet_with_applicability_sess<'a>( sess: &Session, span: Span, default: &'a str, applicability: &mut Applicability, ) -> Cow<'a, str>264 fn snippet_with_applicability_sess<'a>(
265 sess: &Session,
266 span: Span,
267 default: &'a str,
268 applicability: &mut Applicability,
269 ) -> Cow<'a, str> {
270 if *applicability != Applicability::Unspecified && span.from_expansion() {
271 *applicability = Applicability::MaybeIncorrect;
272 }
273 snippet_opt_sess(sess, span).map_or_else(
274 || {
275 if *applicability == Applicability::MachineApplicable {
276 *applicability = Applicability::HasPlaceholders;
277 }
278 Cow::Borrowed(default)
279 },
280 From::from,
281 )
282 }
283
284 /// Converts a span to a code snippet. Returns `None` if not available.
snippet_opt(cx: &impl LintContext, span: Span) -> Option<String>285 pub fn snippet_opt(cx: &impl LintContext, span: Span) -> Option<String> {
286 snippet_opt_sess(cx.sess(), span)
287 }
288
snippet_opt_sess(sess: &Session, span: Span) -> Option<String>289 fn snippet_opt_sess(sess: &Session, span: Span) -> Option<String> {
290 sess.source_map().span_to_snippet(span).ok()
291 }
292
293 /// Converts a span (from a block) to a code snippet if available, otherwise use default.
294 ///
295 /// This trims the code of indentation, except for the first line. Use it for blocks or block-like
296 /// things which need to be printed as such.
297 ///
298 /// The `indent_relative_to` arg can be used, to provide a span, where the indentation of the
299 /// resulting snippet of the given span.
300 ///
301 /// # Example
302 ///
303 /// ```rust,ignore
304 /// snippet_block(cx, block.span, "..", None)
305 /// // where, `block` is the block of the if expr
306 /// if x {
307 /// y;
308 /// }
309 /// // will return the snippet
310 /// {
311 /// y;
312 /// }
313 /// ```
314 ///
315 /// ```rust,ignore
316 /// snippet_block(cx, block.span, "..", Some(if_expr.span))
317 /// // where, `block` is the block of the if expr
318 /// if x {
319 /// y;
320 /// }
321 /// // will return the snippet
322 /// {
323 /// y;
324 /// } // aligned with `if`
325 /// ```
326 /// Note that the first line of the snippet always has 0 indentation.
snippet_block<'a, T: LintContext>( cx: &T, span: Span, default: &'a str, indent_relative_to: Option<Span>, ) -> Cow<'a, str>327 pub fn snippet_block<'a, T: LintContext>(
328 cx: &T,
329 span: Span,
330 default: &'a str,
331 indent_relative_to: Option<Span>,
332 ) -> Cow<'a, str> {
333 let snip = snippet(cx, span, default);
334 let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
335 reindent_multiline(snip, true, indent)
336 }
337
338 /// Same as `snippet_block`, but adapts the applicability level by the rules of
339 /// `snippet_with_applicability`.
snippet_block_with_applicability<'a>( cx: &impl LintContext, span: Span, default: &'a str, indent_relative_to: Option<Span>, applicability: &mut Applicability, ) -> Cow<'a, str>340 pub fn snippet_block_with_applicability<'a>(
341 cx: &impl LintContext,
342 span: Span,
343 default: &'a str,
344 indent_relative_to: Option<Span>,
345 applicability: &mut Applicability,
346 ) -> Cow<'a, str> {
347 let snip = snippet_with_applicability(cx, span, default, applicability);
348 let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
349 reindent_multiline(snip, true, indent)
350 }
351
snippet_block_with_context<'a>( cx: &impl LintContext, span: Span, outer: SyntaxContext, default: &'a str, indent_relative_to: Option<Span>, app: &mut Applicability, ) -> (Cow<'a, str>, bool)352 pub fn snippet_block_with_context<'a>(
353 cx: &impl LintContext,
354 span: Span,
355 outer: SyntaxContext,
356 default: &'a str,
357 indent_relative_to: Option<Span>,
358 app: &mut Applicability,
359 ) -> (Cow<'a, str>, bool) {
360 let (snip, from_macro) = snippet_with_context(cx, span, outer, default, app);
361 let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
362 (reindent_multiline(snip, true, indent), from_macro)
363 }
364
365 /// Same as `snippet_with_applicability`, but first walks the span up to the given context. This
366 /// will result in the macro call, rather then the expansion, if the span is from a child context.
367 /// If the span is not from a child context, it will be used directly instead.
368 ///
369 /// e.g. Given the expression `&vec![]`, getting a snippet from the span for `vec![]` as a HIR node
370 /// would result in `box []`. If given the context of the address of expression, this function will
371 /// correctly get a snippet of `vec![]`.
372 ///
373 /// This will also return whether or not the snippet is a macro call.
snippet_with_context<'a>( cx: &impl LintContext, span: Span, outer: SyntaxContext, default: &'a str, applicability: &mut Applicability, ) -> (Cow<'a, str>, bool)374 pub fn snippet_with_context<'a>(
375 cx: &impl LintContext,
376 span: Span,
377 outer: SyntaxContext,
378 default: &'a str,
379 applicability: &mut Applicability,
380 ) -> (Cow<'a, str>, bool) {
381 snippet_with_context_sess(cx.sess(), span, outer, default, applicability)
382 }
383
snippet_with_context_sess<'a>( sess: &Session, span: Span, outer: SyntaxContext, default: &'a str, applicability: &mut Applicability, ) -> (Cow<'a, str>, bool)384 fn snippet_with_context_sess<'a>(
385 sess: &Session,
386 span: Span,
387 outer: SyntaxContext,
388 default: &'a str,
389 applicability: &mut Applicability,
390 ) -> (Cow<'a, str>, bool) {
391 let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
392 || {
393 // The span is from a macro argument, and the outer context is the macro using the argument
394 if *applicability != Applicability::Unspecified {
395 *applicability = Applicability::MaybeIncorrect;
396 }
397 // TODO: get the argument span.
398 (span, false)
399 },
400 |outer_span| (outer_span, span.ctxt() != outer),
401 );
402
403 (
404 snippet_with_applicability_sess(sess, span, default, applicability),
405 is_macro_call,
406 )
407 }
408
409 /// Walks the span up to the target context, thereby returning the macro call site if the span is
410 /// inside a macro expansion, or the original span if it is not. Note this will return `None` in the
411 /// case of the span being in a macro expansion, but the target context is from expanding a macro
412 /// argument.
413 ///
414 /// Given the following
415 ///
416 /// ```rust,ignore
417 /// macro_rules! m { ($e:expr) => { f($e) }; }
418 /// g(m!(0))
419 /// ```
420 ///
421 /// If called with a span of the call to `f` and a context of the call to `g` this will return a
422 /// span containing `m!(0)`. However, if called with a span of the literal `0` this will give a span
423 /// containing `0` as the context is the same as the outer context.
424 ///
425 /// This will traverse through multiple macro calls. Given the following:
426 ///
427 /// ```rust,ignore
428 /// macro_rules! m { ($e:expr) => { n!($e, 0) }; }
429 /// macro_rules! n { ($e:expr, $f:expr) => { f($e, $f) }; }
430 /// g(m!(0))
431 /// ```
432 ///
433 /// If called with a span of the call to `f` and a context of the call to `g` this will return a
434 /// span containing `m!(0)`.
walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span>435 pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
436 let outer_span = hygiene::walk_chain(span, outer);
437 (outer_span.ctxt() == outer).then_some(outer_span)
438 }
439
440 /// Removes block comments from the given `Vec` of lines.
441 ///
442 /// # Examples
443 ///
444 /// ```rust,ignore
445 /// without_block_comments(vec!["/*", "foo", "*/"]);
446 /// // => vec![]
447 ///
448 /// without_block_comments(vec!["bar", "/*", "foo", "*/"]);
449 /// // => vec!["bar"]
450 /// ```
without_block_comments(lines: Vec<&str>) -> Vec<&str>451 pub fn without_block_comments(lines: Vec<&str>) -> Vec<&str> {
452 let mut without = vec![];
453
454 let mut nest_level = 0;
455
456 for line in lines {
457 if line.contains("/*") {
458 nest_level += 1;
459 continue;
460 } else if line.contains("*/") {
461 nest_level -= 1;
462 continue;
463 }
464
465 if nest_level == 0 {
466 without.push(line);
467 }
468 }
469
470 without
471 }
472
473 /// Trims the whitespace from the start and the end of the span.
trim_span(sm: &SourceMap, span: Span) -> Span474 pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
475 let data = span.data();
476 let sf: &_ = &sm.lookup_source_file(data.lo);
477 let Some(src) = sf.src.as_deref() else {
478 return span;
479 };
480 let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
481 return span;
482 };
483 let trim_start = snip.len() - snip.trim_start().len();
484 let trim_end = snip.len() - snip.trim_end().len();
485 SpanData {
486 lo: data.lo + BytePos::from_usize(trim_start),
487 hi: data.hi - BytePos::from_usize(trim_end),
488 ctxt: data.ctxt,
489 parent: data.parent,
490 }
491 .span()
492 }
493
494 /// Expand a span to include a preceding comma
495 /// ```rust,ignore
496 /// writeln!(o, "") -> writeln!(o, "")
497 /// ^^ ^^^^
498 /// ```
expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span499 pub fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
500 let extended = cx.sess().source_map().span_extend_to_prev_char(span, ',', true);
501 extended.with_lo(extended.lo() - BytePos(1))
502 }
503
504 #[cfg(test)]
505 mod test {
506 use super::{reindent_multiline, without_block_comments};
507
508 #[test]
test_reindent_multiline_single_line()509 fn test_reindent_multiline_single_line() {
510 assert_eq!("", reindent_multiline("".into(), false, None));
511 assert_eq!("...", reindent_multiline("...".into(), false, None));
512 assert_eq!("...", reindent_multiline(" ...".into(), false, None));
513 assert_eq!("...", reindent_multiline("\t...".into(), false, None));
514 assert_eq!("...", reindent_multiline("\t\t...".into(), false, None));
515 }
516
517 #[test]
518 #[rustfmt::skip]
test_reindent_multiline_block()519 fn test_reindent_multiline_block() {
520 assert_eq!("\
521 if x {
522 y
523 } else {
524 z
525 }", reindent_multiline(" if x {
526 y
527 } else {
528 z
529 }".into(), false, None));
530 assert_eq!("\
531 if x {
532 \ty
533 } else {
534 \tz
535 }", reindent_multiline(" if x {
536 \ty
537 } else {
538 \tz
539 }".into(), false, None));
540 }
541
542 #[test]
543 #[rustfmt::skip]
test_reindent_multiline_empty_line()544 fn test_reindent_multiline_empty_line() {
545 assert_eq!("\
546 if x {
547 y
548
549 } else {
550 z
551 }", reindent_multiline(" if x {
552 y
553
554 } else {
555 z
556 }".into(), false, None));
557 }
558
559 #[test]
560 #[rustfmt::skip]
test_reindent_multiline_lines_deeper()561 fn test_reindent_multiline_lines_deeper() {
562 assert_eq!("\
563 if x {
564 y
565 } else {
566 z
567 }", reindent_multiline("\
568 if x {
569 y
570 } else {
571 z
572 }".into(), true, Some(8)));
573 }
574
575 #[test]
test_without_block_comments_lines_without_block_comments()576 fn test_without_block_comments_lines_without_block_comments() {
577 let result = without_block_comments(vec!["/*", "", "*/"]);
578 println!("result: {result:?}");
579 assert!(result.is_empty());
580
581 let result = without_block_comments(vec!["", "/*", "", "*/", "#[crate_type = \"lib\"]", "/*", "", "*/", ""]);
582 assert_eq!(result, vec!["", "#[crate_type = \"lib\"]", ""]);
583
584 let result = without_block_comments(vec!["/* rust", "", "*/"]);
585 assert!(result.is_empty());
586
587 let result = without_block_comments(vec!["/* one-line comment */"]);
588 assert!(result.is_empty());
589
590 let result = without_block_comments(vec!["/* nested", "/* multi-line", "comment", "*/", "test", "*/"]);
591 assert!(result.is_empty());
592
593 let result = without_block_comments(vec!["/* nested /* inline /* comment */ test */ */"]);
594 assert!(result.is_empty());
595
596 let result = without_block_comments(vec!["foo", "bar", "baz"]);
597 assert_eq!(result, vec!["foo", "bar", "baz"]);
598 }
599 }
600