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