From e96b9262304b5ddf75ffa9cb92a8745a8028c861 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sat, 31 May 2025 10:26:18 -0400 Subject: [PATCH 1/6] `clippy_dev`: Move parsing code to it's own module. --- clippy_dev/src/deprecate_lint.rs | 3 +- clippy_dev/src/lib.rs | 2 + clippy_dev/src/new_lint.rs | 3 +- clippy_dev/src/parse.rs | 399 +++++++++++++++++++++++++++++++ clippy_dev/src/rename_lint.rs | 7 +- clippy_dev/src/update_lints.rs | 267 +-------------------- clippy_dev/src/utils.rs | 177 -------------- 7 files changed, 412 insertions(+), 446 deletions(-) create mode 100644 clippy_dev/src/parse.rs diff --git a/clippy_dev/src/deprecate_lint.rs b/clippy_dev/src/deprecate_lint.rs index 3bdc5b277232..9f6d1984eb57 100644 --- a/clippy_dev/src/deprecate_lint.rs +++ b/clippy_dev/src/deprecate_lint.rs @@ -1,4 +1,5 @@ -use crate::update_lints::{DeprecatedLint, Lint, find_lint_decls, generate_lint_files, read_deprecated_lints}; +use crate::parse::{DeprecatedLint, Lint, find_lint_decls, read_deprecated_lints}; +use crate::update_lints::generate_lint_files; use crate::utils::{UpdateMode, Version}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs index 3361443196ab..578753735c8e 100644 --- a/clippy_dev/src/lib.rs +++ b/clippy_dev/src/lib.rs @@ -34,3 +34,5 @@ pub mod setup; pub mod sync; pub mod update_lints; pub mod utils; + +mod parse; diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index 4121daa85e6d..d73802de1706 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -1,4 +1,5 @@ -use crate::utils::{RustSearcher, Token, Version}; +use crate::parse::{RustSearcher, Token}; +use crate::utils::Version; use clap::ValueEnum; use indoc::{formatdoc, writedoc}; use std::fmt::{self, Write as _}; diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs new file mode 100644 index 000000000000..2b2e5fcf9131 --- /dev/null +++ b/clippy_dev/src/parse.rs @@ -0,0 +1,399 @@ +use crate::utils::{ErrAction, File, expect_action}; +use core::ops::Range; +use core::slice; +use rustc_lexer::{self as lexer, FrontmatterAllowed}; +use std::fs; +use std::path::{Path, PathBuf}; +use walkdir::{DirEntry, WalkDir}; + +#[derive(Clone, Copy)] +pub enum Token<'a> { + /// Matches any number of comments / doc comments. + AnyComment, + Ident(&'a str), + CaptureIdent, + LitStr, + CaptureLitStr, + Bang, + CloseBrace, + CloseBracket, + CloseParen, + /// This will consume the first colon even if the second doesn't exist. + DoubleColon, + Comma, + Eq, + Lifetime, + Lt, + Gt, + OpenBrace, + OpenBracket, + OpenParen, + Pound, + Semi, +} + +pub struct RustSearcher<'txt> { + text: &'txt str, + cursor: lexer::Cursor<'txt>, + pos: u32, + next_token: lexer::Token, +} +impl<'txt> RustSearcher<'txt> { + #[must_use] + #[expect(clippy::inconsistent_struct_constructor)] + pub fn new(text: &'txt str) -> Self { + let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes); + Self { + text, + pos: 0, + next_token: cursor.advance_token(), + cursor, + } + } + + #[must_use] + pub fn peek_text(&self) -> &'txt str { + &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] + } + + #[must_use] + pub fn peek_len(&self) -> u32 { + self.next_token.len + } + + #[must_use] + pub fn peek(&self) -> lexer::TokenKind { + self.next_token.kind + } + + #[must_use] + pub fn pos(&self) -> u32 { + self.pos + } + + #[must_use] + pub fn at_end(&self) -> bool { + self.next_token.kind == lexer::TokenKind::Eof + } + + pub fn step(&mut self) { + // `next_token.len` is zero for the eof marker. + self.pos += self.next_token.len; + self.next_token = self.cursor.advance_token(); + } + + /// Consumes the next token if it matches the requested value and captures the value if + /// requested. Returns true if a token was matched. + fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool { + loop { + match (token, self.next_token.kind) { + (_, lexer::TokenKind::Whitespace) + | ( + Token::AnyComment, + lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. }, + ) => self.step(), + (Token::AnyComment, _) => return true, + (Token::Bang, lexer::TokenKind::Bang) + | (Token::CloseBrace, lexer::TokenKind::CloseBrace) + | (Token::CloseBracket, lexer::TokenKind::CloseBracket) + | (Token::CloseParen, lexer::TokenKind::CloseParen) + | (Token::Comma, lexer::TokenKind::Comma) + | (Token::Eq, lexer::TokenKind::Eq) + | (Token::Lifetime, lexer::TokenKind::Lifetime { .. }) + | (Token::Lt, lexer::TokenKind::Lt) + | (Token::Gt, lexer::TokenKind::Gt) + | (Token::OpenBrace, lexer::TokenKind::OpenBrace) + | (Token::OpenBracket, lexer::TokenKind::OpenBracket) + | (Token::OpenParen, lexer::TokenKind::OpenParen) + | (Token::Pound, lexer::TokenKind::Pound) + | (Token::Semi, lexer::TokenKind::Semi) + | ( + Token::LitStr, + lexer::TokenKind::Literal { + kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, + .. + }, + ) => { + self.step(); + return true; + }, + (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => { + self.step(); + return true; + }, + (Token::DoubleColon, lexer::TokenKind::Colon) => { + self.step(); + if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) { + self.step(); + return true; + } + return false; + }, + #[rustfmt::skip] + ( + Token::CaptureLitStr, + lexer::TokenKind::Literal { + kind: + lexer::LiteralKind::Str { terminated: true } + | lexer::LiteralKind::RawStr { n_hashes: Some(_) }, + .. + }, + ) + | (Token::CaptureIdent, lexer::TokenKind::Ident) => { + **captures.next().unwrap() = self.peek_text(); + self.step(); + return true; + }, + _ => return false, + } + } + } + + #[must_use] + pub fn find_token(&mut self, token: Token<'_>) -> bool { + let mut capture = [].iter_mut(); + while !self.read_token(token, &mut capture) { + self.step(); + if self.at_end() { + return false; + } + } + true + } + + #[must_use] + pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> { + let mut res = ""; + let mut capture = &mut res; + let mut capture = slice::from_mut(&mut capture).iter_mut(); + while !self.read_token(token, &mut capture) { + self.step(); + if self.at_end() { + return None; + } + } + Some(res) + } + + #[must_use] + pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool { + let mut captures = captures.iter_mut(); + tokens.iter().all(|&t| self.read_token(t, &mut captures)) + } +} + +pub struct Lint { + pub name: String, + pub group: String, + pub module: String, + pub path: PathBuf, + pub declaration_range: Range, +} + +pub struct DeprecatedLint { + pub name: String, + pub reason: String, + pub version: String, +} + +pub struct RenamedLint { + pub old_name: String, + pub new_name: String, + pub version: String, +} + +/// Finds all lint declarations (`declare_clippy_lint!`) +#[must_use] +pub fn find_lint_decls() -> Vec { + 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, "."); + if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() { + continue; + } + let Ok(mut name) = e.file_name().into_string() else { + continue; + }; + if name.starts_with("clippy_lints") && name != "clippy_lints_internal" { + name.push_str("/src"); + for (file, module) in read_src_with_module(name.as_ref()) { + parse_clippy_lint_decls( + file.path(), + File::open_read_to_cleared_string(file.path(), &mut contents), + &module, + &mut lints, + ); + } + } + } + lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + lints +} + +/// Reads the source files from the given root directory +fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator { + WalkDir::new(src_root).into_iter().filter_map(move |e| { + let e = expect_action(e, ErrAction::Read, src_root); + let path = e.path().as_os_str().as_encoded_bytes(); + if let Some(path) = path.strip_suffix(b".rs") + && let Some(path) = path.get(src_root.as_os_str().len() + 1..) + { + if path == b"lib" { + Some((e, String::new())) + } else { + let path = if let Some(path) = path.strip_suffix(b"mod") + && let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\")) + { + path + } else { + path + }; + if let Ok(path) = str::from_utf8(path) { + let path = path.replace(['/', '\\'], "::"); + Some((e, path)) + } else { + None + } + } + } else { + None + } + }) +} + +/// Parse a source file looking for `declare_clippy_lint` macro invocations. +fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec) { + #[allow(clippy::enum_glob_use)] + use Token::*; + #[rustfmt::skip] + static DECL_TOKENS: &[Token<'_>] = &[ + // !{ /// 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 searcher = RustSearcher::new(contents); + while searcher.find_token(Ident("declare_clippy_lint")) { + let start = searcher.pos() as usize - "declare_clippy_lint".len(); + let (mut name, mut group) = ("", ""); + if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) { + lints.push(Lint { + name: name.to_lowercase(), + group: group.into(), + module: module.into(), + path: path.into(), + declaration_range: start..searcher.pos() as usize, + }); + } + } +} + +#[must_use] +pub fn read_deprecated_lints() -> (Vec, Vec) { + #[allow(clippy::enum_glob_use)] + use Token::*; + #[rustfmt::skip] + static DECL_TOKENS: &[Token<'_>] = &[ + // #[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: &[Token<'_>] = &[ + // !{ DEPRECATED(DEPRECATED_VERSION) = [ + Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket, + ]; + #[rustfmt::skip] + static RENAMED_TOKENS: &[Token<'_>] = &[ + // !{ 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 searcher = RustSearcher::new(&contents); + + // First instance is the macro definition. + assert!( + searcher.find_token(Ident("declare_with_version")), + "error reading deprecated lints" + ); + + if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { + let mut version = ""; + let mut name = ""; + let mut reason = ""; + while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { + deprecated.push(DeprecatedLint { + name: parse_str_single_line(path.as_ref(), name), + reason: parse_str_single_line(path.as_ref(), reason), + version: parse_str_single_line(path.as_ref(), version), + }); + } + } else { + panic!("error reading deprecated lints"); + } + + if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) { + let mut version = ""; + let mut old_name = ""; + let mut new_name = ""; + while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { + renamed.push(RenamedLint { + old_name: parse_str_single_line(path.as_ref(), old_name), + new_name: parse_str_single_line(path.as_ref(), new_name), + version: parse_str_single_line(path.as_ref(), version), + }); + } + } 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(s: &str) -> String { + 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 { + s.into() + } else { + let mut res = String::with_capacity(s.len()); + rustc_literal_escaper::unescape_str(s, &mut |_, ch| { + if let Ok(ch) = ch { + res.push(ch); + } + }); + res + } +} + +fn parse_str_single_line(path: &Path, s: &str) -> String { + let value = parse_str_lit(s); + assert!( + !value.contains('\n'), + "error parsing `{}`: `{s}` should be a single line string", + path.display(), + ); + value +} diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs index d62597428e21..a46c03a61fee 100644 --- a/clippy_dev/src/rename_lint.rs +++ b/clippy_dev/src/rename_lint.rs @@ -1,7 +1,8 @@ -use crate::update_lints::{RenamedLint, find_lint_decls, generate_lint_files, read_deprecated_lints}; +use crate::parse::{RenamedLint, RustSearcher, Token, find_lint_decls, read_deprecated_lints}; +use crate::update_lints::generate_lint_files; use crate::utils::{ - ErrAction, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, - delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, + ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, + expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, }; use rustc_lexer::TokenKind; use std::ffi::OsString; diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs index 5f6e874ffe25..87f81ecd6a46 100644 --- a/clippy_dev/src/update_lints.rs +++ b/clippy_dev/src/update_lints.rs @@ -1,13 +1,9 @@ -use crate::utils::{ - ErrAction, File, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, expect_action, update_text_region_fn, -}; +use crate::parse::{DeprecatedLint, Lint, RenamedLint, RustSearcher, Token, find_lint_decls, read_deprecated_lints}; +use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn}; use itertools::Itertools; use std::collections::HashSet; use std::fmt::Write; -use std::fs; -use std::ops::Range; -use std::path::{self, Path, PathBuf}; -use walkdir::{DirEntry, WalkDir}; +use std::path::{self, Path}; const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\ // Use that command to update this file and do not edit by hand.\n\ @@ -200,260 +196,3 @@ pub fn generate_lint_files( fn round_to_fifty(count: usize) -> usize { count / 50 * 50 } - -/// Lint data parsed from the Clippy source code. -#[derive(PartialEq, Eq, Debug)] -pub struct Lint { - pub name: String, - pub group: String, - pub module: String, - pub path: PathBuf, - pub declaration_range: Range, -} - -pub struct DeprecatedLint { - pub name: String, - pub reason: String, - pub version: String, -} - -pub struct RenamedLint { - pub old_name: String, - pub new_name: String, - pub version: String, -} - -/// Finds all lint declarations (`declare_clippy_lint!`) -#[must_use] -pub fn find_lint_decls() -> Vec { - 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, "."); - if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() { - continue; - } - let Ok(mut name) = e.file_name().into_string() else { - continue; - }; - if name.starts_with("clippy_lints") && name != "clippy_lints_internal" { - name.push_str("/src"); - for (file, module) in read_src_with_module(name.as_ref()) { - parse_clippy_lint_decls( - file.path(), - File::open_read_to_cleared_string(file.path(), &mut contents), - &module, - &mut lints, - ); - } - } - } - lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); - lints -} - -/// Reads the source files from the given root directory -fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator { - WalkDir::new(src_root).into_iter().filter_map(move |e| { - let e = expect_action(e, ErrAction::Read, src_root); - let path = e.path().as_os_str().as_encoded_bytes(); - if let Some(path) = path.strip_suffix(b".rs") - && let Some(path) = path.get(src_root.as_os_str().len() + 1..) - { - if path == b"lib" { - Some((e, String::new())) - } else { - let path = if let Some(path) = path.strip_suffix(b"mod") - && let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\")) - { - path - } else { - path - }; - if let Ok(path) = str::from_utf8(path) { - let path = path.replace(['/', '\\'], "::"); - Some((e, path)) - } else { - None - } - } - } else { - None - } - }) -} - -/// Parse a source file looking for `declare_clippy_lint` macro invocations. -fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec) { - #[allow(clippy::enum_glob_use)] - use Token::*; - #[rustfmt::skip] - static DECL_TOKENS: &[Token<'_>] = &[ - // !{ /// 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 searcher = RustSearcher::new(contents); - while searcher.find_token(Ident("declare_clippy_lint")) { - let start = searcher.pos() as usize - "declare_clippy_lint".len(); - let (mut name, mut group) = ("", ""); - if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) { - lints.push(Lint { - name: name.to_lowercase(), - group: group.into(), - module: module.into(), - path: path.into(), - declaration_range: start..searcher.pos() as usize, - }); - } - } -} - -#[must_use] -pub fn read_deprecated_lints() -> (Vec, Vec) { - #[allow(clippy::enum_glob_use)] - use Token::*; - #[rustfmt::skip] - static DECL_TOKENS: &[Token<'_>] = &[ - // #[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: &[Token<'_>] = &[ - // !{ DEPRECATED(DEPRECATED_VERSION) = [ - Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket, - ]; - #[rustfmt::skip] - static RENAMED_TOKENS: &[Token<'_>] = &[ - // !{ 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 searcher = RustSearcher::new(&contents); - - // First instance is the macro definition. - assert!( - searcher.find_token(Ident("declare_with_version")), - "error reading deprecated lints" - ); - - if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { - let mut version = ""; - let mut name = ""; - let mut reason = ""; - while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { - deprecated.push(DeprecatedLint { - name: parse_str_single_line(path.as_ref(), name), - reason: parse_str_single_line(path.as_ref(), reason), - version: parse_str_single_line(path.as_ref(), version), - }); - } - } else { - panic!("error reading deprecated lints"); - } - - if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) { - let mut version = ""; - let mut old_name = ""; - let mut new_name = ""; - while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { - renamed.push(RenamedLint { - old_name: parse_str_single_line(path.as_ref(), old_name), - new_name: parse_str_single_line(path.as_ref(), new_name), - version: parse_str_single_line(path.as_ref(), version), - }); - } - } 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(s: &str) -> String { - let s = s.strip_prefix("r").unwrap_or(s).trim_matches('#'); - let s = s - .strip_prefix('"') - .and_then(|s| s.strip_suffix('"')) - .unwrap_or_else(|| panic!("expected quoted string, found `{s}`")); - let mut res = String::with_capacity(s.len()); - rustc_literal_escaper::unescape_str(s, &mut |_, ch| { - if let Ok(ch) = ch { - res.push(ch); - } - }); - res -} - -fn parse_str_single_line(path: &Path, s: &str) -> String { - let value = parse_str_lit(s); - assert!( - !value.contains('\n'), - "error parsing `{}`: `{s}` should be a single line string", - path.display(), - ); - value -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_clippy_lint_decls() { - static CONTENTS: &str = r#" - declare_clippy_lint! { - #[clippy::version = "Hello Clippy!"] - pub PTR_ARG, - style, - "really long \ - text" - } - - declare_clippy_lint!{ - #[clippy::version = "Test version"] - pub DOC_MARKDOWN, - pedantic, - "single line" - } - "#; - let mut result = Vec::new(); - parse_clippy_lint_decls("".as_ref(), CONTENTS, "module_name", &mut result); - for r in &mut result { - r.declaration_range = Range::default(); - } - - let expected = vec![ - Lint { - name: "ptr_arg".into(), - group: "style".into(), - module: "module_name".into(), - path: PathBuf::new(), - declaration_range: Range::default(), - }, - Lint { - name: "doc_markdown".into(), - group: "pedantic".into(), - module: "module_name".into(), - path: PathBuf::new(), - declaration_range: Range::default(), - }, - ]; - assert_eq!(expected, result); - } -} diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index 89962a110341..fc1a07a14314 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -1,9 +1,7 @@ use core::fmt::{self, Display}; use core::num::NonZero; use core::ops::Range; -use core::slice; use core::str::FromStr; -use rustc_lexer::{self as lexer, FrontmatterAllowed}; use std::ffi::OsStr; use std::fs::{self, OpenOptions}; use std::io::{self, Read as _, Seek as _, SeekFrom, Write}; @@ -426,181 +424,6 @@ pub fn update_text_region_fn( move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert) } -#[derive(Clone, Copy)] -pub enum Token<'a> { - /// Matches any number of comments / doc comments. - AnyComment, - Ident(&'a str), - CaptureIdent, - LitStr, - CaptureLitStr, - Bang, - CloseBrace, - CloseBracket, - CloseParen, - /// This will consume the first colon even if the second doesn't exist. - DoubleColon, - Comma, - Eq, - Lifetime, - Lt, - Gt, - OpenBrace, - OpenBracket, - OpenParen, - Pound, - Semi, - Slash, -} - -pub struct RustSearcher<'txt> { - text: &'txt str, - cursor: lexer::Cursor<'txt>, - pos: u32, - next_token: lexer::Token, -} -impl<'txt> RustSearcher<'txt> { - #[must_use] - #[expect(clippy::inconsistent_struct_constructor)] - pub fn new(text: &'txt str) -> Self { - let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes); - Self { - text, - pos: 0, - next_token: cursor.advance_token(), - cursor, - } - } - - #[must_use] - pub fn peek_text(&self) -> &'txt str { - &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] - } - - #[must_use] - pub fn peek_len(&self) -> u32 { - self.next_token.len - } - - #[must_use] - pub fn peek(&self) -> lexer::TokenKind { - self.next_token.kind - } - - #[must_use] - pub fn pos(&self) -> u32 { - self.pos - } - - #[must_use] - pub fn at_end(&self) -> bool { - self.next_token.kind == lexer::TokenKind::Eof - } - - pub fn step(&mut self) { - // `next_len` is zero for the sentinel value and the eof marker. - self.pos += self.next_token.len; - self.next_token = self.cursor.advance_token(); - } - - /// Consumes the next token if it matches the requested value and captures the value if - /// requested. Returns true if a token was matched. - fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool { - loop { - match (token, self.next_token.kind) { - (_, lexer::TokenKind::Whitespace) - | ( - Token::AnyComment, - lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. }, - ) => self.step(), - (Token::AnyComment, _) => return true, - (Token::Bang, lexer::TokenKind::Bang) - | (Token::CloseBrace, lexer::TokenKind::CloseBrace) - | (Token::CloseBracket, lexer::TokenKind::CloseBracket) - | (Token::CloseParen, lexer::TokenKind::CloseParen) - | (Token::Comma, lexer::TokenKind::Comma) - | (Token::Eq, lexer::TokenKind::Eq) - | (Token::Lifetime, lexer::TokenKind::Lifetime { .. }) - | (Token::Lt, lexer::TokenKind::Lt) - | (Token::Gt, lexer::TokenKind::Gt) - | (Token::OpenBrace, lexer::TokenKind::OpenBrace) - | (Token::OpenBracket, lexer::TokenKind::OpenBracket) - | (Token::OpenParen, lexer::TokenKind::OpenParen) - | (Token::Pound, lexer::TokenKind::Pound) - | (Token::Semi, lexer::TokenKind::Semi) - | (Token::Slash, lexer::TokenKind::Slash) - | ( - Token::LitStr, - lexer::TokenKind::Literal { - kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, - .. - }, - ) => { - self.step(); - return true; - }, - (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => { - self.step(); - return true; - }, - (Token::DoubleColon, lexer::TokenKind::Colon) => { - self.step(); - if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) { - self.step(); - return true; - } - return false; - }, - ( - Token::CaptureLitStr, - lexer::TokenKind::Literal { - kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, - .. - }, - ) - | (Token::CaptureIdent, lexer::TokenKind::Ident) => { - **captures.next().unwrap() = self.peek_text(); - self.step(); - return true; - }, - _ => return false, - } - } - } - - #[must_use] - pub fn find_token(&mut self, token: Token<'_>) -> bool { - let mut capture = [].iter_mut(); - while !self.read_token(token, &mut capture) { - self.step(); - if self.at_end() { - return false; - } - } - true - } - - #[must_use] - pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> { - let mut res = ""; - let mut capture = &mut res; - let mut capture = slice::from_mut(&mut capture).iter_mut(); - while !self.read_token(token, &mut capture) { - self.step(); - if self.at_end() { - return None; - } - } - Some(res) - } - - #[must_use] - pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool { - let mut captures = captures.iter_mut(); - tokens.iter().all(|&t| self.read_token(t, &mut captures)) - } -} - #[expect(clippy::must_use_candidate)] pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool { match OpenOptions::new().create_new(true).write(true).open(new_name) { From 2ffe004daa7a4aad54648d422fdec222ef9d2949 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Fri, 6 Jun 2025 13:28:46 -0400 Subject: [PATCH 2/6] `clippy_dev` parsing changes: * Keep results together. * Keep lints in a map to detect duplicate names. * Store the crate a lint comes from. --- clippy_dev/src/deprecate_lint.rs | 175 ++++++-------- clippy_dev/src/fmt.rs | 2 +- clippy_dev/src/lib.rs | 6 +- clippy_dev/src/parse.rs | 364 ++++++++++++++++------------- clippy_dev/src/rename_lint.rs | 170 +++++++------- clippy_dev/src/update_lints.rs | 168 ++++++------- clippy_dev/src/utils.rs | 4 +- clippy_lints/src/declared_lints.rs | 30 +-- tests/ui/rename.fixed | 61 ----- tests/ui/rename.rs | 61 ----- tests/ui/rename.stderr | 146 ++++++------ 11 files changed, 544 insertions(+), 643 deletions(-) diff --git a/clippy_dev/src/deprecate_lint.rs b/clippy_dev/src/deprecate_lint.rs index 9f6d1984eb57..c1fed600014d 100644 --- a/clippy_dev/src/deprecate_lint.rs +++ b/clippy_dev/src/deprecate_lint.rs @@ -1,9 +1,10 @@ -use crate::parse::{DeprecatedLint, Lint, find_lint_decls, read_deprecated_lints}; +use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintList}; use crate::update_lints::generate_lint_files; -use crate::utils::{UpdateMode, Version}; +use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists}; +use core::mem; +use rustc_data_structures::fx::FxHashMap; use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::{fs, io}; +use std::path::Path; /// Runs the `deprecate` command /// @@ -14,73 +15,48 @@ use std::{fs, io}; /// # Panics /// /// If a file path could not read from or written to +#[expect(clippy::similar_names)] pub fn deprecate(clippy_version: Version, name: &str, reason: &str) { if let Some((prefix, _)) = name.split_once("::") { panic!("`{name}` should not contain the `{prefix}` prefix"); } - let mut lints = find_lint_decls(); - let (mut deprecated_lints, renamed_lints) = read_deprecated_lints(); - - let Some(lint) = lints.iter().find(|l| l.name == name) else { + let mut list = LintList::collect(); + let Some(entry) = list.lints.get_mut(name) else { eprintln!("error: failed to find lint `{name}`"); return; }; - - let prefixed_name = String::from_iter(["clippy::", name]); - match deprecated_lints.binary_search_by(|x| x.name.cmp(&prefixed_name)) { - Ok(_) => { - println!("`{name}` is already deprecated"); - return; - }, - Err(idx) => deprecated_lints.insert( - idx, - DeprecatedLint { - name: prefixed_name, - reason: reason.into(), - version: clippy_version.rust_display().to_string(), - }, - ), - } - - let mod_path = { - let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module)); - if mod_path.is_dir() { - mod_path = mod_path.join("mod"); - } - - mod_path.set_extension("rs"); - mod_path + let Lint::Active(lint) = mem::replace( + entry, + Lint::Deprecated(DeprecatedLint { + reason: reason.into(), + version: clippy_version.rust_display().to_string(), + }), + ) else { + eprintln!("error: lint `{name}` is already deprecated"); + return; }; - if remove_lint_declaration(name, &mod_path, &mut lints).unwrap_or(false) { - generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints); - println!("info: `{name}` has successfully been deprecated"); - println!("note: you must run `cargo uitest` to update the test results"); - } else { - eprintln!("error: lint not found"); - } + remove_lint_declaration(name, &lint, &list.lints); + generate_lint_files(UpdateMode::Change, &list); + println!("info: `{name}` has successfully been deprecated"); + println!("note: you must run `cargo uitest` to update the test results"); } -fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec) -> io::Result { - fn remove_lint(name: &str, lints: &mut Vec) { - lints.iter().position(|l| l.name == name).map(|pos| lints.remove(pos)); - } - +fn remove_lint_declaration(name: &str, lint: &ActiveLint, lints: &FxHashMap) { fn remove_test_assets(name: &str) { let test_file_stem = format!("tests/ui/{name}"); let path = Path::new(&test_file_stem); // Some lints have their own directories, delete them if path.is_dir() { - let _ = fs::remove_dir_all(path); - return; + delete_dir_if_exists(path); + } else { + // Remove all related test files + delete_file_if_exists(&path.with_extension("rs")); + delete_file_if_exists(&path.with_extension("stderr")); + delete_file_if_exists(&path.with_extension("fixed")); } - - // Remove all related test files - let _ = fs::remove_file(path.with_extension("rs")); - let _ = fs::remove_file(path.with_extension("stderr")); - let _ = fs::remove_file(path.with_extension("fixed")); } fn remove_impl_lint_pass(lint_name_upper: &str, content: &mut String) { @@ -109,54 +85,55 @@ fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec) -> io } } - if path.exists() - && let Some(lint) = lints.iter().find(|l| l.name == name) - { - if lint.module == name { - // The lint name is the same as the file, we can just delete the entire file - fs::remove_file(path)?; + if lints.values().any(|l| { + if let Lint::Active(l) = l { + l.krate == lint.krate && l.module.starts_with(&lint.module) } else { - // We can't delete the entire file, just remove the declaration - - if let Some(Some("mod.rs")) = path.file_name().map(OsStr::to_str) { - // Remove clippy_lints/src/some_mod/some_lint.rs - let mut lint_mod_path = path.to_path_buf(); - lint_mod_path.set_file_name(name); - lint_mod_path.set_extension("rs"); - - let _ = fs::remove_file(lint_mod_path); - } - - let mut content = - fs::read_to_string(path).unwrap_or_else(|_| panic!("failed to read `{}`", path.to_string_lossy())); - - eprintln!( - "warn: you will have to manually remove any code related to `{name}` from `{}`", - path.display() - ); - - assert!( - content[lint.declaration_range.clone()].contains(&name.to_uppercase()), - "error: `{}` does not contain lint `{}`'s declaration", - path.display(), - lint.name - ); - - // Remove lint declaration (declare_clippy_lint!) - content.replace_range(lint.declaration_range.clone(), ""); - - // Remove the module declaration (mod xyz;) - let mod_decl = format!("\nmod {name};"); - content = content.replacen(&mod_decl, "", 1); - - remove_impl_lint_pass(&lint.name.to_uppercase(), &mut content); - fs::write(path, content).unwrap_or_else(|_| panic!("failed to write to `{}`", path.to_string_lossy())); + false } - - remove_test_assets(name); - remove_lint(name, lints); - return Ok(true); + }) { + // Try to delete a sub-module that matches the lint's name + let removed_mod = if lint.path.file_name().map(OsStr::as_encoded_bytes) == Some(b"mod.rs") { + let mut path = lint.path.clone(); + path.set_file_name(name); + path.set_extension("rs"); + delete_file_if_exists(&path) + } else { + false + }; + + FileUpdater::default().update_file(&lint.path, &mut |_, src, dst| { + let (a, b, c, d) = if removed_mod + && let mod_decl = format!("\nmod {name};") + && let Some(mod_start) = src.find(&mod_decl) + { + if mod_start < lint.span.start as usize { + ( + mod_start, + mod_start + mod_decl.len(), + lint.span.start as usize, + lint.span.end as usize, + ) + } else { + ( + lint.span.start as usize, + lint.span.end as usize, + mod_start, + mod_start + mod_decl.len(), + ) + } + } else { + (lint.span.start as usize, lint.span.end as usize, src.len(), src.len()) + }; + dst.push_str(&src[..a]); + dst.push_str(&src[b..c]); + dst.push_str(&src[d..]); + remove_impl_lint_pass(&name.to_uppercase(), dst); + UpdateStatus::Changed + }); + } else { + // No other lint in the same module or a sub-module. + delete_file_if_exists(&lint.path); } - - Ok(false) + remove_test_assets(name); } diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index bd9e57c9f6da..016a57a12862 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -268,7 +268,7 @@ fn run_rustfmt(update_mode: UpdateMode) { .expect("invalid rustfmt path"); rustfmt_path.truncate(rustfmt_path.trim_end().len()); - let args: Vec<_> = walk_dir_no_dot_or_target() + let args: Vec<_> = walk_dir_no_dot_or_target(".") .filter_map(|e| { let e = expect_action(e, ErrAction::Read, "."); e.path() diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs index 578753735c8e..ee3ab204b10f 100644 --- a/clippy_dev/src/lib.rs +++ b/clippy_dev/src/lib.rs @@ -1,5 +1,6 @@ #![feature( rustc_private, + array_windows, exit_status_error, if_let_guard, let_chains, @@ -16,9 +17,10 @@ )] #![allow(clippy::missing_panics_doc)] -// The `rustc_driver` crate seems to be required in order to use the `rust_lexer` crate. -#[allow(unused_extern_crates)] +#[expect(unused_extern_crates, reason = "needed to lint to rustc crates")] extern crate rustc_driver; + +extern crate rustc_data_structures; extern crate rustc_lexer; extern crate rustc_literal_escaper; diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 2b2e5fcf9131..07be3cb30b89 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -1,10 +1,10 @@ -use crate::utils::{ErrAction, File, expect_action}; +use crate::utils::{ErrAction, File, expect_action, walk_dir_no_dot_or_target}; use core::ops::Range; use core::slice; +use rustc_data_structures::fx::FxHashMap; use rustc_lexer::{self as lexer, FrontmatterAllowed}; use std::fs; -use std::path::{Path, PathBuf}; -use walkdir::{DirEntry, WalkDir}; +use std::path::{self, Path, PathBuf}; #[derive(Clone, Copy)] pub enum Token<'a> { @@ -76,6 +76,7 @@ impl<'txt> RustSearcher<'txt> { self.next_token.kind == lexer::TokenKind::Eof } + /// Steps to the next token, or `TokenKind::Eof` if there are no more tokens. pub fn step(&mut self) { // `next_token.len` is zero for the eof marker. self.pos += self.next_token.len; @@ -83,7 +84,7 @@ impl<'txt> RustSearcher<'txt> { } /// Consumes the next token if it matches the requested value and captures the value if - /// requested. Returns true if a token was matched. + /// requested. Returns `true` if a token was matched. fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool { loop { match (token, self.next_token.kind) { @@ -123,7 +124,7 @@ impl<'txt> RustSearcher<'txt> { }, (Token::DoubleColon, lexer::TokenKind::Colon) => { self.step(); - if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) { + if matches!(self.next_token.kind, lexer::TokenKind::Colon) { self.step(); return true; } @@ -182,188 +183,220 @@ impl<'txt> RustSearcher<'txt> { } } -pub struct Lint { - pub name: String, - pub group: String, +pub struct ActiveLint { + pub krate: String, pub module: String, + pub group: String, pub path: PathBuf, - pub declaration_range: Range, + pub span: Range, } pub struct DeprecatedLint { - pub name: String, pub reason: String, pub version: String, } pub struct RenamedLint { - pub old_name: String, pub new_name: String, pub version: String, } -/// Finds all lint declarations (`declare_clippy_lint!`) -#[must_use] -pub fn find_lint_decls() -> Vec { - 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, "."); - if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() { - continue; - } - let Ok(mut name) = e.file_name().into_string() else { - continue; +pub enum Lint { + Active(ActiveLint), + Deprecated(DeprecatedLint), + Renamed(RenamedLint), +} + +pub struct LintList { + pub lints: FxHashMap, + pub deprecated_span: Range, + pub renamed_span: Range, +} +impl LintList { + #[expect(clippy::default_trait_access)] + pub fn collect() -> Self { + // 2025-05: Initial capacities should fit everything without reallocating. + let mut parser = Parser { + lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()), + deprecated_span: 0..0, + renamed_span: 0..0, + contents: String::with_capacity(1024 * 128 * 3), }; - if name.starts_with("clippy_lints") && name != "clippy_lints_internal" { - name.push_str("/src"); - for (file, module) in read_src_with_module(name.as_ref()) { - parse_clippy_lint_decls( - file.path(), - File::open_read_to_cleared_string(file.path(), &mut contents), - &module, - &mut lints, - ); - } + parser.parse_src_files(); + parser.parse_deprecated_lints(); + + LintList { + lints: parser.lints, + deprecated_span: parser.deprecated_span, + renamed_span: parser.renamed_span, } } - lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); - lints } -/// Reads the source files from the given root directory -fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator { - WalkDir::new(src_root).into_iter().filter_map(move |e| { - let e = expect_action(e, ErrAction::Read, src_root); - let path = e.path().as_os_str().as_encoded_bytes(); - if let Some(path) = path.strip_suffix(b".rs") - && let Some(path) = path.get(src_root.as_os_str().len() + 1..) - { - if path == b"lib" { - Some((e, String::new())) - } else { - let path = if let Some(path) = path.strip_suffix(b"mod") - && let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\")) - { - path - } else { - path - }; - if let Ok(path) = str::from_utf8(path) { - let path = path.replace(['/', '\\'], "::"); - Some((e, path)) - } else { - None +struct Parser { + lints: FxHashMap, + deprecated_span: Range, + renamed_span: Range, + contents: String, +} +impl Parser { + /// Parses all source files looking for lint declarations (`declare_clippy_lint! { .. }`). + fn parse_src_files(&mut self) { + for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") { + let e = expect_action(e, ErrAction::Read, "."); + if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() { + continue; + } + let Ok(mut crate_path) = e.file_name().into_string() else { + continue; + }; + if crate_path.starts_with("clippy_lints") && crate_path != "clippy_lints_internal" { + crate_path.push_str("/src"); + let krate = &crate_path[..crate_path.len() - 4]; + 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" { + String::new() + } else { + let path = if let Some(path) = path.strip_suffix("mod") + && let Some(path) = path.strip_suffix(path::MAIN_SEPARATOR) + { + path + } else { + path + }; + path.replace(path::MAIN_SEPARATOR, "::") + }; + self.parse_src_file(e.path(), krate, &module); + } } } - } else { - None } - }) -} + } + + /// Parse a source file looking for `declare_clippy_lint` macro invocations. + fn parse_src_file(&mut self, path: &Path, krate: &str, module: &str) { + #[allow(clippy::enum_glob_use)] + use Token::*; + #[rustfmt::skip] + static DECL_TOKENS: &[Token<'_>] = &[ + // !{ /// 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, + ]; -/// Parse a source file looking for `declare_clippy_lint` macro invocations. -fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec) { - #[allow(clippy::enum_glob_use)] - use Token::*; - #[rustfmt::skip] - static DECL_TOKENS: &[Token<'_>] = &[ - // !{ /// 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 searcher = RustSearcher::new(contents); - while searcher.find_token(Ident("declare_clippy_lint")) { - let start = searcher.pos() as usize - "declare_clippy_lint".len(); - let (mut name, mut group) = ("", ""); - if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) { - lints.push(Lint { - name: name.to_lowercase(), - group: group.into(), - module: module.into(), - path: path.into(), - declaration_range: start..searcher.pos() as usize, - }); + File::open_read_to_cleared_string(path, &mut self.contents); + let mut searcher = RustSearcher::new(&self.contents); + #[expect(clippy::cast_possible_truncation)] + while searcher.find_token(Ident("declare_clippy_lint")) { + let start = searcher.pos() - "declare_clippy_lint".len() as u32; + let (mut name, mut group) = ("", ""); + if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) { + self.lints.insert( + name.to_ascii_lowercase(), + Lint::Active(ActiveLint { + krate: krate.into(), + module: module.into(), + group: group.into(), + path: path.into(), + span: start..searcher.pos(), + }), + ); + } } } -} -#[must_use] -pub fn read_deprecated_lints() -> (Vec, Vec) { - #[allow(clippy::enum_glob_use)] - use Token::*; - #[rustfmt::skip] - static DECL_TOKENS: &[Token<'_>] = &[ - // #[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: &[Token<'_>] = &[ - // !{ DEPRECATED(DEPRECATED_VERSION) = [ - Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket, - ]; - #[rustfmt::skip] - static RENAMED_TOKENS: &[Token<'_>] = &[ - // !{ 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 searcher = RustSearcher::new(&contents); - - // First instance is the macro definition. - assert!( - searcher.find_token(Ident("declare_with_version")), - "error reading deprecated lints" - ); + pub fn parse_deprecated_lints(&mut self) { + #[allow(clippy::enum_glob_use)] + use Token::*; + #[rustfmt::skip] + static DECL_TOKENS: &[Token<'_>] = &[ + // #[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: &[Token<'_>] = &[ + // !{ DEPRECATED(DEPRECATED_VERSION) = [ + Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket, + ]; + #[rustfmt::skip] + static RENAMED_TOKENS: &[Token<'_>] = &[ + // !{ RENAMED(RENAMED_VERSION) = [ + Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket, + ]; - if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { - let mut version = ""; - let mut name = ""; - let mut reason = ""; - while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { - deprecated.push(DeprecatedLint { - name: parse_str_single_line(path.as_ref(), name), - reason: parse_str_single_line(path.as_ref(), reason), - version: parse_str_single_line(path.as_ref(), version), - }); + let path = Path::new("clippy_lints/src/deprecated_lints.rs"); + File::open_read_to_cleared_string(path, &mut self.contents); + + let mut searcher = RustSearcher::new(&self.contents); + // First instance is the macro definition. + assert!( + searcher.find_token(Ident("declare_with_version")), + "error parsing `clippy_lints/src/deprecated_lints.rs`" + ); + + if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { + let start = searcher.pos(); + let mut end = start; + let mut version = ""; + let mut name = ""; + let mut reason = ""; + while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { + self.lints.insert( + parse_clippy_lint_name(path, name), + Lint::Deprecated(DeprecatedLint { + reason: parse_str_single_line(path, reason), + version: parse_str_single_line(path, version), + }), + ); + end = searcher.pos(); + } + self.deprecated_span = start..end; + } else { + panic!("error reading deprecated lints"); } - } else { - panic!("error reading deprecated lints"); - } - if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) { - let mut version = ""; - let mut old_name = ""; - let mut new_name = ""; - while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { - renamed.push(RenamedLint { - old_name: parse_str_single_line(path.as_ref(), old_name), - new_name: parse_str_single_line(path.as_ref(), new_name), - version: parse_str_single_line(path.as_ref(), version), - }); + if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) { + let start = searcher.pos(); + let mut end = start; + let mut version = ""; + let mut old_name = ""; + let mut new_name = ""; + while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { + self.lints.insert( + parse_clippy_lint_name(path, old_name), + Lint::Renamed(RenamedLint { + new_name: parse_maybe_clippy_lint_name(path, new_name), + version: parse_str_single_line(path, version), + }), + ); + end = searcher.pos(); + } + self.renamed_span = start..end; + } else { + panic!("error reading renamed lints"); } - } 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) +fn assert_lint_name(path: &Path, s: &str) { + assert!( + s.bytes() + .all(|c| matches!(c, b'_' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')), + "error parsing `{}`: `{s}` is not a valid lint name", + path.display(), + ); } -/// Removes the line splices and surrounding quotes from a string literal fn parse_str_lit(s: &str) -> String { let (s, is_raw) = if let Some(s) = s.strip_prefix("r") { (s.trim_matches('#'), true) @@ -389,11 +422,30 @@ fn parse_str_lit(s: &str) -> String { } fn parse_str_single_line(path: &Path, s: &str) -> String { - let value = parse_str_lit(s); + let s = parse_str_lit(s); assert!( - !value.contains('\n'), + !s.contains('\n'), "error parsing `{}`: `{s}` should be a single line string", path.display(), ); - value + s +} + +fn parse_clippy_lint_name(path: &Path, s: &str) -> String { + let mut s = parse_str_lit(s); + let Some(name) = s.strip_prefix("clippy::") else { + panic!( + "error parsing `{}`: `{s}` needs to have the `clippy::` prefix", + path.display() + ); + }; + assert_lint_name(path, name); + s.drain(.."clippy::".len()); + s +} + +fn parse_maybe_clippy_lint_name(path: &Path, s: &str) -> String { + let s = parse_str_lit(s); + assert_lint_name(path, s.strip_prefix("clippy::").unwrap_or(&s)); + s } diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs index a46c03a61fee..b770d18d8cf5 100644 --- a/clippy_dev/src/rename_lint.rs +++ b/clippy_dev/src/rename_lint.rs @@ -1,9 +1,11 @@ -use crate::parse::{RenamedLint, RustSearcher, Token, find_lint_decls, read_deprecated_lints}; +use crate::parse::{Lint, LintList, RenamedLint, RustSearcher, Token}; use crate::update_lints::generate_lint_files; use crate::utils::{ ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, }; +use core::mem; +use rustc_data_structures::fx::{FxHashMap, StdEntry}; use rustc_lexer::TokenKind; use std::ffi::OsString; use std::fs; @@ -24,7 +26,7 @@ use std::path::Path; /// * If either lint name has a prefix /// * If `old_name` doesn't name an existing lint. /// * If `old_name` names a deprecated or renamed lint. -#[expect(clippy::too_many_lines)] +#[expect(clippy::too_many_lines, clippy::similar_names)] pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: bool) { if let Some((prefix, _)) = old_name.split_once("::") { panic!("`{old_name}` should not contain the `{prefix}` prefix"); @@ -34,67 +36,58 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b } let mut updater = FileUpdater::default(); - let mut lints = find_lint_decls(); - let (deprecated_lints, mut renamed_lints) = read_deprecated_lints(); - - let Ok(lint_idx) = lints.binary_search_by(|x| x.name.as_str().cmp(old_name)) else { - panic!("could not find lint `{old_name}`"); - }; - let lint = &lints[lint_idx]; + let mut list = LintList::collect(); - let old_name_prefixed = String::from_iter(["clippy::", old_name]); + // Update any existing renames let new_name_prefixed = if uplift { new_name.to_owned() } else { String::from_iter(["clippy::", new_name]) }; - - for lint in &mut renamed_lints { - if lint.new_name == old_name_prefixed { + for lint in list.lints.values_mut() { + if let Lint::Renamed(lint) = lint + && lint.new_name.strip_prefix("clippy::") == Some(old_name) + { lint.new_name.clone_from(&new_name_prefixed); } } - match renamed_lints.binary_search_by(|x| x.old_name.cmp(&old_name_prefixed)) { - Ok(_) => { - println!("`{old_name}` already has a rename registered"); - return; - }, - Err(idx) => { - renamed_lints.insert( - idx, - RenamedLint { - old_name: old_name_prefixed, - new_name: if uplift { - new_name.to_owned() - } else { - String::from_iter(["clippy::", new_name]) - }, - version: clippy_version.rust_display().to_string(), - }, - ); - }, - } - // Some tests are named `lint_name_suffix` which should also be renamed, - // but we can't do that if the renamed lint's name overlaps with another - // lint. e.g. renaming 'foo' to 'bar' when a lint 'foo_bar' also exists. - let change_prefixed_tests = lints.get(lint_idx + 1).is_none_or(|l| !l.name.starts_with(old_name)); + // Mark the lint as renamed + let Some(old_entry) = list.lints.get_mut(old_name) else { + eprintln!("error: failed to find lint `{old_name}`"); + return; + }; + let Lint::Active(mut lint) = mem::replace( + old_entry, + Lint::Renamed(RenamedLint { + new_name: new_name_prefixed, + version: clippy_version.rust_display().to_string(), + }), + ) else { + eprintln!("error: lint `{old_name}` is already deprecated"); + return; + }; let mut mod_edit = ModEdit::None; if uplift { - let is_unique_mod = lints[..lint_idx].iter().any(|l| l.module == lint.module) - || lints[lint_idx + 1..].iter().any(|l| l.module == lint.module); + let is_unique_mod = list.lints.values().any(|x| { + if let Lint::Active(x) = x { + x.module == lint.module + } else { + false + } + }); if is_unique_mod { if delete_file_if_exists(lint.path.as_ref()) { mod_edit = ModEdit::Delete; } } else { updater.update_file(&lint.path, &mut |_, src, dst| -> UpdateStatus { - let mut start = &src[..lint.declaration_range.start]; + let mut start = &src[..lint.span.start as usize]; if start.ends_with("\n\n") { start = &start[..start.len() - 1]; } - let mut end = &src[lint.declaration_range.end..]; + let mut end = &src[lint.span.end as usize..]; if end.starts_with("\n\n") { end = &end[1..]; } @@ -103,10 +96,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b UpdateStatus::Changed }); } - delete_test_files(old_name, change_prefixed_tests); - lints.remove(lint_idx); - } else if lints.binary_search_by(|x| x.name.as_str().cmp(new_name)).is_err() { - let lint = &mut lints[lint_idx]; + delete_test_files(old_name, &list.lints); + } else if let StdEntry::Vacant(entry) = list.lints.entry(new_name.to_owned()) { if lint.module.ends_with(old_name) && lint .path @@ -116,6 +107,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b let mut new_path = lint.path.with_file_name(new_name).into_os_string(); new_path.push(".rs"); if try_rename_file(lint.path.as_ref(), new_path.as_ref()) { + lint.path = new_path.into(); mod_edit = ModEdit::Rename; } @@ -123,9 +115,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b lint.module.truncate(mod_len - old_name.len()); lint.module.push_str(new_name); } - rename_test_files(old_name, new_name, change_prefixed_tests); - new_name.clone_into(&mut lints[lint_idx].name); - lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + entry.insert(Lint::Active(lint)); + rename_test_files(old_name, new_name, &list.lints); } else { println!("Renamed `clippy::{old_name}` to `clippy::{new_name}`"); println!("Since `{new_name}` already exists the existing code has not been changed"); @@ -133,13 +124,13 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b } let mut update_fn = file_update_fn(old_name, new_name, mod_edit); - for e in walk_dir_no_dot_or_target() { + for e in walk_dir_no_dot_or_target(".") { let e = expect_action(e, ErrAction::Read, "."); if e.path().as_os_str().as_encoded_bytes().ends_with(b".rs") { updater.update_file(e.path(), &mut update_fn); } } - generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints); + generate_lint_files(UpdateMode::Change, &list); if uplift { println!("Uplifted `clippy::{old_name}` as `{new_name}`"); @@ -164,47 +155,67 @@ enum ModEdit { Rename, } -fn collect_ui_test_names(lint: &str, rename_prefixed: bool, dst: &mut Vec<(OsString, bool)>) { +fn is_lint_test(lint: &str, name: &str, lints: &FxHashMap) -> bool { + let is_num = |c: char| c.is_ascii_digit(); + let Some(suffix) = name.strip_prefix(lint) else { + return false; + }; + let suffix = suffix.trim_start_matches(is_num); + let Some(suffix) = suffix.strip_prefix('_') else { + return true; + }; + + // Some lint names are a prefix of other lint names. Make sure we don't mix test files + // between the two lints. + !(lints.contains_key(name.trim_end_matches(is_num)) + || suffix + .bytes() + .zip(suffix.len() - name.len()..) + .any(|(c, i)| c == b'_' && lints.contains_key(name[..i].trim_end_matches(is_num)))) +} + +fn collect_ui_test_names(lint: &str, lints: &FxHashMap, dst: &mut Vec<(String, bool)>) { for e in fs::read_dir("tests/ui").expect("error reading `tests/ui`") { let e = e.expect("error reading `tests/ui`"); - let name = e.file_name(); - if let Some((name_only, _)) = name.as_encoded_bytes().split_once(|&x| x == b'.') { - if name_only.starts_with(lint.as_bytes()) && (rename_prefixed || name_only.len() == lint.len()) { + if let Ok(name) = e.file_name().into_string() { + if e.file_type().is_ok_and(|ty| ty.is_dir()) { + if is_lint_test(lint, &name, lints) { + dst.push((name, false)); + } + } else if let Some((name_only, _)) = name.split_once('.') + && is_lint_test(lint, name_only, lints) + { dst.push((name, true)); } - } else if name.as_encoded_bytes().starts_with(lint.as_bytes()) && (rename_prefixed || name.len() == lint.len()) - { - dst.push((name, false)); } } } -fn collect_ui_toml_test_names(lint: &str, rename_prefixed: bool, dst: &mut Vec<(OsString, bool)>) { - if rename_prefixed { - for e in fs::read_dir("tests/ui-toml").expect("error reading `tests/ui-toml`") { - let e = e.expect("error reading `tests/ui-toml`"); - let name = e.file_name(); - if name.as_encoded_bytes().starts_with(lint.as_bytes()) && e.file_type().is_ok_and(|ty| ty.is_dir()) { - dst.push((name, false)); - } +fn collect_ui_toml_test_names(lint: &str, lints: &FxHashMap, dst: &mut Vec<(String, bool)>) { + for e in fs::read_dir("tests/ui-toml").expect("error reading `tests/ui-toml`") { + let e = e.expect("error reading `tests/ui-toml`"); + if e.file_type().is_ok_and(|ty| ty.is_dir()) + && let Ok(name) = e.file_name().into_string() + && is_lint_test(lint, &name, lints) + { + dst.push((name, false)); } - } else { - dst.push((lint.into(), false)); } } /// Renames all test files for the given lint. /// /// If `rename_prefixed` is `true` this will also rename tests which have the lint name as a prefix. -fn rename_test_files(old_name: &str, new_name: &str, rename_prefixed: bool) { +fn rename_test_files(old_name: &str, new_name: &str, lints: &FxHashMap) { let mut tests = Vec::new(); let mut old_buf = OsString::from("tests/ui/"); let mut new_buf = OsString::from("tests/ui/"); - collect_ui_test_names(old_name, rename_prefixed, &mut tests); + collect_ui_test_names(old_name, lints, &mut tests); for &(ref name, is_file) in &tests { old_buf.push(name); - new_buf.extend([new_name.as_ref(), name.slice_encoded_bytes(old_name.len()..)]); + new_buf.push(new_name); + new_buf.push(&name[old_name.len()..]); if is_file { try_rename_file(old_buf.as_ref(), new_buf.as_ref()); } else { @@ -219,21 +230,22 @@ fn rename_test_files(old_name: &str, new_name: &str, rename_prefixed: bool) { new_buf.truncate("tests/ui".len()); old_buf.push("-toml/"); new_buf.push("-toml/"); - collect_ui_toml_test_names(old_name, rename_prefixed, &mut tests); + collect_ui_toml_test_names(old_name, lints, &mut tests); for (name, _) in &tests { old_buf.push(name); - new_buf.extend([new_name.as_ref(), name.slice_encoded_bytes(old_name.len()..)]); + new_buf.push(new_name); + new_buf.push(&name[old_name.len()..]); try_rename_dir(old_buf.as_ref(), new_buf.as_ref()); - old_buf.truncate("tests/ui/".len()); - new_buf.truncate("tests/ui/".len()); + old_buf.truncate("tests/ui-toml/".len()); + new_buf.truncate("tests/ui-toml/".len()); } } -fn delete_test_files(lint: &str, rename_prefixed: bool) { +fn delete_test_files(lint: &str, lints: &FxHashMap) { let mut tests = Vec::new(); let mut buf = OsString::from("tests/ui/"); - collect_ui_test_names(lint, rename_prefixed, &mut tests); + collect_ui_test_names(lint, lints, &mut tests); for &(ref name, is_file) in &tests { buf.push(name); if is_file { @@ -248,11 +260,11 @@ fn delete_test_files(lint: &str, rename_prefixed: bool) { buf.push("-toml/"); tests.clear(); - collect_ui_toml_test_names(lint, rename_prefixed, &mut tests); + collect_ui_toml_test_names(lint, lints, &mut tests); for (name, _) in &tests { buf.push(name); delete_dir_if_exists(buf.as_ref()); - buf.truncate("tests/ui/".len()); + buf.truncate("tests/ui-toml/".len()); } } @@ -296,7 +308,7 @@ fn file_update_fn<'a, 'b>( let text = searcher.peek_text(); searcher.step(); match text { - // clippy::line_name or clippy::lint-name + // clippy::lint_name "clippy" => { if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture]) && capture == old_name diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs index 87f81ecd6a46..639f6ccbd009 100644 --- a/clippy_dev/src/update_lints.rs +++ b/clippy_dev/src/update_lints.rs @@ -1,9 +1,8 @@ -use crate::parse::{DeprecatedLint, Lint, RenamedLint, RustSearcher, Token, find_lint_decls, read_deprecated_lints}; +use crate::parse::{Lint, LintList}; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn}; use itertools::Itertools; -use std::collections::HashSet; use std::fmt::Write; -use std::path::{self, Path}; +use std::path::Path; const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\ // Use that command to update this file and do not edit by hand.\n\ @@ -21,91 +20,86 @@ const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.ht /// /// Panics if a file path could not read from or then written to pub fn update(update_mode: UpdateMode) { - let lints = find_lint_decls(); - let (deprecated, renamed) = read_deprecated_lints(); - generate_lint_files(update_mode, &lints, &deprecated, &renamed); + generate_lint_files(update_mode, &LintList::collect()); } -#[expect(clippy::too_many_lines)] -pub fn generate_lint_files( - update_mode: UpdateMode, - lints: &[Lint], - deprecated: &[DeprecatedLint], - renamed: &[RenamedLint], -) { +#[expect(clippy::too_many_lines, clippy::similar_names)] +pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { let mut updater = FileUpdater::default(); + + let mut lints: Vec<_> = list.lints.iter().map(|(x, y)| (&**x, y)).collect(); + lints.sort_by_key(|&(x, _)| x); + updater.update_file_checked( "cargo dev update_lints", update_mode, - "README.md", - &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { - write!(dst, "{}", round_to_fifty(lints.len())).unwrap(); - }), + "CHANGELOG.md", + &mut update_text_region_fn( + "\n", + "", + |dst| { + for &(lint, _) in &lints { + writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap(); + } + }, + ), ); + + let mut active = Vec::with_capacity(list.lints.len()); + let mut deprecated = Vec::with_capacity(list.lints.len()); + let mut renamed = Vec::with_capacity(list.lints.len()); + for (name, lint) in lints { + match lint { + Lint::Active(lint) => active.push((name, lint)), + Lint::Deprecated(lint) => deprecated.push((name, lint)), + Lint::Renamed(lint) => renamed.push((name, lint)), + } + } + active.sort_by(|(_, x), (_, y)| (&*x.krate, &*x.module).cmp(&(&*y.krate, &*y.module))); + + // Round the lint count down to avoid merge conflicts every time a new lint is added. + let lint_count = active.len() / 50 * 50; updater.update_file_checked( "cargo dev update_lints", update_mode, - "book/src/README.md", + "README.md", &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { - write!(dst, "{}", round_to_fifty(lints.len())).unwrap(); + write!(dst, "{lint_count}").unwrap(); }), ); updater.update_file_checked( "cargo dev update_lints", update_mode, - "CHANGELOG.md", - &mut update_text_region_fn( - "\n", - "", - |dst| { - for lint in lints - .iter() - .map(|l| &*l.name) - .chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::"))) - .chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::"))) - .sorted() - { - writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap(); - } - }, - ), + "book/src/README.md", + &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { + write!(dst, "{lint_count}").unwrap(); + }), ); + updater.update_file_checked( "cargo dev update_lints", update_mode, "clippy_lints/src/deprecated_lints.rs", &mut |_, src, dst| { - let mut searcher = RustSearcher::new(src); - assert!( - searcher.find_token(Token::Ident("declare_with_version")) - && searcher.find_token(Token::Ident("declare_with_version")), - "error reading deprecated lints" - ); - dst.push_str(&src[..searcher.pos() as usize]); - dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n"); - for lint in deprecated { + dst.push_str(&src[..list.deprecated_span.start as usize]); + for &(name, lint) in &deprecated { write!( dst, - " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", - lint.version, lint.name, lint.reason, + "\n #[clippy::version = \"{}\"]\n (\"clippy::{name}\", \"{}\"),", + lint.version, lint.reason, ) .unwrap(); } - dst.push_str( - "]}\n\n\ - #[rustfmt::skip]\n\ - declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\ - ", - ); - for lint in renamed { + dst.push_str(&src[list.deprecated_span.end as usize..list.renamed_span.start as usize]); + for &(name, lint) in &renamed { write!( dst, - " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", - lint.version, lint.old_name, lint.new_name, + "\n #[clippy::version = \"{}\"]\n (\"clippy::{name}\", \"{}\"),", + lint.version, lint.new_name, ) .unwrap(); } - dst.push_str("]}\n"); + dst.push_str(&src[list.renamed_span.end as usize..]); UpdateStatus::from_changed(src != dst) }, ); @@ -115,8 +109,8 @@ pub fn generate_lint_files( "tests/ui/deprecated.rs", &mut |_, src, dst| { dst.push_str(GENERATED_FILE_COMMENT); - for lint in deprecated { - writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap(); + for &(name, _) in &deprecated { + writeln!(dst, "#![warn(clippy::{name})] //~ ERROR: lint `clippy::{name}`").unwrap(); } dst.push_str("\nfn main() {}\n"); UpdateStatus::from_changed(src != dst) @@ -127,47 +121,33 @@ pub fn generate_lint_files( update_mode, "tests/ui/rename.rs", &mut move |_, src, dst| { - let mut seen_lints = HashSet::new(); dst.push_str(GENERATED_FILE_COMMENT); dst.push_str("#![allow(clippy::duplicated_attributes)]\n"); - for lint in renamed { - if seen_lints.insert(&lint.new_name) { - writeln!(dst, "#![allow({})]", lint.new_name).unwrap(); - } - } - seen_lints.clear(); - for lint in renamed { - if seen_lints.insert(&lint.old_name) { - writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap(); - } + for &(name, _) in &renamed { + writeln!(dst, "#![warn(clippy::{name})] //~ ERROR: lint `clippy::{name}`").unwrap(); } dst.push_str("\nfn main() {}\n"); UpdateStatus::from_changed(src != dst) }, ); - for (crate_name, lints) in lints.iter().into_group_map_by(|&l| { - let Some(path::Component::Normal(name)) = l.path.components().next() else { - // All paths should start with `{crate_name}/src` when parsed from `find_lint_decls` - panic!("internal error: can't read crate name from path `{}`", l.path.display()); - }; - name - }) { + + let mut active = &*active; + while let [(_, first_lint), ..] = active { + let (crate_lints, tail) = active.split_at(active.partition_point(|(_, x)| x.krate == first_lint.krate)); + active = tail; + updater.update_file_checked( "cargo dev update_lints", update_mode, - Path::new(crate_name).join("src/lib.rs"), + Path::new(&first_lint.krate).join("src/lib.rs"), &mut update_text_region_fn( "// begin lints modules, do not remove this comment, it's used in `update_lints`\n", "// end lints modules, do not remove this comment, it's used in `update_lints`", |dst| { - for lint_mod in lints - .iter() - .filter(|l| !l.module.is_empty()) - .map(|l| l.module.split_once("::").map_or(&*l.module, |x| x.0)) - .sorted() - .dedup() - { - writeln!(dst, "mod {lint_mod};").unwrap(); + for (_, lint) in crate_lints.iter().dedup_by(|(_, x), (_, y)| x.module == y.module) { + if !lint.module.is_empty() { + writeln!(dst, "mod {};", lint.module).unwrap(); + } } }, ), @@ -175,15 +155,19 @@ pub fn generate_lint_files( updater.update_file_checked( "cargo dev update_lints", update_mode, - Path::new(crate_name).join("src/declared_lints.rs"), + Path::new(&first_lint.krate).join("src/declared_lints.rs"), &mut |_, src, dst| { dst.push_str(GENERATED_FILE_COMMENT); dst.push_str("pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[\n"); - for (module_path, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() { - if module_path.is_empty() { - writeln!(dst, " crate::{lint_name}_INFO,").unwrap(); + let mut buf = String::new(); + for (name, lint) in crate_lints { + buf.clear(); + buf.push_str(name); + buf.make_ascii_uppercase(); + if lint.module.is_empty() { + writeln!(dst, " crate::{buf}_INFO,").unwrap(); } else { - writeln!(dst, " crate::{module_path}::{lint_name}_INFO,").unwrap(); + writeln!(dst, " crate::{}::{buf}_INFO,", lint.module).unwrap(); } } dst.push_str("];\n"); @@ -192,7 +176,3 @@ pub fn generate_lint_files( ); } } - -fn round_to_fifty(count: usize) -> usize { - count / 50 * 50 -} diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index fc1a07a14314..8ff9f2a775e4 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -579,8 +579,8 @@ pub fn delete_dir_if_exists(path: &Path) { } /// Walks all items excluding top-level dot files/directories and any target directories. -pub fn walk_dir_no_dot_or_target() -> impl Iterator> { - WalkDir::new(".").into_iter().filter_entry(|e| { +pub fn walk_dir_no_dot_or_target(p: impl AsRef) -> impl Iterator> { + WalkDir::new(p.as_ref()).into_iter().filter_entry(|e| { e.path() .file_name() .is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.')) diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index c3f8e02b4c06..188f8f999165 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -84,8 +84,8 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::collection_is_never_read::COLLECTION_IS_NEVER_READ_INFO, crate::comparison_chain::COMPARISON_CHAIN_INFO, crate::copies::BRANCHES_SHARING_CODE_INFO, - crate::copies::IFS_SAME_COND_INFO, crate::copies::IF_SAME_THEN_ELSE_INFO, + crate::copies::IFS_SAME_COND_INFO, crate::copies::SAME_FUNCTIONS_IN_IF_CONDITION_INFO, crate::copy_iterator::COPY_ITERATOR_INFO, crate::crate_in_macro_def::CRATE_IN_MACRO_DEF_INFO, @@ -102,9 +102,9 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::dereference::NEEDLESS_BORROW_INFO, crate::dereference::REF_BINDING_TO_REFERENCE_INFO, crate::derivable_impls::DERIVABLE_IMPLS_INFO, - crate::derive::DERIVED_HASH_WITH_MANUAL_EQ_INFO, crate::derive::DERIVE_ORD_XOR_PARTIAL_ORD_INFO, crate::derive::DERIVE_PARTIAL_EQ_WITHOUT_EQ_INFO, + crate::derive::DERIVED_HASH_WITH_MANUAL_EQ_INFO, crate::derive::EXPL_IMPL_CLONE_ON_COPY_INFO, crate::derive::UNSAFE_DERIVE_DESERIALIZE_INFO, crate::disallowed_macros::DISALLOWED_MACROS_INFO, @@ -333,8 +333,8 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::matches::MATCH_SAME_ARMS_INFO, crate::matches::MATCH_SINGLE_BINDING_INFO, crate::matches::MATCH_STR_CASE_MISMATCH_INFO, - crate::matches::MATCH_WILDCARD_FOR_SINGLE_VARIANTS_INFO, crate::matches::MATCH_WILD_ERR_ARM_INFO, + crate::matches::MATCH_WILDCARD_FOR_SINGLE_VARIANTS_INFO, crate::matches::NEEDLESS_MATCH_INFO, crate::matches::REDUNDANT_GUARDS_INFO, crate::matches::REDUNDANT_PATTERN_MATCHING_INFO, @@ -356,9 +356,9 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::CHARS_LAST_CMP_INFO, crate::methods::CHARS_NEXT_CMP_INFO, crate::methods::CLEAR_WITH_DRAIN_INFO, - crate::methods::CLONED_INSTEAD_OF_COPIED_INFO, crate::methods::CLONE_ON_COPY_INFO, crate::methods::CLONE_ON_REF_PTR_INFO, + crate::methods::CLONED_INSTEAD_OF_COPIED_INFO, crate::methods::COLLAPSIBLE_STR_REPLACE_INFO, crate::methods::CONST_IS_EMPTY_INFO, crate::methods::DOUBLE_ENDED_ITERATOR_LAST_INFO, @@ -386,7 +386,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::IO_OTHER_ERROR_INFO, crate::methods::IP_CONSTANT_INFO, crate::methods::IS_DIGIT_ASCII_RADIX_INFO, - crate::methods::ITERATOR_STEP_BY_ZERO_INFO, crate::methods::ITER_CLONED_COLLECT_INFO, crate::methods::ITER_COUNT_INFO, crate::methods::ITER_FILTER_IS_OK_INFO, @@ -402,9 +401,10 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::ITER_SKIP_NEXT_INFO, crate::methods::ITER_SKIP_ZERO_INFO, crate::methods::ITER_WITH_DRAIN_INFO, + crate::methods::ITERATOR_STEP_BY_ZERO_INFO, crate::methods::JOIN_ABSOLUTE_PATHS_INFO, - crate::methods::MANUAL_CONTAINS_INFO, crate::methods::MANUAL_C_STR_LITERALS_INFO, + crate::methods::MANUAL_CONTAINS_INFO, crate::methods::MANUAL_FILTER_MAP_INFO, crate::methods::MANUAL_FIND_MAP_INFO, crate::methods::MANUAL_INSPECT_INFO, @@ -433,8 +433,8 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::NEEDLESS_OPTION_TAKE_INFO, crate::methods::NEEDLESS_SPLITN_INFO, crate::methods::NEW_RET_NO_SELF_INFO, - crate::methods::NONSENSICAL_OPEN_OPTIONS_INFO, crate::methods::NO_EFFECT_REPLACE_INFO, + crate::methods::NONSENSICAL_OPEN_OPTIONS_INFO, crate::methods::OBFUSCATED_IF_ELSE_INFO, crate::methods::OK_EXPECT_INFO, crate::methods::OPTION_AS_REF_CLONED_INFO, @@ -446,8 +446,8 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO, crate::methods::PATH_ENDS_WITH_EXT_INFO, crate::methods::RANGE_ZIP_WITH_LEN_INFO, - crate::methods::READONLY_WRITE_LOCK_INFO, crate::methods::READ_LINE_WITHOUT_TRIM_INFO, + crate::methods::READONLY_WRITE_LOCK_INFO, crate::methods::REDUNDANT_AS_STR_INFO, crate::methods::REPEAT_ONCE_INFO, crate::methods::RESULT_FILTER_MAP_INFO, @@ -461,9 +461,9 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::SKIP_WHILE_NEXT_INFO, crate::methods::SLICED_STRING_AS_BYTES_INFO, crate::methods::STABLE_SORT_PRIMITIVE_INFO, + crate::methods::STR_SPLIT_AT_NEWLINE_INFO, crate::methods::STRING_EXTEND_CHARS_INFO, crate::methods::STRING_LIT_CHARS_ANY_INFO, - crate::methods::STR_SPLIT_AT_NEWLINE_INFO, crate::methods::SUSPICIOUS_COMMAND_ARG_SPACE_INFO, crate::methods::SUSPICIOUS_MAP_INFO, crate::methods::SUSPICIOUS_OPEN_OPTIONS_INFO, @@ -633,8 +633,8 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::ranges::RANGE_MINUS_ONE_INFO, crate::ranges::RANGE_PLUS_ONE_INFO, crate::ranges::REVERSED_EMPTY_RANGES_INFO, - crate::raw_strings::NEEDLESS_RAW_STRINGS_INFO, crate::raw_strings::NEEDLESS_RAW_STRING_HASHES_INFO, + crate::raw_strings::NEEDLESS_RAW_STRINGS_INFO, crate::rc_clone_in_vec_init::RC_CLONE_IN_VEC_INIT_INFO, crate::read_zero_byte_vec::READ_ZERO_BYTE_VEC_INFO, crate::redundant_async_block::REDUNDANT_ASYNC_BLOCK_INFO, @@ -685,13 +685,13 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::std_instead_of_core::STD_INSTEAD_OF_CORE_INFO, crate::string_patterns::MANUAL_PATTERN_CHAR_COMPARISON_INFO, crate::string_patterns::SINGLE_CHAR_PATTERN_INFO, + crate::strings::STR_TO_STRING_INFO, crate::strings::STRING_ADD_INFO, crate::strings::STRING_ADD_ASSIGN_INFO, crate::strings::STRING_FROM_UTF8_AS_BYTES_INFO, crate::strings::STRING_LIT_AS_BYTES_INFO, crate::strings::STRING_SLICE_INFO, crate::strings::STRING_TO_STRING_INFO, - crate::strings::STR_TO_STRING_INFO, crate::strings::TRIM_SPLIT_WHITESPACE_INFO, crate::strlen_on_c_strings::STRLEN_ON_C_STRINGS_INFO, crate::suspicious_operation_groupings::SUSPICIOUS_OPERATION_GROUPINGS_INFO, @@ -712,7 +712,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::transmute::CROSSPOINTER_TRANSMUTE_INFO, crate::transmute::EAGER_TRANSMUTE_INFO, crate::transmute::MISSING_TRANSMUTE_ANNOTATIONS_INFO, - crate::transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS_INFO, crate::transmute::TRANSMUTE_BYTES_TO_STR_INFO, crate::transmute::TRANSMUTE_INT_TO_BOOL_INFO, crate::transmute::TRANSMUTE_INT_TO_NON_ZERO_INFO, @@ -720,6 +719,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::transmute::TRANSMUTE_PTR_TO_PTR_INFO, crate::transmute::TRANSMUTE_PTR_TO_REF_INFO, crate::transmute::TRANSMUTE_UNDEFINED_REPR_INFO, + crate::transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS_INFO, crate::transmute::TRANSMUTING_NULL_INFO, crate::transmute::UNSOUND_COLLECTION_TRANSMUTE_INFO, crate::transmute::USELESS_TRANSMUTE_INFO, @@ -776,19 +776,19 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::vec::USELESS_VEC_INFO, crate::vec_init_then_push::VEC_INIT_THEN_PUSH_INFO, crate::visibility::NEEDLESS_PUB_SELF_INFO, - crate::visibility::PUB_WITHOUT_SHORTHAND_INFO, crate::visibility::PUB_WITH_SHORTHAND_INFO, + crate::visibility::PUB_WITHOUT_SHORTHAND_INFO, crate::wildcard_imports::ENUM_GLOB_USE_INFO, crate::wildcard_imports::WILDCARD_IMPORTS_INFO, - crate::write::PRINTLN_EMPTY_STRING_INFO, crate::write::PRINT_LITERAL_INFO, crate::write::PRINT_STDERR_INFO, crate::write::PRINT_STDOUT_INFO, crate::write::PRINT_WITH_NEWLINE_INFO, + crate::write::PRINTLN_EMPTY_STRING_INFO, crate::write::USE_DEBUG_INFO, - crate::write::WRITELN_EMPTY_STRING_INFO, crate::write::WRITE_LITERAL_INFO, crate::write::WRITE_WITH_NEWLINE_INFO, + crate::write::WRITELN_EMPTY_STRING_INFO, crate::zero_div_zero::ZERO_DIVIDED_BY_ZERO_INFO, crate::zero_repeat_side_effects::ZERO_REPEAT_SIDE_EFFECTS_INFO, crate::zero_sized_map_values::ZERO_SIZED_MAP_VALUES_INFO, diff --git a/tests/ui/rename.fixed b/tests/ui/rename.fixed index ff81c6426027..07f999e74c19 100644 --- a/tests/ui/rename.fixed +++ b/tests/ui/rename.fixed @@ -3,67 +3,6 @@ // Manual edits will be overwritten. #![allow(clippy::duplicated_attributes)] -#![allow(clippy::almost_complete_range)] -#![allow(clippy::disallowed_names)] -#![allow(clippy::blocks_in_conditions)] -#![allow(clippy::box_collection)] -#![allow(invalid_reference_casting)] -#![allow(suspicious_double_ref_op)] -#![allow(invalid_nan_comparisons)] -#![allow(clippy::redundant_static_lifetimes)] -#![allow(clippy::cognitive_complexity)] -#![allow(clippy::derived_hash_with_manual_eq)] -#![allow(clippy::disallowed_methods)] -#![allow(clippy::disallowed_types)] -#![allow(double_negations)] -#![allow(drop_bounds)] -#![allow(dropping_copy_types)] -#![allow(dropping_references)] -#![allow(clippy::mixed_read_write_in_expression)] -#![allow(clippy::manual_filter_map)] -#![allow(clippy::manual_find_map)] -#![allow(unpredictable_function_pointer_comparisons)] -#![allow(useless_ptr_null_checks)] -#![allow(for_loops_over_fallibles)] -#![allow(forgetting_copy_types)] -#![allow(forgetting_references)] -#![allow(clippy::useless_conversion)] -#![allow(clippy::redundant_pattern_matching)] -#![allow(clippy::match_result_ok)] -#![allow(clippy::non_canonical_clone_impl)] -#![allow(clippy::non_canonical_partial_ord_impl)] -#![allow(clippy::arithmetic_side_effects)] -#![allow(array_into_iter)] -#![allow(invalid_atomic_ordering)] -#![allow(invalid_null_arguments)] -#![allow(invalid_value)] -#![allow(invalid_from_utf8_unchecked)] -#![allow(let_underscore_drop)] -#![allow(clippy::overly_complex_bool_expr)] -#![allow(unexpected_cfgs)] -#![allow(enum_intrinsics_non_enums)] -#![allow(clippy::new_without_default)] -#![allow(clippy::bind_instead_of_map)] -#![allow(clippy::expect_used)] -#![allow(clippy::map_unwrap_or)] -#![allow(clippy::unwrap_used)] -#![allow(clippy::panicking_overflow_checks)] -#![allow(non_fmt_panics)] -#![allow(named_arguments_used_positionally)] -#![allow(clippy::needless_borrow)] -#![allow(clippy::reversed_empty_ranges)] -#![allow(clippy::single_char_add_str)] -#![allow(clippy::module_name_repetitions)] -#![allow(dangling_pointers_from_temporaries)] -#![allow(clippy::missing_const_for_thread_local)] -#![allow(clippy::recursive_format_impl)] -#![allow(unnecessary_transmutes)] -#![allow(undropped_manually_drops)] -#![allow(unknown_lints)] -#![allow(unused_labels)] -#![allow(clippy::unwrap_or_default)] -#![allow(ambiguous_wide_pointer_comparisons)] -#![allow(clippy::invisible_characters)] #![warn(clippy::almost_complete_range)] //~ ERROR: lint `clippy::almost_complete_letter_range` #![warn(clippy::disallowed_names)] //~ ERROR: lint `clippy::blacklisted_name` #![warn(clippy::blocks_in_conditions)] //~ ERROR: lint `clippy::block_in_if_condition_expr` diff --git a/tests/ui/rename.rs b/tests/ui/rename.rs index b5d5d07e639a..721bf4734e54 100644 --- a/tests/ui/rename.rs +++ b/tests/ui/rename.rs @@ -3,67 +3,6 @@ // Manual edits will be overwritten. #![allow(clippy::duplicated_attributes)] -#![allow(clippy::almost_complete_range)] -#![allow(clippy::disallowed_names)] -#![allow(clippy::blocks_in_conditions)] -#![allow(clippy::box_collection)] -#![allow(invalid_reference_casting)] -#![allow(suspicious_double_ref_op)] -#![allow(invalid_nan_comparisons)] -#![allow(clippy::redundant_static_lifetimes)] -#![allow(clippy::cognitive_complexity)] -#![allow(clippy::derived_hash_with_manual_eq)] -#![allow(clippy::disallowed_methods)] -#![allow(clippy::disallowed_types)] -#![allow(double_negations)] -#![allow(drop_bounds)] -#![allow(dropping_copy_types)] -#![allow(dropping_references)] -#![allow(clippy::mixed_read_write_in_expression)] -#![allow(clippy::manual_filter_map)] -#![allow(clippy::manual_find_map)] -#![allow(unpredictable_function_pointer_comparisons)] -#![allow(useless_ptr_null_checks)] -#![allow(for_loops_over_fallibles)] -#![allow(forgetting_copy_types)] -#![allow(forgetting_references)] -#![allow(clippy::useless_conversion)] -#![allow(clippy::redundant_pattern_matching)] -#![allow(clippy::match_result_ok)] -#![allow(clippy::non_canonical_clone_impl)] -#![allow(clippy::non_canonical_partial_ord_impl)] -#![allow(clippy::arithmetic_side_effects)] -#![allow(array_into_iter)] -#![allow(invalid_atomic_ordering)] -#![allow(invalid_null_arguments)] -#![allow(invalid_value)] -#![allow(invalid_from_utf8_unchecked)] -#![allow(let_underscore_drop)] -#![allow(clippy::overly_complex_bool_expr)] -#![allow(unexpected_cfgs)] -#![allow(enum_intrinsics_non_enums)] -#![allow(clippy::new_without_default)] -#![allow(clippy::bind_instead_of_map)] -#![allow(clippy::expect_used)] -#![allow(clippy::map_unwrap_or)] -#![allow(clippy::unwrap_used)] -#![allow(clippy::panicking_overflow_checks)] -#![allow(non_fmt_panics)] -#![allow(named_arguments_used_positionally)] -#![allow(clippy::needless_borrow)] -#![allow(clippy::reversed_empty_ranges)] -#![allow(clippy::single_char_add_str)] -#![allow(clippy::module_name_repetitions)] -#![allow(dangling_pointers_from_temporaries)] -#![allow(clippy::missing_const_for_thread_local)] -#![allow(clippy::recursive_format_impl)] -#![allow(unnecessary_transmutes)] -#![allow(undropped_manually_drops)] -#![allow(unknown_lints)] -#![allow(unused_labels)] -#![allow(clippy::unwrap_or_default)] -#![allow(ambiguous_wide_pointer_comparisons)] -#![allow(clippy::invisible_characters)] #![warn(clippy::almost_complete_letter_range)] //~ ERROR: lint `clippy::almost_complete_letter_range` #![warn(clippy::blacklisted_name)] //~ ERROR: lint `clippy::blacklisted_name` #![warn(clippy::block_in_if_condition_expr)] //~ ERROR: lint `clippy::block_in_if_condition_expr` diff --git a/tests/ui/rename.stderr b/tests/ui/rename.stderr index 2487dfc8eba4..579a76db2cf6 100644 --- a/tests/ui/rename.stderr +++ b/tests/ui/rename.stderr @@ -1,5 +1,5 @@ error: lint `clippy::almost_complete_letter_range` has been renamed to `clippy::almost_complete_range` - --> tests/ui/rename.rs:67:9 + --> tests/ui/rename.rs:6:9 | LL | #![warn(clippy::almost_complete_letter_range)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::almost_complete_range` @@ -8,433 +8,433 @@ LL | #![warn(clippy::almost_complete_letter_range)] = help: to override `-D warnings` add `#[allow(renamed_and_removed_lints)]` error: lint `clippy::blacklisted_name` has been renamed to `clippy::disallowed_names` - --> tests/ui/rename.rs:68:9 + --> tests/ui/rename.rs:7:9 | LL | #![warn(clippy::blacklisted_name)] | ^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::disallowed_names` error: lint `clippy::block_in_if_condition_expr` has been renamed to `clippy::blocks_in_conditions` - --> tests/ui/rename.rs:69:9 + --> tests/ui/rename.rs:8:9 | LL | #![warn(clippy::block_in_if_condition_expr)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::blocks_in_conditions` error: lint `clippy::block_in_if_condition_stmt` has been renamed to `clippy::blocks_in_conditions` - --> tests/ui/rename.rs:70:9 + --> tests/ui/rename.rs:9:9 | LL | #![warn(clippy::block_in_if_condition_stmt)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::blocks_in_conditions` error: lint `clippy::blocks_in_if_conditions` has been renamed to `clippy::blocks_in_conditions` - --> tests/ui/rename.rs:71:9 + --> tests/ui/rename.rs:10:9 | LL | #![warn(clippy::blocks_in_if_conditions)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::blocks_in_conditions` error: lint `clippy::box_vec` has been renamed to `clippy::box_collection` - --> tests/ui/rename.rs:72:9 + --> tests/ui/rename.rs:11:9 | LL | #![warn(clippy::box_vec)] | ^^^^^^^^^^^^^^^ help: use the new name: `clippy::box_collection` error: lint `clippy::cast_ref_to_mut` has been renamed to `invalid_reference_casting` - --> tests/ui/rename.rs:73:9 + --> tests/ui/rename.rs:12:9 | LL | #![warn(clippy::cast_ref_to_mut)] | ^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `invalid_reference_casting` error: lint `clippy::clone_double_ref` has been renamed to `suspicious_double_ref_op` - --> tests/ui/rename.rs:74:9 + --> tests/ui/rename.rs:13:9 | LL | #![warn(clippy::clone_double_ref)] | ^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `suspicious_double_ref_op` error: lint `clippy::cmp_nan` has been renamed to `invalid_nan_comparisons` - --> tests/ui/rename.rs:75:9 + --> tests/ui/rename.rs:14:9 | LL | #![warn(clippy::cmp_nan)] | ^^^^^^^^^^^^^^^ help: use the new name: `invalid_nan_comparisons` error: lint `clippy::const_static_lifetime` has been renamed to `clippy::redundant_static_lifetimes` - --> tests/ui/rename.rs:76:9 + --> tests/ui/rename.rs:15:9 | LL | #![warn(clippy::const_static_lifetime)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::redundant_static_lifetimes` error: lint `clippy::cyclomatic_complexity` has been renamed to `clippy::cognitive_complexity` - --> tests/ui/rename.rs:77:9 + --> tests/ui/rename.rs:16:9 | LL | #![warn(clippy::cyclomatic_complexity)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::cognitive_complexity` error: lint `clippy::derive_hash_xor_eq` has been renamed to `clippy::derived_hash_with_manual_eq` - --> tests/ui/rename.rs:78:9 + --> tests/ui/rename.rs:17:9 | LL | #![warn(clippy::derive_hash_xor_eq)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::derived_hash_with_manual_eq` error: lint `clippy::disallowed_method` has been renamed to `clippy::disallowed_methods` - --> tests/ui/rename.rs:79:9 + --> tests/ui/rename.rs:18:9 | LL | #![warn(clippy::disallowed_method)] | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::disallowed_methods` error: lint `clippy::disallowed_type` has been renamed to `clippy::disallowed_types` - --> tests/ui/rename.rs:80:9 + --> tests/ui/rename.rs:19:9 | LL | #![warn(clippy::disallowed_type)] | ^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::disallowed_types` error: lint `clippy::double_neg` has been renamed to `double_negations` - --> tests/ui/rename.rs:81:9 + --> tests/ui/rename.rs:20:9 | LL | #![warn(clippy::double_neg)] | ^^^^^^^^^^^^^^^^^^ help: use the new name: `double_negations` error: lint `clippy::drop_bounds` has been renamed to `drop_bounds` - --> tests/ui/rename.rs:82:9 + --> tests/ui/rename.rs:21:9 | LL | #![warn(clippy::drop_bounds)] | ^^^^^^^^^^^^^^^^^^^ help: use the new name: `drop_bounds` error: lint `clippy::drop_copy` has been renamed to `dropping_copy_types` - --> tests/ui/rename.rs:83:9 + --> tests/ui/rename.rs:22:9 | LL | #![warn(clippy::drop_copy)] | ^^^^^^^^^^^^^^^^^ help: use the new name: `dropping_copy_types` error: lint `clippy::drop_ref` has been renamed to `dropping_references` - --> tests/ui/rename.rs:84:9 + --> tests/ui/rename.rs:23:9 | LL | #![warn(clippy::drop_ref)] | ^^^^^^^^^^^^^^^^ help: use the new name: `dropping_references` error: lint `clippy::eval_order_dependence` has been renamed to `clippy::mixed_read_write_in_expression` - --> tests/ui/rename.rs:85:9 + --> tests/ui/rename.rs:24:9 | LL | #![warn(clippy::eval_order_dependence)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::mixed_read_write_in_expression` error: lint `clippy::filter_map` has been renamed to `clippy::manual_filter_map` - --> tests/ui/rename.rs:86:9 + --> tests/ui/rename.rs:25:9 | LL | #![warn(clippy::filter_map)] | ^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::manual_filter_map` error: lint `clippy::find_map` has been renamed to `clippy::manual_find_map` - --> tests/ui/rename.rs:87:9 + --> tests/ui/rename.rs:26:9 | LL | #![warn(clippy::find_map)] | ^^^^^^^^^^^^^^^^ help: use the new name: `clippy::manual_find_map` error: lint `clippy::fn_address_comparisons` has been renamed to `unpredictable_function_pointer_comparisons` - --> tests/ui/rename.rs:88:9 + --> tests/ui/rename.rs:27:9 | LL | #![warn(clippy::fn_address_comparisons)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unpredictable_function_pointer_comparisons` error: lint `clippy::fn_null_check` has been renamed to `useless_ptr_null_checks` - --> tests/ui/rename.rs:89:9 + --> tests/ui/rename.rs:28:9 | LL | #![warn(clippy::fn_null_check)] | ^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `useless_ptr_null_checks` error: lint `clippy::for_loop_over_option` has been renamed to `for_loops_over_fallibles` - --> tests/ui/rename.rs:90:9 + --> tests/ui/rename.rs:29:9 | LL | #![warn(clippy::for_loop_over_option)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `for_loops_over_fallibles` error: lint `clippy::for_loop_over_result` has been renamed to `for_loops_over_fallibles` - --> tests/ui/rename.rs:91:9 + --> tests/ui/rename.rs:30:9 | LL | #![warn(clippy::for_loop_over_result)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `for_loops_over_fallibles` error: lint `clippy::for_loops_over_fallibles` has been renamed to `for_loops_over_fallibles` - --> tests/ui/rename.rs:92:9 + --> tests/ui/rename.rs:31:9 | LL | #![warn(clippy::for_loops_over_fallibles)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `for_loops_over_fallibles` error: lint `clippy::forget_copy` has been renamed to `forgetting_copy_types` - --> tests/ui/rename.rs:93:9 + --> tests/ui/rename.rs:32:9 | LL | #![warn(clippy::forget_copy)] | ^^^^^^^^^^^^^^^^^^^ help: use the new name: `forgetting_copy_types` error: lint `clippy::forget_ref` has been renamed to `forgetting_references` - --> tests/ui/rename.rs:94:9 + --> tests/ui/rename.rs:33:9 | LL | #![warn(clippy::forget_ref)] | ^^^^^^^^^^^^^^^^^^ help: use the new name: `forgetting_references` error: lint `clippy::identity_conversion` has been renamed to `clippy::useless_conversion` - --> tests/ui/rename.rs:95:9 + --> tests/ui/rename.rs:34:9 | LL | #![warn(clippy::identity_conversion)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::useless_conversion` error: lint `clippy::if_let_redundant_pattern_matching` has been renamed to `clippy::redundant_pattern_matching` - --> tests/ui/rename.rs:96:9 + --> tests/ui/rename.rs:35:9 | LL | #![warn(clippy::if_let_redundant_pattern_matching)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::redundant_pattern_matching` error: lint `clippy::if_let_some_result` has been renamed to `clippy::match_result_ok` - --> tests/ui/rename.rs:97:9 + --> tests/ui/rename.rs:36:9 | LL | #![warn(clippy::if_let_some_result)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::match_result_ok` error: lint `clippy::incorrect_clone_impl_on_copy_type` has been renamed to `clippy::non_canonical_clone_impl` - --> tests/ui/rename.rs:98:9 + --> tests/ui/rename.rs:37:9 | LL | #![warn(clippy::incorrect_clone_impl_on_copy_type)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::non_canonical_clone_impl` error: lint `clippy::incorrect_partial_ord_impl_on_ord_type` has been renamed to `clippy::non_canonical_partial_ord_impl` - --> tests/ui/rename.rs:99:9 + --> tests/ui/rename.rs:38:9 | LL | #![warn(clippy::incorrect_partial_ord_impl_on_ord_type)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::non_canonical_partial_ord_impl` error: lint `clippy::integer_arithmetic` has been renamed to `clippy::arithmetic_side_effects` - --> tests/ui/rename.rs:100:9 + --> tests/ui/rename.rs:39:9 | LL | #![warn(clippy::integer_arithmetic)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::arithmetic_side_effects` error: lint `clippy::into_iter_on_array` has been renamed to `array_into_iter` - --> tests/ui/rename.rs:101:9 + --> tests/ui/rename.rs:40:9 | LL | #![warn(clippy::into_iter_on_array)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `array_into_iter` error: lint `clippy::invalid_atomic_ordering` has been renamed to `invalid_atomic_ordering` - --> tests/ui/rename.rs:102:9 + --> tests/ui/rename.rs:41:9 | LL | #![warn(clippy::invalid_atomic_ordering)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `invalid_atomic_ordering` error: lint `clippy::invalid_null_ptr_usage` has been renamed to `invalid_null_arguments` - --> tests/ui/rename.rs:103:9 + --> tests/ui/rename.rs:42:9 | LL | #![warn(clippy::invalid_null_ptr_usage)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `invalid_null_arguments` error: lint `clippy::invalid_ref` has been renamed to `invalid_value` - --> tests/ui/rename.rs:104:9 + --> tests/ui/rename.rs:43:9 | LL | #![warn(clippy::invalid_ref)] | ^^^^^^^^^^^^^^^^^^^ help: use the new name: `invalid_value` error: lint `clippy::invalid_utf8_in_unchecked` has been renamed to `invalid_from_utf8_unchecked` - --> tests/ui/rename.rs:105:9 + --> tests/ui/rename.rs:44:9 | LL | #![warn(clippy::invalid_utf8_in_unchecked)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `invalid_from_utf8_unchecked` error: lint `clippy::let_underscore_drop` has been renamed to `let_underscore_drop` - --> tests/ui/rename.rs:106:9 + --> tests/ui/rename.rs:45:9 | LL | #![warn(clippy::let_underscore_drop)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `let_underscore_drop` error: lint `clippy::logic_bug` has been renamed to `clippy::overly_complex_bool_expr` - --> tests/ui/rename.rs:107:9 + --> tests/ui/rename.rs:46:9 | LL | #![warn(clippy::logic_bug)] | ^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::overly_complex_bool_expr` error: lint `clippy::maybe_misused_cfg` has been renamed to `unexpected_cfgs` - --> tests/ui/rename.rs:108:9 + --> tests/ui/rename.rs:47:9 | LL | #![warn(clippy::maybe_misused_cfg)] | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unexpected_cfgs` error: lint `clippy::mem_discriminant_non_enum` has been renamed to `enum_intrinsics_non_enums` - --> tests/ui/rename.rs:109:9 + --> tests/ui/rename.rs:48:9 | LL | #![warn(clippy::mem_discriminant_non_enum)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `enum_intrinsics_non_enums` error: lint `clippy::mismatched_target_os` has been renamed to `unexpected_cfgs` - --> tests/ui/rename.rs:110:9 + --> tests/ui/rename.rs:49:9 | LL | #![warn(clippy::mismatched_target_os)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unexpected_cfgs` error: lint `clippy::new_without_default_derive` has been renamed to `clippy::new_without_default` - --> tests/ui/rename.rs:111:9 + --> tests/ui/rename.rs:50:9 | LL | #![warn(clippy::new_without_default_derive)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::new_without_default` error: lint `clippy::option_and_then_some` has been renamed to `clippy::bind_instead_of_map` - --> tests/ui/rename.rs:112:9 + --> tests/ui/rename.rs:51:9 | LL | #![warn(clippy::option_and_then_some)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::bind_instead_of_map` error: lint `clippy::option_expect_used` has been renamed to `clippy::expect_used` - --> tests/ui/rename.rs:113:9 + --> tests/ui/rename.rs:52:9 | LL | #![warn(clippy::option_expect_used)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::expect_used` error: lint `clippy::option_map_unwrap_or` has been renamed to `clippy::map_unwrap_or` - --> tests/ui/rename.rs:114:9 + --> tests/ui/rename.rs:53:9 | LL | #![warn(clippy::option_map_unwrap_or)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::map_unwrap_or` error: lint `clippy::option_map_unwrap_or_else` has been renamed to `clippy::map_unwrap_or` - --> tests/ui/rename.rs:115:9 + --> tests/ui/rename.rs:54:9 | LL | #![warn(clippy::option_map_unwrap_or_else)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::map_unwrap_or` error: lint `clippy::option_unwrap_used` has been renamed to `clippy::unwrap_used` - --> tests/ui/rename.rs:116:9 + --> tests/ui/rename.rs:55:9 | LL | #![warn(clippy::option_unwrap_used)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::unwrap_used` error: lint `clippy::overflow_check_conditional` has been renamed to `clippy::panicking_overflow_checks` - --> tests/ui/rename.rs:117:9 + --> tests/ui/rename.rs:56:9 | LL | #![warn(clippy::overflow_check_conditional)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::panicking_overflow_checks` error: lint `clippy::panic_params` has been renamed to `non_fmt_panics` - --> tests/ui/rename.rs:118:9 + --> tests/ui/rename.rs:57:9 | LL | #![warn(clippy::panic_params)] | ^^^^^^^^^^^^^^^^^^^^ help: use the new name: `non_fmt_panics` error: lint `clippy::positional_named_format_parameters` has been renamed to `named_arguments_used_positionally` - --> tests/ui/rename.rs:119:9 + --> tests/ui/rename.rs:58:9 | LL | #![warn(clippy::positional_named_format_parameters)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `named_arguments_used_positionally` error: lint `clippy::ref_in_deref` has been renamed to `clippy::needless_borrow` - --> tests/ui/rename.rs:120:9 + --> tests/ui/rename.rs:59:9 | LL | #![warn(clippy::ref_in_deref)] | ^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::needless_borrow` error: lint `clippy::result_expect_used` has been renamed to `clippy::expect_used` - --> tests/ui/rename.rs:121:9 + --> tests/ui/rename.rs:60:9 | LL | #![warn(clippy::result_expect_used)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::expect_used` error: lint `clippy::result_map_unwrap_or_else` has been renamed to `clippy::map_unwrap_or` - --> tests/ui/rename.rs:122:9 + --> tests/ui/rename.rs:61:9 | LL | #![warn(clippy::result_map_unwrap_or_else)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::map_unwrap_or` error: lint `clippy::result_unwrap_used` has been renamed to `clippy::unwrap_used` - --> tests/ui/rename.rs:123:9 + --> tests/ui/rename.rs:62:9 | LL | #![warn(clippy::result_unwrap_used)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::unwrap_used` error: lint `clippy::reverse_range_loop` has been renamed to `clippy::reversed_empty_ranges` - --> tests/ui/rename.rs:124:9 + --> tests/ui/rename.rs:63:9 | LL | #![warn(clippy::reverse_range_loop)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::reversed_empty_ranges` error: lint `clippy::single_char_push_str` has been renamed to `clippy::single_char_add_str` - --> tests/ui/rename.rs:125:9 + --> tests/ui/rename.rs:64:9 | LL | #![warn(clippy::single_char_push_str)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::single_char_add_str` error: lint `clippy::stutter` has been renamed to `clippy::module_name_repetitions` - --> tests/ui/rename.rs:126:9 + --> tests/ui/rename.rs:65:9 | LL | #![warn(clippy::stutter)] | ^^^^^^^^^^^^^^^ help: use the new name: `clippy::module_name_repetitions` error: lint `clippy::temporary_cstring_as_ptr` has been renamed to `dangling_pointers_from_temporaries` - --> tests/ui/rename.rs:127:9 + --> tests/ui/rename.rs:66:9 | LL | #![warn(clippy::temporary_cstring_as_ptr)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `dangling_pointers_from_temporaries` error: lint `clippy::thread_local_initializer_can_be_made_const` has been renamed to `clippy::missing_const_for_thread_local` - --> tests/ui/rename.rs:128:9 + --> tests/ui/rename.rs:67:9 | LL | #![warn(clippy::thread_local_initializer_can_be_made_const)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::missing_const_for_thread_local` error: lint `clippy::to_string_in_display` has been renamed to `clippy::recursive_format_impl` - --> tests/ui/rename.rs:129:9 + --> tests/ui/rename.rs:68:9 | LL | #![warn(clippy::to_string_in_display)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::recursive_format_impl` error: lint `clippy::transmute_float_to_int` has been renamed to `unnecessary_transmutes` - --> tests/ui/rename.rs:130:9 + --> tests/ui/rename.rs:69:9 | LL | #![warn(clippy::transmute_float_to_int)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unnecessary_transmutes` error: lint `clippy::transmute_int_to_char` has been renamed to `unnecessary_transmutes` - --> tests/ui/rename.rs:131:9 + --> tests/ui/rename.rs:70:9 | LL | #![warn(clippy::transmute_int_to_char)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unnecessary_transmutes` error: lint `clippy::transmute_int_to_float` has been renamed to `unnecessary_transmutes` - --> tests/ui/rename.rs:132:9 + --> tests/ui/rename.rs:71:9 | LL | #![warn(clippy::transmute_int_to_float)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unnecessary_transmutes` error: lint `clippy::transmute_num_to_bytes` has been renamed to `unnecessary_transmutes` - --> tests/ui/rename.rs:133:9 + --> tests/ui/rename.rs:72:9 | LL | #![warn(clippy::transmute_num_to_bytes)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unnecessary_transmutes` error: lint `clippy::undropped_manually_drops` has been renamed to `undropped_manually_drops` - --> tests/ui/rename.rs:134:9 + --> tests/ui/rename.rs:73:9 | LL | #![warn(clippy::undropped_manually_drops)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `undropped_manually_drops` error: lint `clippy::unknown_clippy_lints` has been renamed to `unknown_lints` - --> tests/ui/rename.rs:135:9 + --> tests/ui/rename.rs:74:9 | LL | #![warn(clippy::unknown_clippy_lints)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unknown_lints` error: lint `clippy::unused_label` has been renamed to `unused_labels` - --> tests/ui/rename.rs:136:9 + --> tests/ui/rename.rs:75:9 | LL | #![warn(clippy::unused_label)] | ^^^^^^^^^^^^^^^^^^^^ help: use the new name: `unused_labels` error: lint `clippy::unwrap_or_else_default` has been renamed to `clippy::unwrap_or_default` - --> tests/ui/rename.rs:137:9 + --> tests/ui/rename.rs:76:9 | LL | #![warn(clippy::unwrap_or_else_default)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::unwrap_or_default` error: lint `clippy::vtable_address_comparisons` has been renamed to `ambiguous_wide_pointer_comparisons` - --> tests/ui/rename.rs:138:9 + --> tests/ui/rename.rs:77:9 | LL | #![warn(clippy::vtable_address_comparisons)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `ambiguous_wide_pointer_comparisons` error: lint `clippy::zero_width_space` has been renamed to `clippy::invisible_characters` - --> tests/ui/rename.rs:139:9 + --> tests/ui/rename.rs:78:9 | LL | #![warn(clippy::zero_width_space)] | ^^^^^^^^^^^^^^^^^^^^^^^^ help: use the new name: `clippy::invisible_characters` From 107ab93958100e937d14ec1134105ec67fe08a05 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Wed, 9 Jul 2025 11:15:51 -0400 Subject: [PATCH 3/6] `clippy_dev`: Keep a source map when parsing files. --- clippy_dev/Cargo.toml | 1 + clippy_dev/src/deprecate_lint.rs | 28 ++++---- clippy_dev/src/lib.rs | 2 + clippy_dev/src/parse.rs | 67 ++++++++++-------- clippy_dev/src/rename_lint.rs | 48 ++++++------- clippy_dev/src/source_map.rs | 113 +++++++++++++++++++++++++++++++ clippy_dev/src/update_lints.rs | 31 +++++---- clippy_dev/src/utils.rs | 15 ++-- 8 files changed, 214 insertions(+), 91 deletions(-) create mode 100644 clippy_dev/src/source_map.rs diff --git a/clippy_dev/Cargo.toml b/clippy_dev/Cargo.toml index 10c08dba50b9..4d5ceb8211f7 100644 --- a/clippy_dev/Cargo.toml +++ b/clippy_dev/Cargo.toml @@ -9,6 +9,7 @@ chrono = { version = "0.4.38", default-features = false, features = ["clock"] } clap = { version = "4.4", features = ["derive"] } indoc = "1.0" itertools = "0.12" +memchr = "2.7.5" opener = "0.7" walkdir = "2.3" diff --git a/clippy_dev/src/deprecate_lint.rs b/clippy_dev/src/deprecate_lint.rs index c1fed600014d..8a19717737ee 100644 --- a/clippy_dev/src/deprecate_lint.rs +++ b/clippy_dev/src/deprecate_lint.rs @@ -1,8 +1,7 @@ -use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintList}; +use crate::parse::{ActiveLint, DeprecatedLint, Lint, ParsedData}; use crate::update_lints::generate_lint_files; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists}; use core::mem; -use rustc_data_structures::fx::FxHashMap; use std::ffi::OsStr; use std::path::Path; @@ -15,14 +14,13 @@ use std::path::Path; /// # Panics /// /// If a file path could not read from or written to -#[expect(clippy::similar_names)] pub fn deprecate(clippy_version: Version, name: &str, reason: &str) { if let Some((prefix, _)) = name.split_once("::") { panic!("`{name}` should not contain the `{prefix}` prefix"); } - let mut list = LintList::collect(); - let Some(entry) = list.lints.get_mut(name) else { + let mut data = ParsedData::collect(); + let Some(entry) = data.lints.get_mut(name) else { eprintln!("error: failed to find lint `{name}`"); return; }; @@ -37,13 +35,13 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) { return; }; - remove_lint_declaration(name, &lint, &list.lints); - generate_lint_files(UpdateMode::Change, &list); + remove_lint_declaration(name, &lint, &data); + generate_lint_files(UpdateMode::Change, &data); println!("info: `{name}` has successfully been deprecated"); println!("note: you must run `cargo uitest` to update the test results"); } -fn remove_lint_declaration(name: &str, lint: &ActiveLint, lints: &FxHashMap) { +fn remove_lint_declaration(name: &str, lint: &ActiveLint, data: &ParsedData) { fn remove_test_assets(name: &str) { let test_file_stem = format!("tests/ui/{name}"); let path = Path::new(&test_file_stem); @@ -85,16 +83,18 @@ fn remove_lint_declaration(name: &str, lint: &ActiveLint, lints: &FxHashMap { @@ -184,11 +185,8 @@ impl<'txt> RustSearcher<'txt> { } pub struct ActiveLint { - pub krate: String, - pub module: String, pub group: String, - pub path: PathBuf, - pub span: Range, + pub span: Span, } pub struct DeprecatedLint { @@ -207,25 +205,27 @@ pub enum Lint { Renamed(RenamedLint), } -pub struct LintList { +pub struct ParsedData { + pub source_map: SourceMap, pub lints: FxHashMap, pub deprecated_span: Range, pub renamed_span: Range, } -impl LintList { +impl ParsedData { #[expect(clippy::default_trait_access)] pub fn collect() -> Self { // 2025-05: Initial capacities should fit everything without reallocating. let mut parser = Parser { + source_map: SourceMap::with_capacity(8, 1000), lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()), deprecated_span: 0..0, renamed_span: 0..0, - contents: String::with_capacity(1024 * 128 * 3), }; parser.parse_src_files(); parser.parse_deprecated_lints(); - LintList { + ParsedData { + source_map: parser.source_map, lints: parser.lints, deprecated_span: parser.deprecated_span, renamed_span: parser.renamed_span, @@ -234,10 +234,10 @@ impl LintList { } struct Parser { + source_map: SourceMap, lints: FxHashMap, deprecated_span: Range, renamed_span: Range, - contents: String, } impl Parser { /// Parses all source files looking for lint declarations (`declare_clippy_lint! { .. }`). @@ -251,8 +251,9 @@ impl Parser { continue; }; if crate_path.starts_with("clippy_lints") && crate_path != "clippy_lints_internal" { - crate_path.push_str("/src"); - let krate = &crate_path[..crate_path.len() - 4]; + let krate = self.source_map.add_new_crate(&crate_path); + 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() @@ -271,7 +272,8 @@ impl Parser { }; path.replace(path::MAIN_SEPARATOR, "::") }; - self.parse_src_file(e.path(), krate, &module); + let file = self.source_map.load_new_file(e.path(), krate, module); + self.parse_src_file(file); } } } @@ -279,7 +281,7 @@ impl Parser { } /// Parse a source file looking for `declare_clippy_lint` macro invocations. - fn parse_src_file(&mut self, path: &Path, krate: &str, module: &str) { + fn parse_src_file(&mut self, file: SourceFile) { #[allow(clippy::enum_glob_use)] use Token::*; #[rustfmt::skip] @@ -292,8 +294,7 @@ impl Parser { Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, ]; - File::open_read_to_cleared_string(path, &mut self.contents); - let mut searcher = RustSearcher::new(&self.contents); + let mut searcher = RustSearcher::new(&self.source_map.files[file].contents); #[expect(clippy::cast_possible_truncation)] while searcher.find_token(Ident("declare_clippy_lint")) { let start = searcher.pos() - "declare_clippy_lint".len() as u32; @@ -302,11 +303,12 @@ impl Parser { self.lints.insert( name.to_ascii_lowercase(), Lint::Active(ActiveLint { - krate: krate.into(), - module: module.into(), group: group.into(), - path: path.into(), - span: start..searcher.pos(), + span: Span { + file, + start, + end: searcher.pos(), + }, }), ); } @@ -334,10 +336,15 @@ impl Parser { Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket, ]; - let path = Path::new("clippy_lints/src/deprecated_lints.rs"); - File::open_read_to_cleared_string(path, &mut self.contents); + let krate = self.source_map.add_crate("clippy_lints"); + let file = self.source_map.load_file( + Path::new("clippy_lints/src/deprecated_lints.rs"), + krate, + "deprecated_lints", + ); + let file = &self.source_map.files[file]; - let mut searcher = RustSearcher::new(&self.contents); + let mut searcher = RustSearcher::new(&file.contents); // First instance is the macro definition. assert!( searcher.find_token(Ident("declare_with_version")), @@ -352,10 +359,10 @@ impl Parser { let mut reason = ""; while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { self.lints.insert( - parse_clippy_lint_name(path, name), + parse_clippy_lint_name(&file.path, name), Lint::Deprecated(DeprecatedLint { - reason: parse_str_single_line(path, reason), - version: parse_str_single_line(path, version), + reason: parse_str_single_line(&file.path, reason), + version: parse_str_single_line(&file.path, version), }), ); end = searcher.pos(); @@ -373,10 +380,10 @@ impl Parser { let mut new_name = ""; while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { self.lints.insert( - parse_clippy_lint_name(path, old_name), + parse_clippy_lint_name(&file.path, old_name), Lint::Renamed(RenamedLint { - new_name: parse_maybe_clippy_lint_name(path, new_name), - version: parse_str_single_line(path, version), + new_name: parse_maybe_clippy_lint_name(&file.path, new_name), + version: parse_str_single_line(&file.path, version), }), ); end = searcher.pos(); diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs index b770d18d8cf5..41afa56c670b 100644 --- a/clippy_dev/src/rename_lint.rs +++ b/clippy_dev/src/rename_lint.rs @@ -1,4 +1,4 @@ -use crate::parse::{Lint, LintList, RenamedLint, RustSearcher, Token}; +use crate::parse::{Lint, ParsedData, RenamedLint, RustSearcher, Token}; use crate::update_lints::generate_lint_files; use crate::utils::{ ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, @@ -26,7 +26,7 @@ use std::path::Path; /// * If either lint name has a prefix /// * If `old_name` doesn't name an existing lint. /// * If `old_name` names a deprecated or renamed lint. -#[expect(clippy::too_many_lines, clippy::similar_names)] +#[expect(clippy::too_many_lines)] pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: bool) { if let Some((prefix, _)) = old_name.split_once("::") { panic!("`{old_name}` should not contain the `{prefix}` prefix"); @@ -36,7 +36,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b } let mut updater = FileUpdater::default(); - let mut list = LintList::collect(); + let mut data = ParsedData::collect(); // Update any existing renames let new_name_prefixed = if uplift { @@ -44,7 +44,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b } else { String::from_iter(["clippy::", new_name]) }; - for lint in list.lints.values_mut() { + for lint in data.lints.values_mut() { if let Lint::Renamed(lint) = lint && lint.new_name.strip_prefix("clippy::") == Some(old_name) { @@ -53,11 +53,11 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b } // Mark the lint as renamed - let Some(old_entry) = list.lints.get_mut(old_name) else { + let Some(old_entry) = data.lints.get_mut(old_name) else { eprintln!("error: failed to find lint `{old_name}`"); return; }; - let Lint::Active(mut lint) = mem::replace( + let Lint::Active(lint) = mem::replace( old_entry, Lint::Renamed(RenamedLint { new_name: new_name_prefixed, @@ -70,19 +70,20 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b let mut mod_edit = ModEdit::None; if uplift { - let is_unique_mod = list.lints.values().any(|x| { + let lint_file = &data.source_map.files[lint.span.file]; + let is_unique_mod = data.lints.values().any(|x| { if let Lint::Active(x) = x { - x.module == lint.module + data.source_map.files[x.span.file].module == lint_file.module } else { false } }); if is_unique_mod { - if delete_file_if_exists(lint.path.as_ref()) { + if delete_file_if_exists(lint_file.path.as_ref()) { mod_edit = ModEdit::Delete; } } else { - updater.update_file(&lint.path, &mut |_, src, dst| -> UpdateStatus { + updater.update_file(&lint_file.path, &mut |_, src, dst| -> UpdateStatus { let mut start = &src[..lint.span.start as usize]; if start.ends_with("\n\n") { start = &start[..start.len() - 1]; @@ -96,27 +97,28 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b UpdateStatus::Changed }); } - delete_test_files(old_name, &list.lints); - } else if let StdEntry::Vacant(entry) = list.lints.entry(new_name.to_owned()) { - if lint.module.ends_with(old_name) - && lint + delete_test_files(old_name, &data.lints); + } else if let StdEntry::Vacant(entry) = data.lints.entry(new_name.to_owned()) { + let lint_file = &mut data.source_map.files[lint.span.file]; + if lint_file.module.ends_with(old_name) + && lint_file .path .file_stem() .is_some_and(|x| x.as_encoded_bytes() == old_name.as_bytes()) { - let mut new_path = lint.path.with_file_name(new_name).into_os_string(); + let mut new_path = lint_file.path.with_file_name(new_name).into_os_string(); new_path.push(".rs"); - if try_rename_file(lint.path.as_ref(), new_path.as_ref()) { - lint.path = new_path.into(); + if try_rename_file(lint_file.path.as_ref(), new_path.as_ref()) { + lint_file.path = new_path.into(); mod_edit = ModEdit::Rename; - } - let mod_len = lint.module.len(); - lint.module.truncate(mod_len - old_name.len()); - lint.module.push_str(new_name); + let mod_len = lint_file.module.len(); + lint_file.module.truncate(mod_len - old_name.len()); + lint_file.module.push_str(new_name); + } } entry.insert(Lint::Active(lint)); - rename_test_files(old_name, new_name, &list.lints); + rename_test_files(old_name, new_name, &data.lints); } else { println!("Renamed `clippy::{old_name}` to `clippy::{new_name}`"); println!("Since `{new_name}` already exists the existing code has not been changed"); @@ -130,7 +132,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b updater.update_file(e.path(), &mut update_fn); } } - generate_lint_files(UpdateMode::Change, &list); + generate_lint_files(UpdateMode::Change, &data); if uplift { println!("Uplifted `clippy::{old_name}` as `{new_name}`"); diff --git a/clippy_dev/src/source_map.rs b/clippy_dev/src/source_map.rs new file mode 100644 index 000000000000..555014c77fc3 --- /dev/null +++ b/clippy_dev/src/source_map.rs @@ -0,0 +1,113 @@ +use crate::utils::File; +use core::fmt::{self, Display}; +use memchr::memchr_iter; +use rustc_index::{IndexVec, newtype_index}; +use std::path::{Path, PathBuf}; + +pub struct SourceData { + pub contents: String, + pub line_starts: Vec, + pub path: PathBuf, + pub module: String, + pub krate: Crate, +} +impl SourceData { + pub fn line_col(&self, pos: u32) -> (u32, u32) { + #[expect(clippy::cast_possible_truncation)] + let (line, offset) = match self.line_starts.binary_search(&pos) { + Ok(i) => (i as u32 + 1, self.line_starts[i]), + Err(i) => (i as u32, self.line_starts[i - 1]), + }; + let mut col = 1; + let mut remain = pos - offset; + let mut chars = self.contents[offset as usize..].chars(); + #[expect(clippy::cast_possible_truncation)] + while remain != 0 + && let Some(c) = chars.next() + { + col += 1; + remain = remain.saturating_sub(c.len_utf8() as u32); + } + (line, col) + } +} + +pub struct SourceMap { + pub crates: IndexVec>, + pub files: IndexVec, +} +impl SourceMap { + pub fn with_capacity(crates: usize, files: usize) -> Self { + Self { + crates: IndexVec::with_capacity(crates), + files: IndexVec::with_capacity(files), + } + } + + pub fn add_new_crate(&mut self, name: &str) -> Crate { + let res = self.crates.next_index(); + self.crates.push(name.into()); + res + } + + pub fn add_crate(&mut self, name: &str) -> Crate { + match self.crates.iter().position(|x| **x == *name) { + Some(x) => Crate::from_usize(x), + None => self.add_new_crate(name), + } + } + + pub fn load_new_file(&mut self, path: &Path, krate: Crate, module: String) -> SourceFile { + let mut contents = String::new(); + File::open_read(path).read_append_to_string(&mut contents); + + let res = self.files.next_index(); + let mut line_starts = Vec::with_capacity(16); + line_starts.push(0); + #[expect(clippy::cast_possible_truncation)] + line_starts.extend(memchr_iter(b'\n', contents.as_bytes()).map(|x| x as u32 + 1)); + self.files.push(SourceData { + contents, + line_starts, + path: path.into(), + module, + krate, + }); + res + } + + pub fn load_file(&mut self, path: &Path, krate: Crate, module: &str) -> SourceFile { + match self.files.iter().position(|x| x.krate == krate && x.module == module) { + Some(x) => SourceFile::from_usize(x), + None => self.load_new_file(path, krate, module.into()), + } + } +} + +newtype_index! { + pub struct SourceFile {} +} +newtype_index! { + #[orderable] + pub struct Crate {} +} + +#[derive(Clone, Copy)] +pub struct Span { + pub file: SourceFile, + pub start: u32, + pub end: u32, +} +impl Span { + pub fn display(self, source_map: &SourceMap) -> impl use<'_> + Display { + struct X<'a>(Span, &'a SourceMap); + impl Display for X<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = &self.1.files[self.0.file]; + let (line, col) = file.line_col(self.0.start); + write!(f, "{}:{line}:{col}", self.1.files[self.0.file].path.display()) + } + } + X(self, source_map) + } +} diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs index 639f6ccbd009..cf5c986984f5 100644 --- a/clippy_dev/src/update_lints.rs +++ b/clippy_dev/src/update_lints.rs @@ -1,4 +1,4 @@ -use crate::parse::{Lint, LintList}; +use crate::parse::{Lint, ParsedData}; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn}; use itertools::Itertools; use std::fmt::Write; @@ -20,14 +20,14 @@ const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.ht /// /// Panics if a file path could not read from or then written to pub fn update(update_mode: UpdateMode) { - generate_lint_files(update_mode, &LintList::collect()); + generate_lint_files(update_mode, &ParsedData::collect()); } -#[expect(clippy::too_many_lines, clippy::similar_names)] -pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { +#[expect(clippy::too_many_lines)] +pub fn generate_lint_files(update_mode: UpdateMode, data: &ParsedData) { let mut updater = FileUpdater::default(); - let mut lints: Vec<_> = list.lints.iter().map(|(x, y)| (&**x, y)).collect(); + let mut lints: Vec<_> = data.lints.iter().map(|(x, y)| (&**x, y)).collect(); lints.sort_by_key(|&(x, _)| x); updater.update_file_checked( @@ -45,17 +45,17 @@ pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { ), ); - let mut active = Vec::with_capacity(list.lints.len()); - let mut deprecated = Vec::with_capacity(list.lints.len()); - let mut renamed = Vec::with_capacity(list.lints.len()); + let mut active = Vec::with_capacity(data.lints.len()); + let mut deprecated = Vec::with_capacity(data.lints.len()); + let mut renamed = Vec::with_capacity(data.lints.len()); for (name, lint) in lints { match lint { - Lint::Active(lint) => active.push((name, lint)), + Lint::Active(lint) => active.push((name, &data.source_map.files[lint.span.file])), Lint::Deprecated(lint) => deprecated.push((name, lint)), Lint::Renamed(lint) => renamed.push((name, lint)), } } - active.sort_by(|(_, x), (_, y)| (&*x.krate, &*x.module).cmp(&(&*y.krate, &*y.module))); + active.sort_by(|(_, x), (_, y)| (x.krate, &*x.module).cmp(&(y.krate, &*y.module))); // Round the lint count down to avoid merge conflicts every time a new lint is added. let lint_count = active.len() / 50 * 50; @@ -81,7 +81,7 @@ pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { update_mode, "clippy_lints/src/deprecated_lints.rs", &mut |_, src, dst| { - dst.push_str(&src[..list.deprecated_span.start as usize]); + dst.push_str(&src[..data.deprecated_span.start as usize]); for &(name, lint) in &deprecated { write!( dst, @@ -90,7 +90,7 @@ pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { ) .unwrap(); } - dst.push_str(&src[list.deprecated_span.end as usize..list.renamed_span.start as usize]); + dst.push_str(&src[data.deprecated_span.end as usize..data.renamed_span.start as usize]); for &(name, lint) in &renamed { write!( dst, @@ -99,7 +99,7 @@ pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { ) .unwrap(); } - dst.push_str(&src[list.renamed_span.end as usize..]); + dst.push_str(&src[data.renamed_span.end as usize..]); UpdateStatus::from_changed(src != dst) }, ); @@ -134,12 +134,13 @@ pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { let mut active = &*active; while let [(_, first_lint), ..] = active { let (crate_lints, tail) = active.split_at(active.partition_point(|(_, x)| x.krate == first_lint.krate)); + let krate_name = &*data.source_map.crates[first_lint.krate]; active = tail; updater.update_file_checked( "cargo dev update_lints", update_mode, - Path::new(&first_lint.krate).join("src/lib.rs"), + Path::new(krate_name).join("src/lib.rs"), &mut update_text_region_fn( "// begin lints modules, do not remove this comment, it's used in `update_lints`\n", "// end lints modules, do not remove this comment, it's used in `update_lints`", @@ -155,7 +156,7 @@ pub fn generate_lint_files(update_mode: UpdateMode, list: &LintList) { updater.update_file_checked( "cargo dev update_lints", update_mode, - Path::new(&first_lint.krate).join("src/declared_lints.rs"), + Path::new(krate_name).join("src/declared_lints.rs"), &mut |_, src, dst| { dst.push_str(GENERATED_FILE_COMMENT); dst.push_str("pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[\n"); diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index 8ff9f2a775e4..e8f91385320b 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -69,6 +69,12 @@ impl<'a> File<'a> { } } + /// Opens a file for reading. Panics on failure. + #[track_caller] + pub fn open_read(path: &'a (impl AsRef + ?Sized)) -> Self { + Self::open(path, OpenOptions::new().read(true)) + } + /// Opens a file if it exists, panicking on any other failure. #[track_caller] pub fn open_if_exists(path: &'a (impl AsRef + ?Sized), options: &mut OpenOptions) -> Option { @@ -80,15 +86,6 @@ impl<'a> File<'a> { } } - /// Opens and reads a file into a string, panicking of failure. - #[track_caller] - pub fn open_read_to_cleared_string<'dst>( - path: &'a (impl AsRef + ?Sized), - dst: &'dst mut String, - ) -> &'dst mut String { - Self::open(path, OpenOptions::new().read(true)).read_to_cleared_string(dst) - } - /// Read the entire contents of a file to the given buffer. #[track_caller] pub fn read_append_to_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String { From 7bc7fe5e48fb856168feed56905d5eb40e55ad07 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Fri, 11 Jul 2025 05:34:36 -0400 Subject: [PATCH 4/6] `clippy_dev`: print parse errors with location info. --- clippy_dev/src/deprecate_lint.rs | 35 +-- clippy_dev/src/new_lint.rs | 8 +- clippy_dev/src/parse.rs | 481 ++++++++++++++++++++++++------- clippy_dev/src/rename_lint.rs | 62 ++-- clippy_dev/src/update_lints.rs | 10 +- 5 files changed, 434 insertions(+), 162 deletions(-) diff --git a/clippy_dev/src/deprecate_lint.rs b/clippy_dev/src/deprecate_lint.rs index 8a19717737ee..b845c033d94f 100644 --- a/clippy_dev/src/deprecate_lint.rs +++ b/clippy_dev/src/deprecate_lint.rs @@ -1,4 +1,4 @@ -use crate::parse::{ActiveLint, DeprecatedLint, Lint, ParsedData}; +use crate::parse::{ActiveLint, DeprecatedLint, LintKind, ParsedData}; use crate::update_lints::generate_lint_files; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists}; use core::mem; @@ -24,9 +24,9 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) { eprintln!("error: failed to find lint `{name}`"); return; }; - let Lint::Active(lint) = mem::replace( - entry, - Lint::Deprecated(DeprecatedLint { + let LintKind::Active(lint) = mem::replace( + &mut entry.kind, + LintKind::Deprecated(DeprecatedLint { reason: reason.into(), version: clippy_version.rust_display().to_string(), }), @@ -83,14 +83,10 @@ fn remove_lint_declaration(name: &str, lint: &ActiveLint, data: &ParsedData) { } } - let lint_file = &data.source_map.files[lint.span.file]; + let lint_file = &data.source_map.files[lint.decl_span.file]; if data.lints.values().any(|l| { - if let Lint::Active(l) = l { - let other_file = &data.source_map.files[l.span.file]; - other_file.krate == lint_file.krate && other_file.module.starts_with(&lint_file.module) - } else { - false - } + let other_file = &data.source_map.files[l.name_span.file]; + other_file.krate == lint_file.krate && other_file.module.starts_with(&lint_file.module) }) { // Try to delete a sub-module that matches the lint's name let removed_mod = if lint_file.path.file_name().map(OsStr::as_encoded_bytes) == Some(b"mod.rs") { @@ -107,23 +103,28 @@ fn remove_lint_declaration(name: &str, lint: &ActiveLint, data: &ParsedData) { && let mod_decl = format!("\nmod {name};") && let Some(mod_start) = src.find(&mod_decl) { - if mod_start < lint.span.start as usize { + if mod_start < lint.decl_span.start as usize { ( mod_start, mod_start + mod_decl.len(), - lint.span.start as usize, - lint.span.end as usize, + lint.decl_span.start as usize, + lint.decl_span.end as usize, ) } else { ( - lint.span.start as usize, - lint.span.end as usize, + lint.decl_span.start as usize, + lint.decl_span.end as usize, mod_start, mod_start + mod_decl.len(), ) } } else { - (lint.span.start as usize, lint.span.end as usize, src.len(), src.len()) + ( + lint.decl_span.start as usize, + lint.decl_span.end as usize, + src.len(), + src.len(), + ) }; dst.push_str(&src[..a]); dst.push_str(&src[b..c]); diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index d73802de1706..3d27dc438987 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -1,4 +1,4 @@ -use crate::parse::{RustSearcher, Token}; +use crate::parse::{Capture, RustSearcher, Token}; use crate::utils::Version; use clap::ValueEnum; use indoc::{formatdoc, writedoc}; @@ -523,6 +523,7 @@ fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) { let mut context = None; let mut decl_end = None; let mut searcher = RustSearcher::new(contents); + let mut captures = [Capture::EMPTY]; while let Some(name) = searcher.find_capture_token(CaptureIdent) { match name { "declare_clippy_lint" => { @@ -531,9 +532,8 @@ fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) { } }, "impl" => { - let mut capture = ""; - if searcher.match_tokens(&[Lt, Lifetime, Gt, CaptureIdent], &mut [&mut capture]) { - match capture { + if searcher.match_tokens(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) { + match searcher.get_capture(captures[0]) { "LateLintPass" => context = Some("LateContext"), "EarlyLintPass" => context = Some("EarlyContext"), _ => {}, diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index d7c316460e89..703a0922709a 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -4,13 +4,17 @@ use core::ops::Range; use core::slice; use rustc_data_structures::fx::FxHashMap; use rustc_lexer::{self as lexer, FrontmatterAllowed}; -use std::fs; +use std::collections::hash_map::{Entry, VacantEntry}; +use std::panic::Location; use std::path::{self, Path}; +use std::{fs, process}; #[derive(Clone, Copy)] pub enum Token<'a> { /// Matches any number of comments / doc comments. AnyComment, + AnyIdent, + At, Ident(&'a str), CaptureIdent, LitStr, @@ -24,6 +28,7 @@ pub enum Token<'a> { Comma, Eq, Lifetime, + Literal, Lt, Gt, OpenBrace, @@ -33,6 +38,26 @@ pub enum Token<'a> { Semi, } +#[derive(Clone, Copy)] +pub struct Capture { + pub pos: u32, + pub len: u32, +} +impl Capture { + pub const EMPTY: Self = Self { pos: 0, len: 0 }; + pub fn to_index(self) -> Range { + self.pos as usize..(self.pos + self.len) as usize + } + + pub fn to_span(self, file: SourceFile) -> Span { + Span { + file, + start: self.pos, + end: self.pos + self.len, + } + } +} + pub struct RustSearcher<'txt> { text: &'txt str, cursor: lexer::Cursor<'txt>, @@ -52,11 +77,34 @@ impl<'txt> RustSearcher<'txt> { } } + #[must_use] + pub fn get_capture(&self, capture: Capture) -> &'txt str { + &self.text[capture.to_index()] + } + #[must_use] pub fn peek_text(&self) -> &'txt str { &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] } + #[must_use] + pub fn peek_span(&self, file: SourceFile) -> Span { + Span { + file, + start: self.pos, + end: self.pos + self.next_token.len, + } + } + + #[track_caller] + fn get_unexpected_err(&self, file: SourceFile) -> Error { + ErrorKind::UnexpectedToken { + token: self.peek_text().into(), + span: self.peek_span(file), + } + .into() + } + #[must_use] pub fn peek_len(&self) -> u32 { self.next_token.len @@ -86,7 +134,7 @@ impl<'txt> RustSearcher<'txt> { /// Consumes the next token if it matches the requested value and captures the value if /// requested. Returns `true` if a token was matched. - fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool { + fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, Capture>) -> bool { loop { match (token, self.next_token.kind) { (_, lexer::TokenKind::Whitespace) @@ -95,7 +143,9 @@ impl<'txt> RustSearcher<'txt> { lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. }, ) => self.step(), (Token::AnyComment, _) => return true, - (Token::Bang, lexer::TokenKind::Bang) + (Token::At, lexer::TokenKind::At) + | (Token::AnyIdent, lexer::TokenKind::Ident) + | (Token::Bang, lexer::TokenKind::Bang) | (Token::CloseBrace, lexer::TokenKind::CloseBrace) | (Token::CloseBracket, lexer::TokenKind::CloseBracket) | (Token::CloseParen, lexer::TokenKind::CloseParen) @@ -115,10 +165,31 @@ impl<'txt> RustSearcher<'txt> { kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, .. }, + ) + | ( + Token::Literal, + lexer::TokenKind::Literal { + kind: + lexer::LiteralKind::Int { .. } + | lexer::LiteralKind::Float { .. } + | lexer::LiteralKind::Byte { terminated: true } + | lexer::LiteralKind::ByteStr { terminated: true } + | lexer::LiteralKind::Char { terminated: true } + | lexer::LiteralKind::CStr { terminated: true } + | lexer::LiteralKind::Str { terminated: true } + | lexer::LiteralKind::RawByteStr { .. } + | lexer::LiteralKind::RawCStr { .. } + | lexer::LiteralKind::RawStr { .. }, + .. + }, ) => { self.step(); return true; }, + (Token::Literal, lexer::TokenKind::Ident) if matches!(self.peek_text(), "true" | "false") => { + self.step(); + return true; + }, (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => { self.step(); return true; @@ -142,7 +213,7 @@ impl<'txt> RustSearcher<'txt> { }, ) | (Token::CaptureIdent, lexer::TokenKind::Ident) => { - **captures.next().unwrap() = self.peek_text(); + *captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len }; self.step(); return true; }, @@ -153,8 +224,8 @@ impl<'txt> RustSearcher<'txt> { #[must_use] pub fn find_token(&mut self, token: Token<'_>) -> bool { - let mut capture = [].iter_mut(); - while !self.read_token(token, &mut capture) { + let mut captures = [].iter_mut(); + while !self.read_token(token, &mut captures) { self.step(); if self.at_end() { return false; @@ -165,20 +236,25 @@ impl<'txt> RustSearcher<'txt> { #[must_use] pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> { - let mut res = ""; - let mut capture = &mut res; - let mut capture = slice::from_mut(&mut capture).iter_mut(); - while !self.read_token(token, &mut capture) { + let mut capture = Capture::EMPTY; + let mut captures = slice::from_mut(&mut capture).iter_mut(); + while !self.read_token(token, &mut captures) { self.step(); if self.at_end() { return None; } } - Some(res) + Some(&self.text[capture.to_index()]) } #[must_use] - pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool { + pub fn match_token(&mut self, token: Token<'_>) -> bool { + let mut captures = [].iter_mut(); + self.read_token(token, &mut captures) + } + + #[must_use] + pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [Capture]) -> bool { let mut captures = captures.iter_mut(); tokens.iter().all(|&t| self.read_token(t, &mut captures)) } @@ -186,7 +262,7 @@ impl<'txt> RustSearcher<'txt> { pub struct ActiveLint { pub group: String, - pub span: Span, + pub decl_span: Span, } pub struct DeprecatedLint { @@ -199,12 +275,17 @@ pub struct RenamedLint { pub version: String, } -pub enum Lint { +pub enum LintKind { Active(ActiveLint), Deprecated(DeprecatedLint), Renamed(RenamedLint), } +pub struct Lint { + pub kind: LintKind, + pub name_span: Span, +} + pub struct ParsedData { pub source_map: SourceMap, pub lints: FxHashMap, @@ -220,10 +301,57 @@ impl ParsedData { lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()), deprecated_span: 0..0, renamed_span: 0..0, + errors: Vec::new(), }; parser.parse_src_files(); parser.parse_deprecated_lints(); + if !parser.errors.is_empty() { + for error in &parser.errors { + match &error.kind { + ErrorKind::DuplicateLint { name, span, prev_span } => { + eprint!( + "error: duplicate lint `{name}` found\n at: {}\n previous: {}\n", + span.display(&parser.source_map), + prev_span.display(&parser.source_map), + ); + }, + ErrorKind::NotLintName(span) => { + eprint!( + "error: invalid lint name found\n at: {}\n", + span.display(&parser.source_map), + ); + }, + ErrorKind::LintMissingPrefix(span) => { + eprint!( + "error: lint name missing `clippy::` prefix\n at: {}\n", + span.display(&parser.source_map), + ); + }, + ErrorKind::StrLit(span) => { + eprint!( + "error: invalid string literal\n at: {}\n", + span.display(&parser.source_map), + ); + }, + ErrorKind::StrLitEol(span) => { + eprint!( + "error: string literal contains a line ending\n at: {}\n", + span.display(&parser.source_map), + ); + }, + ErrorKind::UnexpectedToken { token, span } => { + eprint!( + "error: unexpected token `{token}`\n at: {}\n", + span.display(&parser.source_map), + ); + }, + } + eprintln!(" error-src: {}", error.loc); + } + process::exit(1); + } + ParsedData { source_map: parser.source_map, lints: parser.lints, @@ -233,11 +361,47 @@ impl ParsedData { } } +enum ErrorKind { + /// Multiple lint declarations with the same name. + DuplicateLint { + name: String, + span: Span, + prev_span: Span, + }, + /// A string literal is not a valid lint name. + NotLintName(Span), + /// A lint name is missing the `clippy::` prefix. + LintMissingPrefix(Span), + /// Error when parsing a string literal. + StrLit(Span), + // A string literal contains a line terminator. + StrLitEol(Span), + // A token not expected in the source was found. + UnexpectedToken { + token: String, + span: Span, + }, +} +struct Error { + kind: ErrorKind, + loc: &'static Location<'static>, +} +impl From for Error { + #[track_caller] + fn from(kind: ErrorKind) -> Self { + Self { + kind, + loc: Location::caller(), + } + } +} + struct Parser { source_map: SourceMap, lints: FxHashMap, deprecated_span: Range, renamed_span: Range, + errors: Vec, } impl Parser { /// Parses all source files looking for lint declarations (`declare_clippy_lint! { .. }`). @@ -286,31 +450,51 @@ impl Parser { use Token::*; #[rustfmt::skip] static DECL_TOKENS: &[Token<'_>] = &[ - // !{ /// docs - Bang, OpenBrace, AnyComment, + // { /// docs + 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, + // pub NAME, GROUP, "desc" + Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, AnyComment, LitStr, + ]; + #[rustfmt::skip] + static EXTRA_TOKENS: &[Token<'_>] = &[ + // , @option = value + Comma, AnyComment, At, AnyIdent, Eq, Literal, ]; let mut searcher = RustSearcher::new(&self.source_map.files[file].contents); + let mut captures = [Capture::EMPTY; 2]; #[expect(clippy::cast_possible_truncation)] while searcher.find_token(Ident("declare_clippy_lint")) { let start = searcher.pos() - "declare_clippy_lint".len() as u32; - let (mut name, mut group) = ("", ""); - if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) { - self.lints.insert( - name.to_ascii_lowercase(), - Lint::Active(ActiveLint { - group: group.into(), - span: Span { - file, - start, - end: searcher.pos(), - }, - }), - ); + if searcher.match_token(Bang) { + if !searcher.match_tokens(DECL_TOKENS, &mut captures) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + let name_span = captures[0].to_span(file); + let name = searcher.get_capture(captures[0]).to_ascii_lowercase(); + if let Some(e) = get_vacant_lint(name, name_span, &mut self.lints, &mut self.errors) { + e.insert(Lint { + kind: LintKind::Active(ActiveLint { + group: searcher.get_capture(captures[1]).into(), + decl_span: Span { + file, + start, + end: searcher.pos(), + }, + }), + name_span, + }); + } + while searcher.match_tokens(EXTRA_TOKENS, &mut []) { + // nothing + } + if !searcher.match_token(CloseBrace) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } } } } @@ -335,6 +519,11 @@ impl Parser { // !{ RENAMED(RENAMED_VERSION) = [ Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket, ]; + #[rustfmt::skip] + static END_TOKENS: &[Token<'_>] = &[ + // ]} + CloseBracket, CloseBrace, + ]; let krate = self.source_map.add_crate("clippy_lints"); let file = self.source_map.load_file( @@ -342,117 +531,197 @@ impl Parser { krate, "deprecated_lints", ); - let file = &self.source_map.files[file]; + let file_data = &self.source_map.files[file]; - let mut searcher = RustSearcher::new(&file.contents); + let mut captures = [Capture::EMPTY; 3]; + let mut searcher = RustSearcher::new(&file_data.contents); // First instance is the macro definition. assert!( searcher.find_token(Ident("declare_with_version")), "error parsing `clippy_lints/src/deprecated_lints.rs`" ); - if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { - let start = searcher.pos(); - let mut end = start; - let mut version = ""; - let mut name = ""; - let mut reason = ""; - while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { - self.lints.insert( - parse_clippy_lint_name(&file.path, name), - Lint::Deprecated(DeprecatedLint { - reason: parse_str_single_line(&file.path, reason), - version: parse_str_single_line(&file.path, version), - }), - ); - end = searcher.pos(); + if !searcher.find_token(Ident("declare_with_version")) || !searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + let start = searcher.pos(); + let mut end = start; + while searcher.match_tokens(DECL_TOKENS, &mut captures) { + end = searcher.pos(); + let name_span = captures[1].to_span(file); + let (Some(version), Some(name), Some(reason)) = ( + parse_str_single_line(captures[0], searcher.text, file, &mut self.errors), + parse_clippy_lint_name(captures[1], searcher.text, file, &mut self.errors), + parse_str_single_line(captures[2], searcher.text, file, &mut self.errors), + ) else { + continue; + }; + if let Some(e) = get_vacant_lint(name, name_span, &mut self.lints, &mut self.errors) { + e.insert(Lint { + kind: LintKind::Deprecated(DeprecatedLint { reason, version }), + name_span, + }); } - self.deprecated_span = start..end; - } else { - panic!("error reading deprecated lints"); + } + self.deprecated_span = start..end; + if !searcher.match_tokens(END_TOKENS, &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + return; } - if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) { - let start = searcher.pos(); - let mut end = start; - let mut version = ""; - let mut old_name = ""; - let mut new_name = ""; - while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { - self.lints.insert( - parse_clippy_lint_name(&file.path, old_name), - Lint::Renamed(RenamedLint { - new_name: parse_maybe_clippy_lint_name(&file.path, new_name), - version: parse_str_single_line(&file.path, version), - }), - ); - end = searcher.pos(); + if !searcher.find_token(Ident("declare_with_version")) || !searcher.match_tokens(RENAMED_TOKENS, &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + let start = searcher.pos(); + let mut end = start; + while searcher.match_tokens(DECL_TOKENS, &mut captures) { + end = searcher.pos(); + let name_span = captures[1].to_span(file); + let (Some(version), Some(name), Some(new_name)) = ( + parse_str_single_line(captures[0], searcher.text, file, &mut self.errors), + parse_clippy_lint_name(captures[1], searcher.text, file, &mut self.errors), + parse_maybe_clippy_lint_name(captures[2], searcher.text, file, &mut self.errors), + ) else { + continue; + }; + if let Some(e) = get_vacant_lint(name, name_span, &mut self.lints, &mut self.errors) { + e.insert(Lint { + kind: LintKind::Renamed(RenamedLint { new_name, version }), + name_span, + }); } - self.renamed_span = start..end; - } else { - panic!("error reading renamed lints"); + } + self.renamed_span = start..end; + if !searcher.match_tokens(END_TOKENS, &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + #[expect(clippy::needless_return)] + return; } } } -fn assert_lint_name(path: &Path, s: &str) { - assert!( - s.bytes() - .all(|c| matches!(c, b'_' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')), - "error parsing `{}`: `{s}` is not a valid lint name", - path.display(), - ); +#[track_caller] +fn get_vacant_lint<'a>( + name: String, + name_span: Span, + lints: &'a mut FxHashMap, + errors: &mut Vec, +) -> Option> { + match lints.entry(name) { + Entry::Vacant(e) => Some(e), + Entry::Occupied(e) => { + errors.push( + ErrorKind::DuplicateLint { + name: e.key().clone(), + span: name_span, + prev_span: e.get().name_span, + } + .into(), + ); + None + }, + } } -fn parse_str_lit(s: &str) -> String { +#[track_caller] +fn parse_str_lit(capture: Capture, text: &str, file: SourceFile, errors: &mut Vec) -> Option { + let s = &text[capture.to_index()]; 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}`")); + let Some(s) = s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) else { + errors.push(ErrorKind::StrLit(capture.to_span(file)).into()); + return None; + }; - if is_raw { + let mut no_err = true; + let res = if is_raw { + #[expect(clippy::cast_possible_truncation)] + rustc_literal_escaper::check_raw_str(s, |sp, ch| { + if let Err(e) = ch + && e.is_fatal() + { + errors.push( + ErrorKind::StrLit(Span { + file, + start: capture.pos + sp.start as u32, + end: capture.pos + sp.end as u32, + }) + .into(), + ); + no_err = false; + } + }); s.into() } else { let mut res = String::with_capacity(s.len()); - rustc_literal_escaper::unescape_str(s, &mut |_, ch| { - if let Ok(ch) = ch { - res.push(ch); - } + #[expect(clippy::cast_possible_truncation)] + rustc_literal_escaper::unescape_str(s, |sp, ch| match ch { + Ok(ch) => res.push(ch), + Err(e) if e.is_fatal() => { + errors.push( + ErrorKind::StrLit(Span { + file, + start: capture.pos + sp.start as u32, + end: capture.pos + sp.end as u32, + }) + .into(), + ); + no_err = false; + }, + _ => {}, }); res + }; + no_err.then_some(res) +} + +#[track_caller] +fn parse_str_single_line(capture: Capture, text: &str, file: SourceFile, errors: &mut Vec) -> Option { + let s = parse_str_lit(capture, text, file, errors)?; + if s.contains('\n') { + errors.push(ErrorKind::StrLitEol(capture.to_span(file)).into()); + None + } else { + Some(s) } } -fn parse_str_single_line(path: &Path, s: &str) -> String { - let s = parse_str_lit(s); - assert!( - !s.contains('\n'), - "error parsing `{}`: `{s}` should be a single line string", - path.display(), - ); - s +fn is_lint_name(s: &str) -> bool { + s.bytes() + .all(|c| matches!(c, b'_' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')) } -fn parse_clippy_lint_name(path: &Path, s: &str) -> String { - let mut s = parse_str_lit(s); +#[track_caller] +fn parse_clippy_lint_name(capture: Capture, text: &str, file: SourceFile, errors: &mut Vec) -> Option { + let mut s = parse_str_lit(capture, text, file, errors)?; let Some(name) = s.strip_prefix("clippy::") else { - panic!( - "error parsing `{}`: `{s}` needs to have the `clippy::` prefix", - path.display() - ); + errors.push(ErrorKind::LintMissingPrefix(capture.to_span(file)).into()); + return None; }; - assert_lint_name(path, name); + if !is_lint_name(name) { + errors.push(ErrorKind::NotLintName(capture.to_span(file)).into()); + return None; + } s.drain(.."clippy::".len()); - s + Some(s) } -fn parse_maybe_clippy_lint_name(path: &Path, s: &str) -> String { - let s = parse_str_lit(s); - assert_lint_name(path, s.strip_prefix("clippy::").unwrap_or(&s)); - s +#[track_caller] +fn parse_maybe_clippy_lint_name( + capture: Capture, + text: &str, + file: SourceFile, + errors: &mut Vec, +) -> Option { + let s = parse_str_lit(capture, text, file, errors)?; + if !is_lint_name(s.strip_prefix("clippy::").unwrap_or(&s)) { + errors.push(ErrorKind::NotLintName(capture.to_span(file)).into()); + return None; + } + Some(s) } diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs index 41afa56c670b..8875de431ca3 100644 --- a/clippy_dev/src/rename_lint.rs +++ b/clippy_dev/src/rename_lint.rs @@ -1,12 +1,13 @@ -use crate::parse::{Lint, ParsedData, RenamedLint, RustSearcher, Token}; +use crate::parse::{Capture, Lint, LintKind, ParsedData, RenamedLint, RustSearcher, Token}; use crate::update_lints::generate_lint_files; use crate::utils::{ ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, }; use core::mem; -use rustc_data_structures::fx::{FxHashMap, StdEntry}; +use rustc_data_structures::fx::FxHashMap; use rustc_lexer::TokenKind; +use std::collections::hash_map::Entry; use std::ffi::OsString; use std::fs; use std::path::Path; @@ -45,7 +46,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b String::from_iter(["clippy::", new_name]) }; for lint in data.lints.values_mut() { - if let Lint::Renamed(lint) = lint + if let LintKind::Renamed(lint) = &mut lint.kind && lint.new_name.strip_prefix("clippy::") == Some(old_name) { lint.new_name.clone_from(&new_name_prefixed); @@ -53,13 +54,13 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b } // Mark the lint as renamed - let Some(old_entry) = data.lints.get_mut(old_name) else { + let Some(entry) = data.lints.get_mut(old_name) else { eprintln!("error: failed to find lint `{old_name}`"); return; }; - let Lint::Active(lint) = mem::replace( - old_entry, - Lint::Renamed(RenamedLint { + let LintKind::Active(lint) = mem::replace( + &mut entry.kind, + LintKind::Renamed(RenamedLint { new_name: new_name_prefixed, version: clippy_version.rust_display().to_string(), }), @@ -67,28 +68,26 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b eprintln!("error: lint `{old_name}` is already deprecated"); return; }; + let lint_name_span = entry.name_span; let mut mod_edit = ModEdit::None; if uplift { - let lint_file = &data.source_map.files[lint.span.file]; - let is_unique_mod = data.lints.values().any(|x| { - if let Lint::Active(x) = x { - data.source_map.files[x.span.file].module == lint_file.module - } else { - false - } - }); + let lint_file = &data.source_map.files[lint_name_span.file]; + let is_unique_mod = data + .lints + .values() + .any(|x| data.source_map.files[x.name_span.file].module == lint_file.module); if is_unique_mod { if delete_file_if_exists(lint_file.path.as_ref()) { mod_edit = ModEdit::Delete; } } else { updater.update_file(&lint_file.path, &mut |_, src, dst| -> UpdateStatus { - let mut start = &src[..lint.span.start as usize]; + let mut start = &src[..lint.decl_span.start as usize]; if start.ends_with("\n\n") { start = &start[..start.len() - 1]; } - let mut end = &src[lint.span.end as usize..]; + let mut end = &src[lint.decl_span.end as usize..]; if end.starts_with("\n\n") { end = &end[1..]; } @@ -98,8 +97,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b }); } delete_test_files(old_name, &data.lints); - } else if let StdEntry::Vacant(entry) = data.lints.entry(new_name.to_owned()) { - let lint_file = &mut data.source_map.files[lint.span.file]; + } else if let Entry::Vacant(entry) = data.lints.entry(new_name.to_owned()) { + let lint_file = &mut data.source_map.files[lint.decl_span.file]; if lint_file.module.ends_with(old_name) && lint_file .path @@ -117,7 +116,10 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b lint_file.module.push_str(new_name); } } - entry.insert(Lint::Active(lint)); + entry.insert(Lint { + kind: LintKind::Active(lint), + name_span: lint_name_span, + }); rename_test_files(old_name, new_name, &data.lints); } else { println!("Renamed `clippy::{old_name}` to `clippy::{new_name}`"); @@ -301,7 +303,7 @@ fn file_update_fn<'a, 'b>( let mut copy_pos = 0u32; let mut changed = false; let mut searcher = RustSearcher::new(src); - let mut capture = ""; + let mut captures = [Capture::EMPTY]; loop { match searcher.peek() { TokenKind::Eof => break, @@ -312,10 +314,10 @@ fn file_update_fn<'a, 'b>( match text { // clippy::lint_name "clippy" => { - if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture]) - && capture == old_name + if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut captures) + && searcher.get_capture(captures[0]) == old_name { - dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]); + dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); dst.push_str(new_name); copy_pos = searcher.pos(); changed = true; @@ -324,12 +326,12 @@ fn file_update_fn<'a, 'b>( // mod lint_name "mod" => { if !matches!(mod_edit, ModEdit::None) - && searcher.match_tokens(&[Token::CaptureIdent], &mut [&mut capture]) - && capture == old_name + && searcher.match_tokens(&[Token::CaptureIdent], &mut captures) + && searcher.get_capture(captures[0]) == old_name { match mod_edit { ModEdit::Rename => { - dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]); + dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); dst.push_str(new_name); copy_pos = searcher.pos(); changed = true; @@ -392,10 +394,10 @@ fn file_update_fn<'a, 'b>( }, // ::lint_name TokenKind::Colon - if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture]) - && capture == old_name => + if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut captures) + && searcher.get_capture(captures[0]) == old_name => { - dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]); + dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); dst.push_str(new_name); copy_pos = searcher.pos(); changed = true; diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs index cf5c986984f5..83e2ff4bc952 100644 --- a/clippy_dev/src/update_lints.rs +++ b/clippy_dev/src/update_lints.rs @@ -1,4 +1,4 @@ -use crate::parse::{Lint, ParsedData}; +use crate::parse::{LintKind, ParsedData}; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn}; use itertools::Itertools; use std::fmt::Write; @@ -49,10 +49,10 @@ pub fn generate_lint_files(update_mode: UpdateMode, data: &ParsedData) { let mut deprecated = Vec::with_capacity(data.lints.len()); let mut renamed = Vec::with_capacity(data.lints.len()); for (name, lint) in lints { - match lint { - Lint::Active(lint) => active.push((name, &data.source_map.files[lint.span.file])), - Lint::Deprecated(lint) => deprecated.push((name, lint)), - Lint::Renamed(lint) => renamed.push((name, lint)), + match &lint.kind { + LintKind::Active(lint) => active.push((name, &data.source_map.files[lint.decl_span.file])), + LintKind::Deprecated(lint) => deprecated.push((name, lint)), + LintKind::Renamed(lint) => renamed.push((name, lint)), } } active.sort_by(|(_, x), (_, y)| (x.krate, &*x.module).cmp(&(y.krate, &*y.module))); From 7753c9b06c2526fdca011b27862b2bba55b6b634 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sat, 12 Jul 2025 01:55:27 -0400 Subject: [PATCH 5/6] `clippy_dev`: parse lint pass macro calls --- clippy_dev/src/new_lint.rs | 2 +- clippy_dev/src/parse.rs | 174 +++++++++++++++++++++++++++++-------- 2 files changed, 137 insertions(+), 39 deletions(-) diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index 3d27dc438987..b353520fb9fe 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -524,7 +524,7 @@ fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) { let mut decl_end = None; let mut searcher = RustSearcher::new(contents); let mut captures = [Capture::EMPTY]; - while let Some(name) = searcher.find_capture_token(CaptureIdent) { + while let Some(name) = searcher.find_any_ident() { match name { "declare_clippy_lint" => { if searcher.match_tokens(&[Bang, OpenBrace], &mut []) && searcher.find_token(CloseBrace) { diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 703a0922709a..20f110a169c4 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -3,6 +3,7 @@ use crate::utils::{ErrAction, expect_action, walk_dir_no_dot_or_target}; use core::ops::Range; use core::slice; use rustc_data_structures::fx::FxHashMap; +use rustc_index::{IndexVec, newtype_index}; use rustc_lexer::{self as lexer, FrontmatterAllowed}; use std::collections::hash_map::{Entry, VacantEntry}; use std::panic::Location; @@ -27,6 +28,7 @@ pub enum Token<'a> { DoubleColon, Comma, Eq, + FatArrow, Lifetime, Literal, Lt, @@ -34,6 +36,7 @@ pub enum Token<'a> { OpenBrace, OpenBracket, OpenParen, + OptLifetimeArgs, Pound, Semi, } @@ -134,6 +137,7 @@ impl<'txt> RustSearcher<'txt> { /// Consumes the next token if it matches the requested value and captures the value if /// requested. Returns `true` if a token was matched. + #[expect(clippy::too_many_lines)] fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, Capture>) -> bool { loop { match (token, self.next_token.kind) { @@ -202,6 +206,25 @@ impl<'txt> RustSearcher<'txt> { } return false; }, + (Token::FatArrow, lexer::TokenKind::Eq) => { + self.step(); + if matches!(self.next_token.kind, lexer::TokenKind::Gt) { + self.step(); + return true; + } + return false; + }, + (Token::OptLifetimeArgs, lexer::TokenKind::Lt) => { + self.step(); + while self.read_token(Token::Lifetime, captures) { + if !self.read_token(Token::Comma, captures) { + break; + } + } + return self.read_token(Token::Gt, captures); + }, + #[expect(clippy::match_same_arms)] + (Token::OptLifetimeArgs, _) => return true, #[rustfmt::skip] ( Token::CaptureLitStr, @@ -235,16 +258,28 @@ impl<'txt> RustSearcher<'txt> { } #[must_use] - pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> { - let mut capture = Capture::EMPTY; - let mut captures = slice::from_mut(&mut capture).iter_mut(); - while !self.read_token(token, &mut captures) { - self.step(); - if self.at_end() { - return None; + pub fn find_any_ident(&mut self) -> Option<&'txt str> { + loop { + match self.next_token.kind { + lexer::TokenKind::Ident => { + let res = self.peek_text(); + self.step(); + return Some(res); + }, + lexer::TokenKind::Eof => return None, + _ => self.step(), } } - Some(&self.text[capture.to_index()]) + } + + #[must_use] + pub fn find_ident(&mut self, s: &str) -> bool { + while let Some(x) = self.find_any_ident() { + if x == s { + return true; + } + } + false } #[must_use] @@ -286,9 +321,27 @@ pub struct Lint { pub name_span: Span, } +pub struct LintPassData { + pub name: String, + /// Span of the `impl_lint_pass` or `declare_lint_pass` macro call. + pub mac_span: Span, +} + +newtype_index! { + #[orderable] + pub struct LintPass {} +} + +pub struct LintRegistration { + pub name: String, + pub pass: LintPass, +} + pub struct ParsedData { pub source_map: SourceMap, pub lints: FxHashMap, + pub lint_passes: IndexVec, + pub lint_registrations: Vec, pub deprecated_span: Range, pub renamed_span: Range, } @@ -299,6 +352,8 @@ impl ParsedData { let mut parser = Parser { source_map: SourceMap::with_capacity(8, 1000), lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()), + lint_passes: IndexVec::with_capacity(400), + lint_registrations: Vec::with_capacity(1000), deprecated_span: 0..0, renamed_span: 0..0, errors: Vec::new(), @@ -355,6 +410,8 @@ impl ParsedData { ParsedData { source_map: parser.source_map, lints: parser.lints, + lint_passes: parser.lint_passes, + lint_registrations: parser.lint_registrations, deprecated_span: parser.deprecated_span, renamed_span: parser.renamed_span, } @@ -399,6 +456,8 @@ impl From for Error { struct Parser { source_map: SourceMap, lints: FxHashMap, + lint_passes: IndexVec, + lint_registrations: Vec, deprecated_span: Range, renamed_span: Range, errors: Vec, @@ -449,7 +508,7 @@ impl Parser { #[allow(clippy::enum_glob_use)] use Token::*; #[rustfmt::skip] - static DECL_TOKENS: &[Token<'_>] = &[ + static LINT_DECL_TOKENS: &[Token<'_>] = &[ // { /// docs OpenBrace, AnyComment, // #[clippy::version = "version"] @@ -458,42 +517,81 @@ impl Parser { Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, AnyComment, LitStr, ]; #[rustfmt::skip] - static EXTRA_TOKENS: &[Token<'_>] = &[ + static LINT_DECL_EXTRA_TOKENS: &[Token<'_>] = &[ // , @option = value Comma, AnyComment, At, AnyIdent, Eq, Literal, ]; + #[rustfmt::skip] + static LINT_PASS_TOKENS: &[Token<'_>] = &[ + // ( name <'lt> => [ + OpenParen, AnyComment, CaptureIdent, OptLifetimeArgs, FatArrow, OpenBracket, + ]; let mut searcher = RustSearcher::new(&self.source_map.files[file].contents); let mut captures = [Capture::EMPTY; 2]; - #[expect(clippy::cast_possible_truncation)] - while searcher.find_token(Ident("declare_clippy_lint")) { - let start = searcher.pos() - "declare_clippy_lint".len() as u32; + while let Some(ident) = searcher.find_any_ident() { + #[expect(clippy::cast_possible_truncation)] + let start = searcher.pos - ident.len() as u32; if searcher.match_token(Bang) { - if !searcher.match_tokens(DECL_TOKENS, &mut captures) { - self.errors.push(searcher.get_unexpected_err(file)); - return; - } - let name_span = captures[0].to_span(file); - let name = searcher.get_capture(captures[0]).to_ascii_lowercase(); - if let Some(e) = get_vacant_lint(name, name_span, &mut self.lints, &mut self.errors) { - e.insert(Lint { - kind: LintKind::Active(ActiveLint { - group: searcher.get_capture(captures[1]).into(), - decl_span: Span { + match ident { + "declare_clippy_lint" => { + if !searcher.match_tokens(LINT_DECL_TOKENS, &mut captures) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + let name_span = captures[0].to_span(file); + let name = searcher.get_capture(captures[0]).to_ascii_lowercase(); + if let Some(e) = get_vacant_lint(name, name_span, &mut self.lints, &mut self.errors) { + e.insert(Lint { + kind: LintKind::Active(ActiveLint { + group: searcher.get_capture(captures[1]).into(), + decl_span: Span { + file, + start, + end: searcher.pos(), + }, + }), + name_span, + }); + } + while searcher.match_tokens(LINT_DECL_EXTRA_TOKENS, &mut []) { + // nothing + } + if !searcher.match_token(CloseBrace) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + }, + "impl_lint_pass" | "declare_lint_pass" => { + if !searcher.match_tokens(LINT_PASS_TOKENS, &mut captures) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + let pass = self.lint_passes.next_index(); + let pass_name = captures[0]; + while searcher.match_tokens(&[AnyComment, CaptureIdent], &mut captures) { + self.lint_registrations.push(LintRegistration { + name: searcher.get_capture(captures[0]).to_ascii_lowercase(), + pass, + }); + if !searcher.match_token(Comma) { + break; + } + } + if !searcher.match_tokens(&[CloseBracket, CloseParen], &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + self.lint_passes.push(LintPassData { + name: searcher.get_capture(pass_name).to_owned(), + mac_span: Span { file, start, end: searcher.pos(), }, - }), - name_span, - }); - } - while searcher.match_tokens(EXTRA_TOKENS, &mut []) { - // nothing - } - if !searcher.match_token(CloseBrace) { - self.errors.push(searcher.get_unexpected_err(file)); - return; + }); + }, + _ => {}, } } } @@ -537,11 +635,11 @@ impl Parser { let mut searcher = RustSearcher::new(&file_data.contents); // First instance is the macro definition. assert!( - searcher.find_token(Ident("declare_with_version")), - "error parsing `clippy_lints/src/deprecated_lints.rs`" + searcher.find_ident("declare_with_version"), + "error parsing `clippy_lints/src/deprecated_lints.rs`", ); - if !searcher.find_token(Ident("declare_with_version")) || !searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { + if !searcher.find_ident("declare_with_version") || !searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { self.errors.push(searcher.get_unexpected_err(file)); return; } @@ -570,7 +668,7 @@ impl Parser { return; } - if !searcher.find_token(Ident("declare_with_version")) || !searcher.match_tokens(RENAMED_TOKENS, &mut []) { + if !searcher.find_ident("declare_with_version") || !searcher.match_tokens(RENAMED_TOKENS, &mut []) { self.errors.push(searcher.get_unexpected_err(file)); return; } From 8b7e638008c482c71cd1d75ed2a5d473301c7ae7 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sun, 13 Jul 2025 13:56:07 -0400 Subject: [PATCH 6/6] `clippy_dev`: check that all lints are registered with a lint pass --- clippy_dev/src/update_lints.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs index 83e2ff4bc952..146230882a60 100644 --- a/clippy_dev/src/update_lints.rs +++ b/clippy_dev/src/update_lints.rs @@ -3,6 +3,7 @@ use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn} use itertools::Itertools; use std::fmt::Write; use std::path::Path; +use std::process; const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\ // Use that command to update this file and do not edit by hand.\n\ @@ -20,7 +21,24 @@ const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.ht /// /// Panics if a file path could not read from or then written to pub fn update(update_mode: UpdateMode) { - generate_lint_files(update_mode, &ParsedData::collect()); + let mut data = ParsedData::collect(); + let mut is_missing = false; + data.lint_registrations.sort_by(|x, y| x.name.cmp(&y.name)); + for (name, lint) in &data.lints { + if matches!(lint.kind, LintKind::Active(_)) + && data.lint_registrations.binary_search_by(|x| x.name.cmp(name)).is_err() + { + is_missing = true; + eprint!( + "error: lint `{name}` is not registered in a lint pass\n declared here: {}\n", + lint.name_span.display(&data.source_map) + ); + } + } + if is_missing { + process::exit(1); + } + generate_lint_files(update_mode, &data); } #[expect(clippy::too_many_lines)]