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