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