blob: de5caf4e1ef65230fee9011abcea5224239cef7e [file] [log] [blame] [edit]
pub mod cursor;
use self::cursor::{Capture, Cursor};
use crate::utils::{ErrAction, File, Scoped, expect_action, walk_dir_no_dot_or_target};
use core::fmt::{Display, Write as _};
use core::range::Range;
use rustc_arena::DroplessArena;
use std::fs;
use std::path::{self, Path, PathBuf};
use std::str::pattern::Pattern;
pub struct ParseCxImpl<'cx> {
pub arena: &'cx DroplessArena,
pub str_buf: StrBuf,
}
pub type ParseCx<'cx> = &'cx mut ParseCxImpl<'cx>;
/// Calls the given function inside a newly created parsing context.
pub fn new_parse_cx<'env, T>(f: impl for<'cx> FnOnce(&'cx mut Scoped<'cx, 'env, ParseCxImpl<'cx>>) -> T) -> T {
let arena = DroplessArena::default();
f(&mut Scoped::new(ParseCxImpl {
arena: &arena,
str_buf: StrBuf::with_capacity(128),
}))
}
/// A string used as a temporary buffer used to avoid allocating for short lived strings.
pub struct StrBuf(String);
impl StrBuf {
/// Creates a new buffer with the specified initial capacity.
pub fn with_capacity(cap: usize) -> Self {
Self(String::with_capacity(cap))
}
/// Allocates the result of formatting the given value onto the arena.
pub fn alloc_display<'cx>(&mut self, arena: &'cx DroplessArena, value: impl Display) -> &'cx str {
self.0.clear();
write!(self.0, "{value}").expect("`Display` impl returned an error");
arena.alloc_str(&self.0)
}
/// Allocates the string onto the arena with all ascii characters converted to
/// lowercase.
pub fn alloc_ascii_lower<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str {
self.0.clear();
self.0.push_str(s);
self.0.make_ascii_lowercase();
arena.alloc_str(&self.0)
}
/// Allocates the result of replacing all instances the pattern with the given string
/// onto the arena.
pub fn alloc_replaced<'cx>(
&mut self,
arena: &'cx DroplessArena,
s: &str,
pat: impl Pattern,
replacement: &str,
) -> &'cx str {
let mut parts = s.split(pat);
let Some(first) = parts.next() else {
return "";
};
self.0.clear();
self.0.push_str(first);
for part in parts {
self.0.push_str(replacement);
self.0.push_str(part);
}
if self.0.is_empty() {
""
} else {
arena.alloc_str(&self.0)
}
}
/// Performs an operation with the freshly cleared buffer.
pub fn with<T>(&mut self, f: impl FnOnce(&mut String) -> T) -> T {
self.0.clear();
f(&mut self.0)
}
}
pub struct Lint<'cx> {
pub name: &'cx str,
pub group: &'cx str,
pub module: &'cx str,
pub path: PathBuf,
pub declaration_range: Range<usize>,
}
pub struct DeprecatedLint<'cx> {
pub name: &'cx str,
pub reason: &'cx str,
pub version: &'cx str,
}
pub struct RenamedLint<'cx> {
pub old_name: &'cx str,
pub new_name: &'cx str,
pub version: &'cx str,
}
impl<'cx> ParseCxImpl<'cx> {
/// Finds all lint declarations (`declare_clippy_lint!`)
#[must_use]
pub fn find_lint_decls(&mut self) -> Vec<Lint<'cx>> {
let mut lints = Vec::with_capacity(1000);
let mut contents = String::new();
for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
let e = expect_action(e, ErrAction::Read, ".");
// Skip if this isn't a lint crate's directory.
let mut crate_path = if expect_action(e.file_type(), ErrAction::Read, ".").is_dir()
&& let Ok(crate_path) = e.file_name().into_string()
&& crate_path.starts_with("clippy_lints")
&& crate_path != "clippy_lints_internal"
{
crate_path
} else {
continue;
};
crate_path.push(path::MAIN_SEPARATOR);
crate_path.push_str("src");
for e in walk_dir_no_dot_or_target(&crate_path) {
let e = expect_action(e, ErrAction::Read, &crate_path);
if let Some(path) = e.path().to_str()
&& let Some(path) = path.strip_suffix(".rs")
&& let Some(path) = path.get(crate_path.len() + 1..)
{
let module = if path == "lib" {
""
} else {
let path = path
.strip_suffix("mod")
.and_then(|x| x.strip_suffix(path::MAIN_SEPARATOR))
.unwrap_or(path);
self.str_buf
.alloc_replaced(self.arena, path, path::MAIN_SEPARATOR, "::")
};
self.parse_clippy_lint_decls(
e.path(),
File::open_read_to_cleared_string(e.path(), &mut contents),
module,
&mut lints,
);
}
}
}
lints.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
lints
}
/// Parse a source file looking for `declare_clippy_lint` macro invocations.
fn parse_clippy_lint_decls(&mut self, path: &Path, contents: &str, module: &'cx str, lints: &mut Vec<Lint<'cx>>) {
#[allow(clippy::enum_glob_use)]
use cursor::Pat::*;
#[rustfmt::skip]
static DECL_TOKENS: &[cursor::Pat<'_>] = &[
// !{ /// docs
Bang, OpenBrace, AnyComment,
// #[clippy::version = "version"]
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
// pub NAME, GROUP,
Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma,
];
let mut cursor = Cursor::new(contents);
let mut captures = [Capture::EMPTY; 2];
while let Some(start) = cursor.find_ident("declare_clippy_lint") {
if cursor.match_all(DECL_TOKENS, &mut captures) && cursor.find_pat(CloseBrace) {
lints.push(Lint {
name: self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[0])),
group: self.arena.alloc_str(cursor.get_text(captures[1])),
module,
path: path.into(),
declaration_range: start as usize..cursor.pos() as usize,
});
}
}
}
#[must_use]
pub fn read_deprecated_lints(&mut self) -> (Vec<DeprecatedLint<'cx>>, Vec<RenamedLint<'cx>>) {
#[allow(clippy::enum_glob_use)]
use cursor::Pat::*;
#[rustfmt::skip]
static DECL_TOKENS: &[cursor::Pat<'_>] = &[
// #[clippy::version = "version"]
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket,
// ("first", "second"),
OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
];
#[rustfmt::skip]
static DEPRECATED_TOKENS: &[cursor::Pat<'_>] = &[
// !{ DEPRECATED(DEPRECATED_VERSION) = [
Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
];
#[rustfmt::skip]
static RENAMED_TOKENS: &[cursor::Pat<'_>] = &[
// !{ RENAMED(RENAMED_VERSION) = [
Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
];
let path = "clippy_lints/src/deprecated_lints.rs";
let mut deprecated = Vec::with_capacity(30);
let mut renamed = Vec::with_capacity(80);
let mut contents = String::new();
File::open_read_to_cleared_string(path, &mut contents);
let mut cursor = Cursor::new(&contents);
let mut captures = [Capture::EMPTY; 3];
// First instance is the macro definition.
assert!(
cursor.find_ident("declare_with_version").is_some(),
"error reading deprecated lints"
);
if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(DEPRECATED_TOKENS, &mut []) {
while cursor.match_all(DECL_TOKENS, &mut captures) {
deprecated.push(DeprecatedLint {
name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[1])),
reason: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[2])),
version: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[0])),
});
}
} else {
panic!("error reading deprecated lints");
}
if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(RENAMED_TOKENS, &mut []) {
while cursor.match_all(DECL_TOKENS, &mut captures) {
renamed.push(RenamedLint {
old_name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[1])),
new_name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[2])),
version: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[0])),
});
}
} else {
panic!("error reading renamed lints");
}
deprecated.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(rhs.old_name));
(deprecated, renamed)
}
/// Removes the line splices and surrounding quotes from a string literal
fn parse_str_lit(&mut self, s: &str) -> &'cx str {
let (s, is_raw) = if let Some(s) = s.strip_prefix("r") {
(s.trim_matches('#'), true)
} else {
(s, false)
};
let s = s
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
if is_raw {
if s.is_empty() { "" } else { self.arena.alloc_str(s) }
} else {
self.str_buf.with(|buf| {
rustc_literal_escaper::unescape_str(s, &mut |_, ch| {
if let Ok(ch) = ch {
buf.push(ch);
}
});
if buf.is_empty() { "" } else { self.arena.alloc_str(buf) }
})
}
}
fn parse_str_single_line(&mut self, path: &Path, s: &str) -> &'cx str {
let value = self.parse_str_lit(s);
assert!(
!value.contains('\n'),
"error parsing `{}`: `{s}` should be a single line string",
path.display(),
);
value
}
}