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