421 lines
13 KiB
Rust
421 lines
13 KiB
Rust
|
//! Utils for extracting, inspecting or transforming source code
|
||
|
|
||
|
#![allow(clippy::module_name_repetitions)]
|
||
|
|
||
|
use crate::line_span;
|
||
|
use rustc_errors::Applicability;
|
||
|
use rustc_hir::{Expr, ExprKind};
|
||
|
use rustc_lint::{LateContext, LintContext};
|
||
|
use rustc_span::hygiene;
|
||
|
use rustc_span::{BytePos, Pos, Span, SyntaxContext};
|
||
|
use std::borrow::Cow;
|
||
|
|
||
|
/// Like `snippet_block`, but add braces if the expr is not an `ExprKind::Block`.
|
||
|
/// Also takes an `Option<String>` which can be put inside the braces.
|
||
|
pub fn expr_block<'a, T: LintContext>(
|
||
|
cx: &T,
|
||
|
expr: &Expr<'_>,
|
||
|
option: Option<String>,
|
||
|
default: &'a str,
|
||
|
indent_relative_to: Option<Span>,
|
||
|
) -> Cow<'a, str> {
|
||
|
let code = snippet_block(cx, expr.span, default, indent_relative_to);
|
||
|
let string = option.unwrap_or_default();
|
||
|
if expr.span.from_expansion() {
|
||
|
Cow::Owned(format!("{{ {} }}", snippet_with_macro_callsite(cx, expr.span, default)))
|
||
|
} else if let ExprKind::Block(_, _) = expr.kind {
|
||
|
Cow::Owned(format!("{}{}", code, string))
|
||
|
} else if string.is_empty() {
|
||
|
Cow::Owned(format!("{{ {} }}", code))
|
||
|
} else {
|
||
|
Cow::Owned(format!("{{\n{};\n{}\n}}", code, string))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Returns a new Span that extends the original Span to the first non-whitespace char of the first
|
||
|
/// line.
|
||
|
///
|
||
|
/// ```rust,ignore
|
||
|
/// let x = ();
|
||
|
/// // ^^
|
||
|
/// // will be converted to
|
||
|
/// let x = ();
|
||
|
/// // ^^^^^^^^^^
|
||
|
/// ```
|
||
|
pub fn first_line_of_span<T: LintContext>(cx: &T, span: Span) -> Span {
|
||
|
first_char_in_first_line(cx, span).map_or(span, |first_char_pos| span.with_lo(first_char_pos))
|
||
|
}
|
||
|
|
||
|
fn first_char_in_first_line<T: LintContext>(cx: &T, span: Span) -> Option<BytePos> {
|
||
|
let line_span = line_span(cx, span);
|
||
|
snippet_opt(cx, line_span).and_then(|snip| {
|
||
|
snip.find(|c: char| !c.is_whitespace())
|
||
|
.map(|pos| line_span.lo() + BytePos::from_usize(pos))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/// Returns the indentation of the line of a span
|
||
|
///
|
||
|
/// ```rust,ignore
|
||
|
/// let x = ();
|
||
|
/// // ^^ -- will return 0
|
||
|
/// let x = ();
|
||
|
/// // ^^ -- will return 4
|
||
|
/// ```
|
||
|
pub fn indent_of<T: LintContext>(cx: &T, span: Span) -> Option<usize> {
|
||
|
snippet_opt(cx, line_span(cx, span)).and_then(|snip| snip.find(|c: char| !c.is_whitespace()))
|
||
|
}
|
||
|
|
||
|
// If the snippet is empty, it's an attribute that was inserted during macro
|
||
|
// expansion and we want to ignore those, because they could come from external
|
||
|
// sources that the user has no control over.
|
||
|
// For some reason these attributes don't have any expansion info on them, so
|
||
|
// we have to check it this way until there is a better way.
|
||
|
pub fn is_present_in_source<T: LintContext>(cx: &T, span: Span) -> bool {
|
||
|
if let Some(snippet) = snippet_opt(cx, span) {
|
||
|
if snippet.is_empty() {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
true
|
||
|
}
|
||
|
|
||
|
/// Returns the positon just before rarrow
|
||
|
///
|
||
|
/// ```rust,ignore
|
||
|
/// fn into(self) -> () {}
|
||
|
/// ^
|
||
|
/// // in case of unformatted code
|
||
|
/// fn into2(self)-> () {}
|
||
|
/// ^
|
||
|
/// fn into3(self) -> () {}
|
||
|
/// ^
|
||
|
/// ```
|
||
|
pub fn position_before_rarrow(s: &str) -> Option<usize> {
|
||
|
s.rfind("->").map(|rpos| {
|
||
|
let mut rpos = rpos;
|
||
|
let chars: Vec<char> = s.chars().collect();
|
||
|
while rpos > 1 {
|
||
|
if let Some(c) = chars.get(rpos - 1) {
|
||
|
if c.is_whitespace() {
|
||
|
rpos -= 1;
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
rpos
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/// Reindent a multiline string with possibility of ignoring the first line.
|
||
|
#[allow(clippy::needless_pass_by_value)]
|
||
|
pub fn reindent_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str> {
|
||
|
let s_space = reindent_multiline_inner(&s, ignore_first, indent, ' ');
|
||
|
let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
|
||
|
reindent_multiline_inner(&s_tab, ignore_first, indent, ' ').into()
|
||
|
}
|
||
|
|
||
|
fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
|
||
|
let x = s
|
||
|
.lines()
|
||
|
.skip(ignore_first as usize)
|
||
|
.filter_map(|l| {
|
||
|
if l.is_empty() {
|
||
|
None
|
||
|
} else {
|
||
|
// ignore empty lines
|
||
|
Some(l.char_indices().find(|&(_, x)| x != ch).unwrap_or((l.len(), ch)).0)
|
||
|
}
|
||
|
})
|
||
|
.min()
|
||
|
.unwrap_or(0);
|
||
|
let indent = indent.unwrap_or(0);
|
||
|
s.lines()
|
||
|
.enumerate()
|
||
|
.map(|(i, l)| {
|
||
|
if (ignore_first && i == 0) || l.is_empty() {
|
||
|
l.to_owned()
|
||
|
} else if x > indent {
|
||
|
l.split_at(x - indent).1.to_owned()
|
||
|
} else {
|
||
|
" ".repeat(indent - x) + l
|
||
|
}
|
||
|
})
|
||
|
.collect::<Vec<String>>()
|
||
|
.join("\n")
|
||
|
}
|
||
|
|
||
|
/// Converts a span to a code snippet if available, otherwise use default.
|
||
|
///
|
||
|
/// This is useful if you want to provide suggestions for your lint or more generally, if you want
|
||
|
/// to convert a given `Span` to a `str`.
|
||
|
///
|
||
|
/// # Example
|
||
|
/// ```rust,ignore
|
||
|
/// snippet(cx, expr.span, "..")
|
||
|
/// ```
|
||
|
pub fn snippet<'a, T: LintContext>(cx: &T, span: Span, default: &'a str) -> Cow<'a, str> {
|
||
|
snippet_opt(cx, span).map_or_else(|| Cow::Borrowed(default), From::from)
|
||
|
}
|
||
|
|
||
|
/// Same as `snippet`, but it adapts the applicability level by following rules:
|
||
|
///
|
||
|
/// - Applicability level `Unspecified` will never be changed.
|
||
|
/// - If the span is inside a macro, change the applicability level to `MaybeIncorrect`.
|
||
|
/// - If the default value is used and the applicability level is `MachineApplicable`, change it to
|
||
|
/// `HasPlaceholders`
|
||
|
pub fn snippet_with_applicability<'a, T: LintContext>(
|
||
|
cx: &T,
|
||
|
span: Span,
|
||
|
default: &'a str,
|
||
|
applicability: &mut Applicability,
|
||
|
) -> Cow<'a, str> {
|
||
|
if *applicability != Applicability::Unspecified && span.from_expansion() {
|
||
|
*applicability = Applicability::MaybeIncorrect;
|
||
|
}
|
||
|
snippet_opt(cx, span).map_or_else(
|
||
|
|| {
|
||
|
if *applicability == Applicability::MachineApplicable {
|
||
|
*applicability = Applicability::HasPlaceholders;
|
||
|
}
|
||
|
Cow::Borrowed(default)
|
||
|
},
|
||
|
From::from,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/// Same as `snippet`, but should only be used when it's clear that the input span is
|
||
|
/// not a macro argument.
|
||
|
pub fn snippet_with_macro_callsite<'a, T: LintContext>(cx: &T, span: Span, default: &'a str) -> Cow<'a, str> {
|
||
|
snippet(cx, span.source_callsite(), default)
|
||
|
}
|
||
|
|
||
|
/// Converts a span to a code snippet. Returns `None` if not available.
|
||
|
pub fn snippet_opt<T: LintContext>(cx: &T, span: Span) -> Option<String> {
|
||
|
cx.sess().source_map().span_to_snippet(span).ok()
|
||
|
}
|
||
|
|
||
|
/// Converts a span (from a block) to a code snippet if available, otherwise use default.
|
||
|
///
|
||
|
/// This trims the code of indentation, except for the first line. Use it for blocks or block-like
|
||
|
/// things which need to be printed as such.
|
||
|
///
|
||
|
/// The `indent_relative_to` arg can be used, to provide a span, where the indentation of the
|
||
|
/// resulting snippet of the given span.
|
||
|
///
|
||
|
/// # Example
|
||
|
///
|
||
|
/// ```rust,ignore
|
||
|
/// snippet_block(cx, block.span, "..", None)
|
||
|
/// // where, `block` is the block of the if expr
|
||
|
/// if x {
|
||
|
/// y;
|
||
|
/// }
|
||
|
/// // will return the snippet
|
||
|
/// {
|
||
|
/// y;
|
||
|
/// }
|
||
|
/// ```
|
||
|
///
|
||
|
/// ```rust,ignore
|
||
|
/// snippet_block(cx, block.span, "..", Some(if_expr.span))
|
||
|
/// // where, `block` is the block of the if expr
|
||
|
/// if x {
|
||
|
/// y;
|
||
|
/// }
|
||
|
/// // will return the snippet
|
||
|
/// {
|
||
|
/// y;
|
||
|
/// } // aligned with `if`
|
||
|
/// ```
|
||
|
/// Note that the first line of the snippet always has 0 indentation.
|
||
|
pub fn snippet_block<'a, T: LintContext>(
|
||
|
cx: &T,
|
||
|
span: Span,
|
||
|
default: &'a str,
|
||
|
indent_relative_to: Option<Span>,
|
||
|
) -> Cow<'a, str> {
|
||
|
let snip = snippet(cx, span, default);
|
||
|
let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
|
||
|
reindent_multiline(snip, true, indent)
|
||
|
}
|
||
|
|
||
|
/// Same as `snippet_block`, but adapts the applicability level by the rules of
|
||
|
/// `snippet_with_applicability`.
|
||
|
pub fn snippet_block_with_applicability<'a, T: LintContext>(
|
||
|
cx: &T,
|
||
|
span: Span,
|
||
|
default: &'a str,
|
||
|
indent_relative_to: Option<Span>,
|
||
|
applicability: &mut Applicability,
|
||
|
) -> Cow<'a, str> {
|
||
|
let snip = snippet_with_applicability(cx, span, default, applicability);
|
||
|
let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
|
||
|
reindent_multiline(snip, true, indent)
|
||
|
}
|
||
|
|
||
|
/// Same as `snippet_with_applicability`, but first walks the span up to the given context. This
|
||
|
/// will result in the macro call, rather then the expansion, if the span is from a child context.
|
||
|
/// If the span is not from a child context, it will be used directly instead.
|
||
|
///
|
||
|
/// e.g. Given the expression `&vec![]`, getting a snippet from the span for `vec![]` as a HIR node
|
||
|
/// would result in `box []`. If given the context of the address of expression, this function will
|
||
|
/// correctly get a snippet of `vec![]`.
|
||
|
///
|
||
|
/// This will also return whether or not the snippet is a macro call.
|
||
|
pub fn snippet_with_context(
|
||
|
cx: &LateContext<'_>,
|
||
|
span: Span,
|
||
|
outer: SyntaxContext,
|
||
|
default: &'a str,
|
||
|
applicability: &mut Applicability,
|
||
|
) -> (Cow<'a, str>, bool) {
|
||
|
let outer_span = hygiene::walk_chain(span, outer);
|
||
|
let (span, is_macro_call) = if outer_span.ctxt() == outer {
|
||
|
(outer_span, span.ctxt() != outer)
|
||
|
} else {
|
||
|
// The span is from a macro argument, and the outer context is the macro using the argument
|
||
|
if *applicability != Applicability::Unspecified {
|
||
|
*applicability = Applicability::MaybeIncorrect;
|
||
|
}
|
||
|
// TODO: get the argument span.
|
||
|
(span, false)
|
||
|
};
|
||
|
|
||
|
(
|
||
|
snippet_with_applicability(cx, span, default, applicability),
|
||
|
is_macro_call,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/// Removes block comments from the given `Vec` of lines.
|
||
|
///
|
||
|
/// # Examples
|
||
|
///
|
||
|
/// ```rust,ignore
|
||
|
/// without_block_comments(vec!["/*", "foo", "*/"]);
|
||
|
/// // => vec![]
|
||
|
///
|
||
|
/// without_block_comments(vec!["bar", "/*", "foo", "*/"]);
|
||
|
/// // => vec!["bar"]
|
||
|
/// ```
|
||
|
pub fn without_block_comments(lines: Vec<&str>) -> Vec<&str> {
|
||
|
let mut without = vec![];
|
||
|
|
||
|
let mut nest_level = 0;
|
||
|
|
||
|
for line in lines {
|
||
|
if line.contains("/*") {
|
||
|
nest_level += 1;
|
||
|
continue;
|
||
|
} else if line.contains("*/") {
|
||
|
nest_level -= 1;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if nest_level == 0 {
|
||
|
without.push(line);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
without
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod test {
|
||
|
use super::{reindent_multiline, without_block_comments};
|
||
|
|
||
|
#[test]
|
||
|
fn test_reindent_multiline_single_line() {
|
||
|
assert_eq!("", reindent_multiline("".into(), false, None));
|
||
|
assert_eq!("...", reindent_multiline("...".into(), false, None));
|
||
|
assert_eq!("...", reindent_multiline(" ...".into(), false, None));
|
||
|
assert_eq!("...", reindent_multiline("\t...".into(), false, None));
|
||
|
assert_eq!("...", reindent_multiline("\t\t...".into(), false, None));
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
#[rustfmt::skip]
|
||
|
fn test_reindent_multiline_block() {
|
||
|
assert_eq!("\
|
||
|
if x {
|
||
|
y
|
||
|
} else {
|
||
|
z
|
||
|
}", reindent_multiline(" if x {
|
||
|
y
|
||
|
} else {
|
||
|
z
|
||
|
}".into(), false, None));
|
||
|
assert_eq!("\
|
||
|
if x {
|
||
|
\ty
|
||
|
} else {
|
||
|
\tz
|
||
|
}", reindent_multiline(" if x {
|
||
|
\ty
|
||
|
} else {
|
||
|
\tz
|
||
|
}".into(), false, None));
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
#[rustfmt::skip]
|
||
|
fn test_reindent_multiline_empty_line() {
|
||
|
assert_eq!("\
|
||
|
if x {
|
||
|
y
|
||
|
|
||
|
} else {
|
||
|
z
|
||
|
}", reindent_multiline(" if x {
|
||
|
y
|
||
|
|
||
|
} else {
|
||
|
z
|
||
|
}".into(), false, None));
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
#[rustfmt::skip]
|
||
|
fn test_reindent_multiline_lines_deeper() {
|
||
|
assert_eq!("\
|
||
|
if x {
|
||
|
y
|
||
|
} else {
|
||
|
z
|
||
|
}", reindent_multiline("\
|
||
|
if x {
|
||
|
y
|
||
|
} else {
|
||
|
z
|
||
|
}".into(), true, Some(8)));
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn test_without_block_comments_lines_without_block_comments() {
|
||
|
let result = without_block_comments(vec!["/*", "", "*/"]);
|
||
|
println!("result: {:?}", result);
|
||
|
assert!(result.is_empty());
|
||
|
|
||
|
let result = without_block_comments(vec!["", "/*", "", "*/", "#[crate_type = \"lib\"]", "/*", "", "*/", ""]);
|
||
|
assert_eq!(result, vec!["", "#[crate_type = \"lib\"]", ""]);
|
||
|
|
||
|
let result = without_block_comments(vec!["/* rust", "", "*/"]);
|
||
|
assert!(result.is_empty());
|
||
|
|
||
|
let result = without_block_comments(vec!["/* one-line comment */"]);
|
||
|
assert!(result.is_empty());
|
||
|
|
||
|
let result = without_block_comments(vec!["/* nested", "/* multi-line", "comment", "*/", "test", "*/"]);
|
||
|
assert!(result.is_empty());
|
||
|
|
||
|
let result = without_block_comments(vec!["/* nested /* inline /* comment */ test */ */"]);
|
||
|
assert!(result.is_empty());
|
||
|
|
||
|
let result = without_block_comments(vec!["foo", "bar", "baz"]);
|
||
|
assert_eq!(result, vec!["foo", "bar", "baz"]);
|
||
|
}
|
||
|
}
|