1 //! Utils for extracting, inspecting or transforming source code
3 #![allow(clippy::module_name_repetitions)]
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}
;
14 /// Like `snippet_block`, but add braces if the expr is not an `ExprKind::Block`.
15 pub fn expr_block
<T
: LintContext
>(
20 indent_relative_to
: Option
<Span
>,
21 app
: &mut Applicability
,
23 let (code
, from_macro
) = snippet_block_with_context(cx
, expr
.span
, outer
, default, indent_relative_to
, app
);
25 format
!("{{ {code} }}")
26 } else if let ExprKind
::Block(_
, _
) = expr
.kind
{
29 format
!("{{ {code} }}")
33 /// Returns a new Span that extends the original Span to the first non-whitespace char of the first
39 /// // will be converted to
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
))
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
))
55 /// Extends the span to the beginning of the spans line, incl. whitespaces.
60 /// // will be converted to
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
)
72 /// Returns the indentation of the line of a span
76 /// // ^^ -- will return 0
78 /// // ^^ -- will return 4
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()))
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();
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() {
107 /// Returns the position just before rarrow
110 /// fn into(self) -> () {}
112 /// // in case of unformatted code
113 /// fn into2(self)-> () {}
115 /// fn into3(self) -> () {}
118 pub fn position_before_rarrow(s
: &str) -> Option
<usize> {
119 s
.rfind("->").map(|rpos
| {
121 let chars
: Vec
<char> = s
.chars().collect();
123 if let Some(c
) = chars
.get(rpos
- 1) {
124 if c
.is_whitespace() {
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()
143 fn reindent_multiline_inner(s
: &str, ignore_first
: bool
, indent
: Option
<usize>, ch
: char) -> String
{
146 .skip(usize::from(ignore_first
))
151 // ignore empty lines
152 Some(l
.char_indices().find(|&(_
, x
)| x
!= ch
).unwrap_or((l
.len(), ch
)).0)
157 let indent
= indent
.unwrap_or(0);
161 if (ignore_first
&& i
== 0) || l
.is_empty() {
163 } else if x
> indent
{
164 l
.split_at(x
- indent
).1.to_owned()
166 " ".repeat(indent
- x
) + l
169 .collect
::<Vec
<String
>>()
173 /// Converts a span to a code snippet if available, otherwise returns the default.
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.
181 /// // Given two spans one for `value` and one for the `init` expression.
182 /// let value = Vec::new();
183 /// // ^^^^^ ^^^^^^^^^^
186 /// // The snipped call would return the corresponding code snippet
187 /// snippet(cx, span1, "..") // -> "value"
188 /// snippet(cx, span2, "..") // -> "Vec::new()"
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
)
194 /// Same as [`snippet`], but it adapts the applicability level by following rules:
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
>(
204 applicability
: &mut Applicability
,
206 snippet_with_applicability_sess(cx
.sess(), span
, default, applicability
)
209 fn snippet_with_applicability_sess
<'a
>(
213 applicability
: &mut Applicability
,
215 if *applicability
!= Applicability
::Unspecified
&& span
.from_expansion() {
216 *applicability
= Applicability
::MaybeIncorrect
;
218 snippet_opt_sess(sess
, span
).map_or_else(
220 if *applicability
== Applicability
::MachineApplicable
{
221 *applicability
= Applicability
::HasPlaceholders
;
223 Cow
::Borrowed(default)
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
)
234 fn snippet_opt_sess(sess
: &Session
, span
: Span
) -> Option
<String
> {
235 sess
.source_map().span_to_snippet(span
).ok()
238 /// Converts a span (from a block) to a code snippet if available, otherwise use default.
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.
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.
249 /// snippet_block(cx, block.span, "..", None)
250 /// // where, `block` is the block of the if expr
254 /// // will return the snippet
261 /// snippet_block(cx, block.span, "..", Some(if_expr.span))
262 /// // where, `block` is the block of the if expr
266 /// // will return the snippet
269 /// } // aligned with `if`
271 /// Note that the first line of the snippet always has 0 indentation.
272 pub fn snippet_block
<'a
, T
: LintContext
>(
276 indent_relative_to
: Option
<Span
>,
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
)
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
,
289 indent_relative_to
: Option
<Span
>,
290 applicability
: &mut Applicability
,
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
)
297 pub fn snippet_block_with_context
<'a
>(
298 cx
: &impl LintContext
,
300 outer
: SyntaxContext
,
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
)
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.
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![]`.
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
,
322 outer
: SyntaxContext
,
324 applicability
: &mut Applicability
,
325 ) -> (Cow
<'a
, str>, bool
) {
326 snippet_with_context_sess(cx
.sess(), span
, outer
, default, applicability
)
329 fn snippet_with_context_sess
<'a
>(
332 outer
: SyntaxContext
,
334 applicability
: &mut Applicability
,
335 ) -> (Cow
<'a
, str>, bool
) {
336 let (span
, is_macro_call
) = walk_span_to_context(span
, outer
).map_or_else(
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
;
342 // TODO: get the argument span.
345 |outer_span
| (outer_span
, span
.ctxt() != outer
),
349 snippet_with_applicability_sess(sess
, span
, default, applicability
),
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
359 /// Given the following
362 /// macro_rules! m { ($e:expr) => { f($e) }; }
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.
370 /// This will traverse through multiple macro calls. Given the following:
373 /// macro_rules! m { ($e:expr) => { n!($e, 0) }; }
374 /// macro_rules! n { ($e:expr, $f:expr) => { f($e, $f) }; }
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
)
385 /// Removes block comments from the given `Vec` of lines.
390 /// without_block_comments(vec!["/*", "foo", "*/"]);
393 /// without_block_comments(vec!["bar", "/*", "foo", "*/"]);
394 /// // => vec!["bar"]
396 pub fn without_block_comments(lines
: Vec
<&str>) -> Vec
<&str> {
397 let mut without
= vec
![];
399 let mut nest_level
= 0;
402 if line
.contains("/*") {
405 } else if line
.contains("*/") {
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 {
425 let Some(snip
) = &src
.get((data
.lo
- sf
.start_pos
).to_usize()..(data
.hi
- sf
.start_pos
).to_usize()) else {
428 let trim_start
= snip
.len() - snip
.trim_start().len();
429 let trim_end
= snip
.len() - snip
.trim_end().len();
431 lo
: data
.lo
+ BytePos
::from_usize(trim_start
),
432 hi
: data
.hi
- BytePos
::from_usize(trim_end
),
439 /// Expand a span to include a preceding comma
441 /// writeln!(o, "") -> writeln!(o, "")
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))
451 use super::{reindent_multiline, without_block_comments}
;
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
));
464 fn test_reindent_multiline_block() {
470 }", reindent_multiline(" if x {
474 }".into(), false, None
));
480 }", reindent_multiline(" if x {
484 }".into(), false, None
));
489 fn test_reindent_multiline_empty_line() {
496 }", reindent_multiline(" if x {
501 }".into(), false, None
));
506 fn test_reindent_multiline_lines_deeper() {
512 }", reindent_multiline("\
517 }".into(), true, Some(8)));
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());
526 let result
= without_block_comments(vec
!["", "/*", "", "*/", "#[crate_type = \"lib\"]", "/*", "", "*/", ""]);
527 assert_eq
!(result
, vec
!["", "#[crate_type = \"lib\"]", ""]);
529 let result
= without_block_comments(vec
!["/* rust", "", "*/"]);
530 assert
!(result
.is_empty());
532 let result
= without_block_comments(vec
!["/* one-line comment */"]);
533 assert
!(result
.is_empty());
535 let result
= without_block_comments(vec
!["/* nested", "/* multi-line", "comment", "*/", "test", "*/"]);
536 assert
!(result
.is_empty());
538 let result
= without_block_comments(vec
!["/* nested /* inline /* comment */ test */ */"]);
539 assert
!(result
.is_empty());
541 let result
= without_block_comments(vec
!["foo", "bar", "baz"]);
542 assert_eq
!(result
, vec
!["foo", "bar", "baz"]);