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