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