]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_utils/src/source.rs
New upstream version 1.63.0+dfsg1
[rustc.git] / src / tools / clippy / clippy_utils / src / source.rs
CommitLineData
cdc7bbd5
XL
1//! Utils for extracting, inspecting or transforming source code
2
3#![allow(clippy::module_name_repetitions)]
4
5use crate::line_span;
6use rustc_errors::Applicability;
7use rustc_hir::{Expr, ExprKind};
8use rustc_lint::{LateContext, LintContext};
9use rustc_span::hygiene;
04454e1e 10use rustc_span::source_map::SourceMap;
923072b8 11use rustc_span::{BytePos, Pos, Span, SpanData, SyntaxContext};
cdc7bbd5
XL
12use std::borrow::Cow;
13
04454e1e
FG
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.
18pub 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
cdc7bbd5
XL
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.
34pub 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/// ```
64pub 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
68fn 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/// ```
84pub 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
89pub 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.
102pub 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
04454e1e 111/// Returns the position just before rarrow
cdc7bbd5
XL
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/// ```
122pub 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.
923072b8 140#[expect(clippy::needless_pass_by_value)]
cdc7bbd5
XL
141pub 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
147fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
148 let x = s
149 .lines()
a2a8927a 150 .skip(usize::from(ignore_first))
cdc7bbd5
XL
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
a2a8927a 177/// Converts a span to a code snippet if available, otherwise returns the default.
cdc7bbd5
XL
178///
179/// This is useful if you want to provide suggestions for your lint or more generally, if you want
a2a8927a
XL
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.
cdc7bbd5
XL
182///
183/// # Example
184/// ```rust,ignore
a2a8927a
XL
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()"
cdc7bbd5
XL
193/// ```
194pub 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
94222f64 198/// Same as [`snippet`], but it adapts the applicability level by following rules:
cdc7bbd5
XL
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`
204pub 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.
226pub 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.
231pub 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.
269pub 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`.
282pub 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.
5099ac24 303pub fn snippet_with_context<'a>(
cdc7bbd5
XL
304 cx: &LateContext<'_>,
305 span: Span,
306 outer: SyntaxContext,
307 default: &'a str,
308 applicability: &mut Applicability,
309) -> (Cow<'a, str>, bool) {
17df50a5
XL
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 );
cdc7bbd5
XL
321
322 (
323 snippet_with_applicability(cx, span, default, applicability),
324 is_macro_call,
325 )
326}
327
17df50a5
XL
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)`.
354pub 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
cdc7bbd5
XL
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/// ```
370pub 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
923072b8
FG
392/// Trims the whitespace from the start and the end of the span.
393pub 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
cdc7bbd5
XL
413#[cfg(test)]
414mod 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}