blob: e589485dc8f4dfcb1dce2690178fef3d4ce2b3c9 [file]
pub mod cursor;
use self::cursor::{Capture, Cursor, IdentPat};
use crate::utils::{ErrAction, Scoped, StrBuf, VecBuf, expect_action, slice_groups_mut, walk_dir_no_dot_or_target};
use crate::{DiagCx, SourceFile, Span};
use core::fmt::{self, Display};
use core::range::Range;
use rustc_arena::{DroplessArena, TypedArena};
use rustc_data_structures::fx::FxHashMap;
use std::collections::hash_map::{Entry, VacantEntry};
use std::{fs, path};
pub struct ParseCxImpl<'cx> {
pub arena: &'cx DroplessArena,
pub source_files: &'cx TypedArena<SourceFile<'cx>>,
pub str_buf: StrBuf,
pub str_list_buf: VecBuf<&'cx str>,
pub dcx: DiagCx,
}
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();
let source_files = TypedArena::default();
f(&mut Scoped::new(ParseCxImpl {
arena: &arena,
source_files: &source_files,
str_buf: StrBuf::with_capacity(128),
str_list_buf: VecBuf::with_capacity(128),
dcx: DiagCx::default(),
}))
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum LintTool {
Rustc,
Clippy,
}
impl LintTool {
/// Gets the namespace prefix to use when naming a lint including the `::`.
pub fn prefix(self) -> &'static str {
match self {
Self::Rustc => "",
Self::Clippy => "clippy::",
}
}
pub fn from_prefix(s: &str) -> Option<Self> {
(s == "clippy").then_some(Self::Clippy)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct LintName<'cx> {
pub tool: LintTool,
pub name: &'cx str,
}
impl<'cx> LintName<'cx> {
pub fn new_rustc(name: &'cx str) -> Self {
Self {
tool: LintTool::Rustc,
name,
}
}
pub fn new_clippy(name: &'cx str) -> Self {
Self {
tool: LintTool::Clippy,
name,
}
}
}
impl Display for LintName<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.tool.prefix())?;
f.write_str(self.name)
}
}
pub struct ActiveLint<'cx> {
pub group: &'cx str,
pub decl_range: Range<u32>,
}
pub struct DeprecatedLint<'cx> {
pub reason: &'cx str,
pub version: &'cx str,
}
pub struct RenamedLint<'cx> {
pub new_name: LintName<'cx>,
pub version: &'cx str,
}
pub enum LintData<'cx> {
Active(ActiveLint<'cx>),
Deprecated(DeprecatedLint<'cx>),
Renamed(RenamedLint<'cx>),
}
pub struct Lint<'cx> {
pub name_sp: Span<'cx>,
pub data: LintData<'cx>,
}
#[derive(Clone, Copy)]
pub enum LintPassMac {
Declare,
Impl,
}
impl LintPassMac {
pub fn name(self) -> &'static str {
match self {
Self::Declare => "declare_lint_pass",
Self::Impl => "impl_lint_pass",
}
}
}
pub struct LintPass<'cx> {
/// The raw text of the documentation comments. May include leading/trailing
/// whitespace and empty lines.
pub docs: &'cx str,
pub name: &'cx str,
pub lt: Option<&'cx str>,
pub mac: LintPassMac,
pub decl_sp: Span<'cx>,
pub lints: &'cx mut [&'cx str],
}
pub struct ParsedLints<'cx> {
pub lints: FxHashMap<&'cx str, Lint<'cx>>,
pub lint_passes: Vec<LintPass<'cx>>,
pub deprecated_file: &'cx SourceFile<'cx>,
}
impl<'cx> ParsedLints<'cx> {
#[expect(clippy::mutable_key_type)]
pub fn mk_file_to_lint_decl_map(&self) -> FxHashMap<&'cx SourceFile<'cx>, Vec<(&'cx str, Range<u32>)>> {
#[expect(clippy::default_trait_access)]
let mut lints = FxHashMap::with_capacity_and_hasher(500, Default::default());
for (&name, lint) in &self.lints {
if let LintData::Active(lint_data) = &lint.data {
lints
.entry(lint.name_sp.file)
.or_insert_with(|| Vec::with_capacity(8))
.push((name, lint_data.decl_range));
}
}
lints
}
pub fn iter_passes_by_file_mut<'s>(&'s mut self) -> impl Iterator<Item = &'s mut [LintPass<'cx>]> {
slice_groups_mut(&mut self.lint_passes, |head, tail| {
tail.iter().take_while(|&x| x.decl_sp.file == head.decl_sp.file).count()
})
}
#[track_caller]
fn get_vacant_lint<'a>(
&'a mut self,
dcx: &mut DiagCx,
name: &'cx str,
name_sp: Span<'cx>,
) -> Option<VacantEntry<'a, &'cx str, Lint<'cx>>> {
match self.lints.entry(name) {
Entry::Vacant(e) => Some(e),
Entry::Occupied(e) => {
dcx.emit_duplicate_lint(name_sp, e.get().name_sp);
None
},
}
}
}
impl<'cx> ParseCxImpl<'cx> {
/// Finds and parses all lint declarations.
pub fn parse_lint_decls(&mut self) -> ParsedLints<'cx> {
let mut data = ParsedLints {
#[expect(clippy::default_trait_access)]
lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()),
lint_passes: Vec::with_capacity(400),
deprecated_file: self.source_files.alloc(SourceFile::load(self.str_buf.alloc_collect(
self.arena,
[
"clippy_lints",
path::MAIN_SEPARATOR_STR,
"src",
path::MAIN_SEPARATOR_STR,
"deprecated_lints.rs",
],
))),
};
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 e.path().as_os_str().as_encoded_bytes().ends_with(b".rs")
&& let Some(file_path) = e.path().to_str()
&& file_path != data.deprecated_file.path.get()
{
let file = self
.source_files
.alloc(SourceFile::load(self.arena.alloc_str(file_path)));
self.parse_lint_src_file(&mut data, file);
}
}
}
self.parse_deprecated_lints(&mut data);
data
}
/// Parse a source file looking for `declare_clippy_lint` macro invocations.
fn parse_lint_src_file(&mut self, data: &mut ParsedLints<'cx>, file: &'cx SourceFile<'cx>) {
#[allow(clippy::enum_glob_use)]
use cursor::Pat::*;
let mut cursor = Cursor::new(&file.contents);
let mut captures = [Capture::EMPTY; 3];
while let Some(mac_name) = cursor.find_capture_ident() {
if !cursor.eat_bang() {
continue;
}
match cursor.get_text(mac_name) {
"declare_clippy_lint" => {
#[rustfmt::skip]
static DECL_START: &[cursor::Pat] = &[
// { /// docs
OpenBrace, AnyComments,
// #[clippy::version = "version"]
Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon,
Ident(IdentPat::version), Eq, CaptureLitStr, CloseBracket,
// pub NAME, GROUP, "desc",
Ident(IdentPat::r#pub), CaptureIdent, Comma,
AnyComments, CaptureIdent, Comma, AnyComments, LitStr,
];
#[rustfmt::skip]
static OPTION: &[cursor::Pat] = &[
// @option = value
AnyComments, At, AnyIdent, Eq, Lit,
];
if let Err(expected) = cursor
.match_all(DECL_START, &mut captures)
.and_then(|()| {
(!cursor.eat_comma()).ok_or(()).or_else(|()| {
cursor.eat_list(|cursor| cursor.match_all(OPTION, &mut []).map(|()| true))
})
})
.and_then(|()| cursor.eat_close_brace().ok_or("`}`"))
{
cursor.emit_unexpected(&mut self.dcx, file, expected);
} else if let name = self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[1]))
&& let name_sp = captures[1].mk_sp(file)
&& let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp)
{
let _ = self.parse_version(cursor.get_text(captures[0]), captures[0].mk_sp(file));
e.insert(Lint {
name_sp,
data: LintData::Active(ActiveLint {
group: cursor.get_text(captures[2]),
decl_range: mac_name.pos..cursor.pos(),
}),
});
}
},
mac @ ("declare_lint_pass" | "impl_lint_pass") => {
let mut has_lt = false;
let mut lints: &mut [_] = &mut [];
if let Err(expected) = cursor
.match_all(&[OpenParen, CaptureDocLines, CaptureIdent], &mut captures)
.and_then(|()| cursor.opt_match_all(&[Lt, CaptureLifetime, Gt], &mut captures[2..]))
.and_then(|res| {
has_lt = res;
cursor.match_all(&[FatArrow, OpenBracket], &mut [])
})
.and_then(|()| {
cursor.capture_list(&mut self.str_list_buf, self.arena, |cursor| {
cursor.capture_opt_path(&mut self.str_buf, self.arena)
})
})
.and_then(|res| {
lints = res;
cursor.match_all(&[CloseBracket, CloseParen, Semi], &mut [])
})
{
cursor.emit_unexpected(&mut self.dcx, file, expected);
} else {
data.lint_passes.push(LintPass {
docs: cursor.get_text(captures[0]),
name: cursor.get_text(captures[1]),
lt: has_lt.then(|| cursor.get_text(captures[2])),
mac: if matches!(mac, "declare_lint_pass") {
LintPassMac::Declare
} else {
LintPassMac::Impl
},
decl_sp: Span::new(file, mac_name.pos..cursor.pos()),
lints,
});
}
},
_ => {},
}
}
}
fn parse_deprecated_lints(&mut self, data: &mut ParsedLints<'cx>) {
#[allow(clippy::enum_glob_use)]
use cursor::Pat::*;
#[rustfmt::skip]
static DECL_TOKENS: &[cursor::Pat] = &[
// #[clippy::version = "version"]
Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon,
Ident(IdentPat::version), Eq, CaptureLitStr, CloseBracket,
// ("first", "second")
OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen,
];
#[rustfmt::skip]
static DEPRECATED_TOKENS: &[cursor::Pat] = &[
// !{ DEPRECATED(DEPRECATED_VERSION) = [
Bang, OpenBrace, Ident(IdentPat::DEPRECATED), OpenParen,
Ident(IdentPat::DEPRECATED_VERSION), CloseParen, Eq, OpenBracket,
];
#[rustfmt::skip]
static RENAMED_TOKENS: &[cursor::Pat] = &[
// !{ RENAMED(RENAMED_VERSION) = [
Bang, OpenBrace, Ident(IdentPat::RENAMED), OpenParen,
Ident(IdentPat::RENAMED_VERSION), CloseParen, Eq, OpenBracket,
];
let file = data.deprecated_file;
let mut cursor = Cursor::new(&file.contents);
let mut captures = [Capture::EMPTY; 3];
if let Err(expected) = cursor
.find_ident("declare_with_version")
.ok_or("`declare_with_version`")
.and_then(|()| {
cursor
.find_ident("declare_with_version")
.ok_or("`declare_with_version`")
})
.and_then(|()| cursor.match_all(DEPRECATED_TOKENS, &mut []))
.and_then(|()| {
cursor.eat_list(|cursor| {
let parsed = cursor.opt_match_all(DECL_TOKENS, &mut captures)?;
if parsed
&& let [version, name, reason] = captures
&& let name_sp = name.mk_sp(file)
&& let (Some(version), Some(name), Some(reason)) = (
self.parse_version(cursor.get_text(version), version.mk_sp(file)),
self.parse_clippy_lint_name(cursor.get_text(name), name_sp),
self.parse_str_lit(cursor.get_text(reason), reason.mk_sp(file)),
)
&& let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp)
{
e.insert(Lint {
name_sp,
data: LintData::Deprecated(DeprecatedLint { reason, version }),
});
}
Ok(parsed)
})
})
.and_then(|()| {
cursor
.find_ident("declare_with_version")
.ok_or("`declare_with_version`")
})
.and_then(|()| cursor.match_all(RENAMED_TOKENS, &mut []))
.and_then(|()| {
cursor.eat_list(|cursor| {
let parsed = cursor.opt_match_all(DECL_TOKENS, &mut captures)?;
if parsed
&& let [version, name, new_name] = captures
&& let name_sp = name.mk_sp(file)
&& let (Some(version), Some(name), Some(new_name)) = (
self.parse_version(cursor.get_text(version), version.mk_sp(file)),
self.parse_clippy_lint_name(cursor.get_text(name), name_sp),
self.parse_lint_name(cursor.get_text(new_name), new_name.mk_sp(file)),
)
&& let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp)
{
e.insert(Lint {
name_sp,
data: LintData::Renamed(RenamedLint { new_name, version }),
});
}
Ok(parsed)
})
})
{
cursor.emit_unexpected(&mut self.dcx, file, expected);
}
}
/// Removes the line splices and surrounding quotes from a string literal.
fn parse_str_lit(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<&'cx str> {
let (s, is_raw, sp_base) = if let Some(trimmed) = s.strip_prefix("r") {
let trimmed = trimmed.trim_start_matches('#');
#[expect(clippy::cast_possible_truncation)]
let sp_base = (s.len() - trimmed.len() + 1) as u32;
(trimmed.trim_end_matches('#'), true, sp_base)
} else {
(s, false, 1)
};
let sp_base = sp.range.start + sp_base;
let s = s
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
let mut is_ok = true;
if is_raw {
rustc_literal_escaper::check_raw_str(s, |range, c| {
if c.is_err_and(|e| e.is_fatal()) {
#[expect(clippy::cast_possible_truncation)]
let range = range.start as u32 + sp_base..range.end as u32 + sp_base;
self.dcx
.emit_spanned_err(Span::new(sp.file, range), "invalid string escape sequence");
is_ok = false;
}
});
is_ok.then_some(s)
} else {
self.str_buf.with(|buf| {
rustc_literal_escaper::unescape_str(s, |range, c| match c {
Ok(c) => buf.push(c),
Err(e) if e.is_fatal() => {
#[expect(clippy::cast_possible_truncation)]
let range = range.start as u32 + sp_base..range.end as u32 + sp_base;
self.dcx
.emit_spanned_err(Span::new(sp.file, range), "invalid string escape sequence");
is_ok = false;
},
Err(_) => {},
});
is_ok.then(|| {
if buf == s {
s
} else if buf.is_empty() {
""
} else {
self.arena.alloc_str(buf)
}
})
})
}
}
#[track_caller]
fn parse_lint_name(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<LintName<'cx>> {
let s = self.parse_str_lit(s, sp)?;
let (tool, name) = match s.split_once("::") {
Some((tool, name)) if let Some(tool) = LintTool::from_prefix(tool) => (tool, name),
Some(..) => {
self.dcx.emit_spanned_err(sp, "unknown lint tool");
return None;
},
None => (LintTool::Rustc, s),
};
if name
.bytes()
.all(|c| matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_'))
{
Some(LintName { tool, name })
} else {
self.dcx.emit_spanned_err(sp, "unparsable lint name");
None
}
}
#[track_caller]
fn parse_clippy_lint_name(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<&'cx str> {
let name = self.parse_lint_name(s, sp)?;
if name.tool == LintTool::Clippy {
Some(name.name)
} else {
self.dcx.emit_not_clippy_lint_name(sp);
None
}
}
#[track_caller]
fn parse_version(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<&'cx str> {
let s = self.parse_str_lit(s, sp)?;
if s.bytes().all(|c| matches!(c, b'0'..=b'9' | b'.')) || matches!(s, "pre 1.29.0" | "CURRENT_RUSTC_VERSION") {
Some(s)
} else {
self.dcx.emit_spanned_err(sp, "unparsable version number");
None
}
}
}