diff --git a/CHANGELOG.md b/CHANGELOG.md index a92fbdc767bd..8d770ad2a338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6304,6 +6304,7 @@ Released 2018-09-13 [`redundant_feature_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_feature_names [`redundant_field_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_field_names [`redundant_guards`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_guards +[`redundant_iter_cloned`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_iter_cloned [`redundant_locals`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_locals [`redundant_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pattern [`redundant_pattern_matching`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pattern_matching diff --git a/Cargo.toml b/Cargo.toml index 2b6139e7e7f3..180280a2e459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,9 @@ path = "src/driver.rs" [dependencies] clippy_config = { path = "clippy_config" } clippy_lints = { path = "clippy_lints" } +clippy_lints_loops = { path = "clippy_lints_loops" } +clippy_lints_matches = { path = "clippy_lints_matches" } +clippy_lints_methods = { path = "clippy_lints_methods" } clippy_utils = { path = "clippy_utils" } declare_clippy_lint = { path = "declare_clippy_lint" } rustc_tools_util = { path = "rustc_tools_util", version = "0.4.2" } 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 3bdc5b277232..b845c033d94f 100644 --- a/clippy_dev/src/deprecate_lint.rs +++ b/clippy_dev/src/deprecate_lint.rs @@ -1,8 +1,9 @@ -use crate::update_lints::{DeprecatedLint, Lint, find_lint_decls, generate_lint_files, read_deprecated_lints}; -use crate::utils::{UpdateMode, Version}; +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; use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::{fs, io}; +use std::path::Path; /// Runs the `deprecate` command /// @@ -18,68 +19,42 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) { 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 data = ParsedData::collect(); + let Some(entry) = data.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 LintKind::Active(lint) = mem::replace( + &mut entry.kind, + LintKind::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, &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, 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, data: &ParsedData) { 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) { @@ -108,54 +83,58 @@ 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)?; + let lint_file = &data.source_map.files[lint.decl_span.file]; + if data.lints.values().any(|l| { + 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") { + let mut path = lint_file.path.clone(); + path.set_file_name(name); + path.set_extension("rs"); + delete_file_if_exists(&path) } 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())); - } - - remove_test_assets(name); - remove_lint(name, lints); - return Ok(true); + false + }; + + FileUpdater::default().update_file(&lint_file.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.decl_span.start as usize { + ( + mod_start, + mod_start + mod_decl.len(), + lint.decl_span.start as usize, + lint.decl_span.end as usize, + ) + } else { + ( + lint.decl_span.start as usize, + lint.decl_span.end as usize, + mod_start, + mod_start + mod_decl.len(), + ) + } + } else { + ( + 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]); + 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_file.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 3361443196ab..0d6ee9b92f1b 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,11 @@ )] #![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_index; extern crate rustc_lexer; extern crate rustc_literal_escaper; @@ -34,3 +37,6 @@ pub mod setup; pub mod sync; pub mod update_lints; pub mod utils; + +mod parse; +mod source_map; diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index 4121daa85e6d..b353520fb9fe 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::{Capture, RustSearcher, Token}; +use crate::utils::Version; use clap::ValueEnum; use indoc::{formatdoc, writedoc}; use std::fmt::{self, Write as _}; @@ -522,7 +523,8 @@ 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); - while let Some(name) = searcher.find_capture_token(CaptureIdent) { + let mut captures = [Capture::EMPTY]; + while let Some(name) = searcher.find_any_ident() { match name { "declare_clippy_lint" => { if searcher.match_tokens(&[Bang, OpenBrace], &mut []) && searcher.find_token(CloseBrace) { @@ -530,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 new file mode 100644 index 000000000000..b4a949331040 --- /dev/null +++ b/clippy_dev/src/parse.rs @@ -0,0 +1,833 @@ +use crate::source_map::{SourceFile, SourceMap, Span}; +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; +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, + CaptureLitStr, + Bang, + CloseBrace, + CloseBracket, + CloseParen, + /// This will consume the first colon even if the second doesn't exist. + DoubleColon, + Comma, + Eq, + FatArrow, + Lifetime, + Literal, + Lt, + Gt, + OpenBrace, + OpenBracket, + OpenParen, + OptLifetimeArgs, + Pound, + 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>, + 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 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 + } + + #[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 + } + + /// 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; + 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. + #[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) { + (_, lexer::TokenKind::Whitespace) + | ( + Token::AnyComment, + lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. }, + ) => self.step(), + (Token::AnyComment, _) => return true, + (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) + | (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 { .. }, + .. + }, + ) + | ( + 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; + }, + (Token::DoubleColon, lexer::TokenKind::Colon) => { + self.step(); + if matches!(self.next_token.kind, lexer::TokenKind::Colon) { + self.step(); + return true; + } + 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, + lexer::TokenKind::Literal { + kind: + lexer::LiteralKind::Str { terminated: true } + | lexer::LiteralKind::RawStr { n_hashes: Some(_) }, + .. + }, + ) + | (Token::CaptureIdent, lexer::TokenKind::Ident) => { + *captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len }; + self.step(); + return true; + }, + _ => return false, + } + } + } + + #[must_use] + pub fn find_token(&mut self, token: Token<'_>) -> bool { + let mut captures = [].iter_mut(); + while !self.read_token(token, &mut captures) { + self.step(); + if self.at_end() { + return false; + } + } + true + } + + #[must_use] + 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(), + } + } + } + + #[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] + 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)) + } +} + +pub struct ActiveLint { + pub group: String, + pub decl_span: Span, +} + +pub struct DeprecatedLint { + pub reason: String, + pub version: String, +} + +pub struct RenamedLint { + pub new_name: String, + pub version: String, +} + +pub enum LintKind { + Active(ActiveLint), + Deprecated(DeprecatedLint), + Renamed(RenamedLint), +} + +pub struct Lint { + pub kind: LintKind, + 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, +} +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()), + lint_passes: IndexVec::with_capacity(400), + lint_registrations: Vec::with_capacity(1000), + 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, + lint_passes: parser.lint_passes, + lint_registrations: parser.lint_registrations, + deprecated_span: parser.deprecated_span, + renamed_span: parser.renamed_span, + } + } +} + +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, + lint_passes: IndexVec, + lint_registrations: Vec, + deprecated_span: Range, + renamed_span: Range, + errors: Vec, +} +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" { + 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() + && 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, "::") + }; + let file = self.source_map.load_new_file(e.path(), krate, module); + self.parse_src_file(file); + } + } + } + } + } + + /// Parse a source file looking for `declare_clippy_lint` macro invocations. + fn parse_src_file(&mut self, file: SourceFile) { + #[allow(clippy::enum_glob_use)] + use Token::*; + #[rustfmt::skip] + static LINT_DECL_TOKENS: &[Token<'_>] = &[ + // { /// docs + OpenBrace, AnyComment, + // #[clippy::version = "version"] + Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket, + // pub NAME, GROUP, "desc" + Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, AnyComment, LitStr, + ]; + #[rustfmt::skip] + 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]; + 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) { + match ident { + "declare_clippy_lint" => { + if !searcher.match_tokens(LINT_DECL_TOKENS, &mut captures) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + while searcher.match_tokens(LINT_DECL_EXTRA_TOKENS, &mut []) { + // nothing + } + if !searcher.match_token(CloseBrace) { + 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, + }); + } + }, + "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) { + // Read a path expression. + while searcher.match_token(DoubleColon) { + // Overwrite the previous capture. The last segment is the lint name we want. + if !searcher.match_tokens(&[CaptureIdent], &mut captures) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + } + 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(), + }, + }); + }, + _ => {}, + } + } + } + } + + 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, + ]; + #[rustfmt::skip] + static END_TOKENS: &[Token<'_>] = &[ + // ]} + CloseBracket, CloseBrace, + ]; + + 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_data = &self.source_map.files[file]; + + let mut captures = [Capture::EMPTY; 3]; + let mut searcher = RustSearcher::new(&file_data.contents); + // First instance is the macro definition. + assert!( + searcher.find_ident("declare_with_version"), + "error parsing `clippy_lints/src/deprecated_lints.rs`", + ); + + if !searcher.find_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; + if !searcher.match_tokens(END_TOKENS, &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + return; + } + + if !searcher.find_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; + if !searcher.match_tokens(END_TOKENS, &mut []) { + self.errors.push(searcher.get_unexpected_err(file)); + #[expect(clippy::needless_return)] + return; + } + } +} + +#[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 + }, + } +} + +#[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 Some(s) = s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) else { + errors.push(ErrorKind::StrLit(capture.to_span(file)).into()); + return None; + }; + + 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()); + #[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 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')) +} + +#[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 { + errors.push(ErrorKind::LintMissingPrefix(capture.to_span(file)).into()); + return None; + }; + if !is_lint_name(name) { + errors.push(ErrorKind::NotLintName(capture.to_span(file)).into()); + return None; + } + s.drain(.."clippy::".len()); + Some(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/release.rs b/clippy_dev/src/release.rs index 15392dd1d292..e9893d97576d 100644 --- a/clippy_dev/src/release.rs +++ b/clippy_dev/src/release.rs @@ -4,6 +4,9 @@ use std::fmt::Write; static CARGO_TOML_FILES: &[&str] = &[ "clippy_config/Cargo.toml", "clippy_lints/Cargo.toml", + "clippy_lints_matches/Cargo.toml", + "clippy_lints_loops/Cargo.toml", + "clippy_lints_methods/Cargo.toml", "clippy_utils/Cargo.toml", "declare_clippy_lint/Cargo.toml", "Cargo.toml", diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs index d62597428e21..8875de431ca3 100644 --- a/clippy_dev/src/rename_lint.rs +++ b/clippy_dev/src/rename_lint.rs @@ -1,9 +1,13 @@ -use crate::update_lints::{RenamedLint, find_lint_decls, generate_lint_files, read_deprecated_lints}; +use crate::parse::{Capture, Lint, LintKind, ParsedData, RenamedLint, RustSearcher, Token}; +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 core::mem; +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; @@ -33,67 +37,57 @@ 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 mut data = ParsedData::collect(); - 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 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 data.lints.values_mut() { + 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); } } - 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(entry) = data.lints.get_mut(old_name) else { + eprintln!("error: failed to find lint `{old_name}`"); + return; + }; + let LintKind::Active(lint) = mem::replace( + &mut entry.kind, + LintKind::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 lint_name_span = entry.name_span; 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 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.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 { - let mut start = &src[..lint.declaration_range.start]; + updater.update_file(&lint_file.path, &mut |_, src, dst| -> UpdateStatus { + 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.declaration_range.end..]; + let mut end = &src[lint.decl_span.end as usize..]; if end.starts_with("\n\n") { end = &end[1..]; } @@ -102,29 +96,31 @@ 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]; - if lint.module.ends_with(old_name) - && lint + delete_test_files(old_name, &data.lints); + } 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 .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()) { + 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); + } } - 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 { + 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}`"); println!("Since `{new_name}` already exists the existing code has not been changed"); @@ -132,13 +128,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, &data); if uplift { println!("Uplifted `clippy::{old_name}` as `{new_name}`"); @@ -163,47 +159,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 { @@ -218,21 +234,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 { @@ -247,11 +264,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()); } } @@ -286,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, @@ -295,12 +312,12 @@ 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 + 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; @@ -309,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; @@ -377,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/serve.rs b/clippy_dev/src/serve.rs index 498ffeba9d67..bb5bb9b41e4f 100644 --- a/clippy_dev/src/serve.rs +++ b/clippy_dev/src/serve.rs @@ -22,6 +22,9 @@ pub fn run(port: u16, lint: Option) -> ! { let index_time = mtime("util/gh-pages/index.html"); let times = [ "clippy_lints/src", + "clippy_lints_loops/src", + "clippy_lints_matches/src", + "clippy_lints_methods/src", "util/gh-pages/index_template.html", "tests/compile-test.rs", ] diff --git a/clippy_dev/src/setup/intellij.rs b/clippy_dev/src/setup/intellij.rs index c56811ee0a01..644efd372a07 100644 --- a/clippy_dev/src/setup/intellij.rs +++ b/clippy_dev/src/setup/intellij.rs @@ -14,6 +14,21 @@ const DEPENDENCIES_SECTION: &str = "[dependencies]"; const CLIPPY_PROJECTS: &[ClippyProjectInfo] = &[ ClippyProjectInfo::new("root", "Cargo.toml", "src/driver.rs"), ClippyProjectInfo::new("clippy_lints", "clippy_lints/Cargo.toml", "clippy_lints/src/lib.rs"), + ClippyProjectInfo::new( + "clippy_lints_loops", + "clippy_lints_loops/Cargo.toml", + "clippy_lints_loops/src/lib.rs", + ), + ClippyProjectInfo::new( + "clippy_lints_matches", + "clippy_lints_matches/Cargo.toml", + "clippy_lints_matches/src/lib.rs", + ), + ClippyProjectInfo::new( + "clippy_lints_methods", + "clippy_lints_methods/Cargo.toml", + "clippy_lints_methods/src/lib.rs", + ), ClippyProjectInfo::new("clippy_utils", "clippy_utils/Cargo.toml", "clippy_utils/src/lib.rs"), ]; 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 5f6e874ffe25..146230882a60 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::{LintKind, ParsedData}; +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::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\ @@ -25,91 +21,103 @@ 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); + 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)] -pub fn generate_lint_files( - update_mode: UpdateMode, - lints: &[Lint], - deprecated: &[DeprecatedLint], - renamed: &[RenamedLint], -) { +pub fn generate_lint_files(update_mode: UpdateMode, data: &ParsedData) { let mut updater = FileUpdater::default(); + + let mut lints: Vec<_> = data.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(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.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))); + + // 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[..data.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[data.deprecated_span.end as usize..data.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[data.renamed_span.end as usize..]); UpdateStatus::from_changed(src != dst) }, ); @@ -119,8 +127,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) @@ -131,47 +139,34 @@ 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)); + let krate_name = &*data.source_map.crates[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(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`", |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(); + } } }, ), @@ -179,15 +174,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(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"); - 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"); @@ -196,264 +195,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..e8f91385320b 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}; @@ -71,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 { @@ -82,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 { @@ -426,181 +421,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) { @@ -756,8 +576,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..1675e897f785 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, @@ -265,30 +265,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::literal_representation::UNREADABLE_LITERAL_INFO, crate::literal_representation::UNUSUAL_BYTE_GROUPINGS_INFO, crate::literal_string_with_formatting_args::LITERAL_STRING_WITH_FORMATTING_ARGS_INFO, - crate::loops::CHAR_INDICES_AS_BYTE_INDICES_INFO, - crate::loops::EMPTY_LOOP_INFO, - crate::loops::EXPLICIT_COUNTER_LOOP_INFO, - crate::loops::EXPLICIT_INTO_ITER_LOOP_INFO, - crate::loops::EXPLICIT_ITER_LOOP_INFO, - crate::loops::FOR_KV_MAP_INFO, - crate::loops::INFINITE_LOOP_INFO, - crate::loops::ITER_NEXT_LOOP_INFO, - crate::loops::MANUAL_FIND_INFO, - crate::loops::MANUAL_FLATTEN_INFO, - crate::loops::MANUAL_MEMCPY_INFO, - crate::loops::MANUAL_SLICE_FILL_INFO, - crate::loops::MANUAL_WHILE_LET_SOME_INFO, - crate::loops::MISSING_SPIN_LOOP_INFO, - crate::loops::MUT_RANGE_BOUND_INFO, - crate::loops::NEEDLESS_RANGE_LOOP_INFO, - crate::loops::NEVER_LOOP_INFO, - crate::loops::SAME_ITEM_PUSH_INFO, - crate::loops::SINGLE_ELEMENT_LOOP_INFO, - crate::loops::UNUSED_ENUMERATE_INDEX_INFO, - crate::loops::WHILE_FLOAT_INFO, - crate::loops::WHILE_IMMUTABLE_CONDITION_INFO, - crate::loops::WHILE_LET_LOOP_INFO, - crate::loops::WHILE_LET_ON_ITERATOR_INFO, crate::macro_metavars_in_unsafe::MACRO_METAVARS_IN_UNSAFE_INFO, crate::macro_use::MACRO_USE_IMPORTS_INFO, crate::main_recursion::MAIN_RECURSION_INFO, @@ -315,188 +291,11 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::manual_slice_size_calculation::MANUAL_SLICE_SIZE_CALCULATION_INFO, crate::manual_string_new::MANUAL_STRING_NEW_INFO, crate::manual_strip::MANUAL_STRIP_INFO, - crate::map_unit_fn::OPTION_MAP_UNIT_FN_INFO, - crate::map_unit_fn::RESULT_MAP_UNIT_FN_INFO, crate::match_result_ok::MATCH_RESULT_OK_INFO, - crate::matches::COLLAPSIBLE_MATCH_INFO, - crate::matches::INFALLIBLE_DESTRUCTURING_MATCH_INFO, - crate::matches::MANUAL_FILTER_INFO, - crate::matches::MANUAL_MAP_INFO, - crate::matches::MANUAL_OK_ERR_INFO, - crate::matches::MANUAL_UNWRAP_OR_INFO, - crate::matches::MANUAL_UNWRAP_OR_DEFAULT_INFO, - crate::matches::MATCH_AS_REF_INFO, - crate::matches::MATCH_BOOL_INFO, - crate::matches::MATCH_LIKE_MATCHES_MACRO_INFO, - crate::matches::MATCH_OVERLAPPING_ARM_INFO, - crate::matches::MATCH_REF_PATS_INFO, - 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::NEEDLESS_MATCH_INFO, - crate::matches::REDUNDANT_GUARDS_INFO, - crate::matches::REDUNDANT_PATTERN_MATCHING_INFO, - crate::matches::REST_PAT_IN_FULLY_BOUND_STRUCTS_INFO, - crate::matches::SIGNIFICANT_DROP_IN_SCRUTINEE_INFO, - crate::matches::SINGLE_MATCH_INFO, - crate::matches::SINGLE_MATCH_ELSE_INFO, - crate::matches::TRY_ERR_INFO, - crate::matches::WILDCARD_ENUM_MATCH_ARM_INFO, - crate::matches::WILDCARD_IN_OR_PATTERNS_INFO, crate::mem_replace::MEM_REPLACE_OPTION_WITH_NONE_INFO, crate::mem_replace::MEM_REPLACE_OPTION_WITH_SOME_INFO, crate::mem_replace::MEM_REPLACE_WITH_DEFAULT_INFO, crate::mem_replace::MEM_REPLACE_WITH_UNINIT_INFO, - crate::methods::BIND_INSTEAD_OF_MAP_INFO, - crate::methods::BYTES_COUNT_TO_LEN_INFO, - crate::methods::BYTES_NTH_INFO, - crate::methods::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS_INFO, - 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::COLLAPSIBLE_STR_REPLACE_INFO, - crate::methods::CONST_IS_EMPTY_INFO, - crate::methods::DOUBLE_ENDED_ITERATOR_LAST_INFO, - crate::methods::DRAIN_COLLECT_INFO, - crate::methods::ERR_EXPECT_INFO, - crate::methods::EXPECT_FUN_CALL_INFO, - crate::methods::EXPECT_USED_INFO, - crate::methods::EXTEND_WITH_DRAIN_INFO, - crate::methods::FILETYPE_IS_FILE_INFO, - crate::methods::FILTER_MAP_BOOL_THEN_INFO, - crate::methods::FILTER_MAP_IDENTITY_INFO, - crate::methods::FILTER_MAP_NEXT_INFO, - crate::methods::FILTER_NEXT_INFO, - crate::methods::FLAT_MAP_IDENTITY_INFO, - crate::methods::FLAT_MAP_OPTION_INFO, - crate::methods::FORMAT_COLLECT_INFO, - crate::methods::FROM_ITER_INSTEAD_OF_COLLECT_INFO, - crate::methods::GET_FIRST_INFO, - crate::methods::GET_LAST_WITH_LEN_INFO, - crate::methods::GET_UNWRAP_INFO, - crate::methods::IMPLICIT_CLONE_INFO, - crate::methods::INEFFICIENT_TO_STRING_INFO, - crate::methods::INSPECT_FOR_EACH_INFO, - crate::methods::INTO_ITER_ON_REF_INFO, - 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, - crate::methods::ITER_FILTER_IS_SOME_INFO, - crate::methods::ITER_KV_MAP_INFO, - crate::methods::ITER_NEXT_SLICE_INFO, - crate::methods::ITER_NTH_INFO, - crate::methods::ITER_NTH_ZERO_INFO, - crate::methods::ITER_ON_EMPTY_COLLECTIONS_INFO, - crate::methods::ITER_ON_SINGLE_ITEMS_INFO, - crate::methods::ITER_OUT_OF_BOUNDS_INFO, - crate::methods::ITER_OVEREAGER_CLONED_INFO, - crate::methods::ITER_SKIP_NEXT_INFO, - crate::methods::ITER_SKIP_ZERO_INFO, - crate::methods::ITER_WITH_DRAIN_INFO, - crate::methods::JOIN_ABSOLUTE_PATHS_INFO, - crate::methods::MANUAL_CONTAINS_INFO, - crate::methods::MANUAL_C_STR_LITERALS_INFO, - crate::methods::MANUAL_FILTER_MAP_INFO, - crate::methods::MANUAL_FIND_MAP_INFO, - crate::methods::MANUAL_INSPECT_INFO, - crate::methods::MANUAL_IS_VARIANT_AND_INFO, - crate::methods::MANUAL_NEXT_BACK_INFO, - crate::methods::MANUAL_OK_OR_INFO, - crate::methods::MANUAL_REPEAT_N_INFO, - crate::methods::MANUAL_SATURATING_ARITHMETIC_INFO, - crate::methods::MANUAL_SPLIT_ONCE_INFO, - crate::methods::MANUAL_STR_REPEAT_INFO, - crate::methods::MANUAL_TRY_FOLD_INFO, - crate::methods::MAP_ALL_ANY_IDENTITY_INFO, - crate::methods::MAP_CLONE_INFO, - crate::methods::MAP_COLLECT_RESULT_UNIT_INFO, - crate::methods::MAP_ERR_IGNORE_INFO, - crate::methods::MAP_FLATTEN_INFO, - crate::methods::MAP_IDENTITY_INFO, - crate::methods::MAP_UNWRAP_OR_INFO, - crate::methods::MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES_INFO, - crate::methods::MUT_MUTEX_LOCK_INFO, - crate::methods::NAIVE_BYTECOUNT_INFO, - crate::methods::NEEDLESS_AS_BYTES_INFO, - crate::methods::NEEDLESS_CHARACTER_ITERATION_INFO, - crate::methods::NEEDLESS_COLLECT_INFO, - crate::methods::NEEDLESS_OPTION_AS_DEREF_INFO, - 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::OBFUSCATED_IF_ELSE_INFO, - crate::methods::OK_EXPECT_INFO, - crate::methods::OPTION_AS_REF_CLONED_INFO, - crate::methods::OPTION_AS_REF_DEREF_INFO, - crate::methods::OPTION_FILTER_MAP_INFO, - crate::methods::OPTION_MAP_OR_NONE_INFO, - crate::methods::OR_FUN_CALL_INFO, - crate::methods::OR_THEN_UNWRAP_INFO, - 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::REDUNDANT_AS_STR_INFO, - crate::methods::REPEAT_ONCE_INFO, - crate::methods::RESULT_FILTER_MAP_INFO, - crate::methods::RESULT_MAP_OR_INTO_OPTION_INFO, - crate::methods::RETURN_AND_THEN_INFO, - crate::methods::SEARCH_IS_SOME_INFO, - crate::methods::SEEK_FROM_CURRENT_INFO, - crate::methods::SEEK_TO_START_INSTEAD_OF_REWIND_INFO, - crate::methods::SHOULD_IMPLEMENT_TRAIT_INFO, - crate::methods::SINGLE_CHAR_ADD_STR_INFO, - crate::methods::SKIP_WHILE_NEXT_INFO, - crate::methods::SLICED_STRING_AS_BYTES_INFO, - crate::methods::STABLE_SORT_PRIMITIVE_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, - crate::methods::SUSPICIOUS_SPLITN_INFO, - crate::methods::SUSPICIOUS_TO_OWNED_INFO, - crate::methods::SWAP_WITH_TEMPORARY_INFO, - crate::methods::TYPE_ID_ON_BOX_INFO, - crate::methods::UNBUFFERED_BYTES_INFO, - crate::methods::UNINIT_ASSUMED_INIT_INFO, - crate::methods::UNIT_HASH_INFO, - crate::methods::UNNECESSARY_FALLIBLE_CONVERSIONS_INFO, - crate::methods::UNNECESSARY_FILTER_MAP_INFO, - crate::methods::UNNECESSARY_FIND_MAP_INFO, - crate::methods::UNNECESSARY_FIRST_THEN_CHECK_INFO, - crate::methods::UNNECESSARY_FOLD_INFO, - crate::methods::UNNECESSARY_GET_THEN_CHECK_INFO, - crate::methods::UNNECESSARY_JOIN_INFO, - crate::methods::UNNECESSARY_LAZY_EVALUATIONS_INFO, - crate::methods::UNNECESSARY_LITERAL_UNWRAP_INFO, - crate::methods::UNNECESSARY_MAP_OR_INFO, - crate::methods::UNNECESSARY_MIN_OR_MAX_INFO, - crate::methods::UNNECESSARY_RESULT_MAP_OR_ELSE_INFO, - crate::methods::UNNECESSARY_SORT_BY_INFO, - crate::methods::UNNECESSARY_TO_OWNED_INFO, - crate::methods::UNWRAP_OR_DEFAULT_INFO, - crate::methods::UNWRAP_USED_INFO, - crate::methods::USELESS_ASREF_INFO, - crate::methods::USELESS_NONZERO_NEW_UNCHECKED_INFO, - crate::methods::VEC_RESIZE_TO_ZERO_INFO, - crate::methods::VERBOSE_FILE_READS_INFO, - crate::methods::WAKER_CLONE_WAKE_INFO, - crate::methods::WRONG_SELF_CONVENTION_INFO, - crate::methods::ZST_OFFSET_INFO, crate::min_ident_chars::MIN_IDENT_CHARS_INFO, crate::minmax::MIN_MAX_INFO, crate::misc::SHORT_CIRCUIT_STATEMENT_INFO, @@ -633,8 +432,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 +484,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 +511,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 +518,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 +575,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/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs index 49397938ca73..66f240f6f71f 100644 --- a/clippy_lints/src/doc/mod.rs +++ b/clippy_lints/src/doc/mod.rs @@ -1,5 +1,3 @@ -#![allow(clippy::lint_without_lint_pass)] - use clippy_config::Conf; use clippy_utils::attrs::is_doc_hidden; use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then}; diff --git a/clippy_lints/src/ineffective_open_options.rs b/clippy_lints/src/ineffective_open_options.rs index 7a751514b647..a159f6157183 100644 --- a/clippy_lints/src/ineffective_open_options.rs +++ b/clippy_lints/src/ineffective_open_options.rs @@ -1,13 +1,12 @@ -use crate::methods::method_call; use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::{peel_blocks, sym}; +use clippy_utils::source::SpanRangeExt; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{peel_blocks, peel_hir_expr_while, sym}; use rustc_ast::LitKind; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind}; use rustc_lint::{LateContext, LateLintPass}; -use rustc_middle::ty; use rustc_session::declare_lint_pass; -use rustc_span::{BytePos, Span}; declare_clippy_lint! { /// ### What it does @@ -43,53 +42,58 @@ declare_clippy_lint! { declare_lint_pass!(IneffectiveOpenOptions => [INEFFECTIVE_OPEN_OPTIONS]); -fn index_if_arg_is_boolean(args: &[Expr<'_>], call_span: Span) -> Option { - if let [arg] = args - && let ExprKind::Lit(lit) = peel_blocks(arg).kind - && lit.node == LitKind::Bool(true) - { - // The `.` is not included in the span so we cheat a little bit to include it as well. - Some(call_span.with_lo(call_span.lo() - BytePos(1))) - } else { - None - } -} - impl<'tcx> LateLintPass<'tcx> for IneffectiveOpenOptions { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - let Some((sym::open, mut receiver, [_arg], _, _)) = method_call(expr) else { - return; - }; - let receiver_ty = cx.typeck_results().expr_ty(receiver); - match receiver_ty.peel_refs().kind() { - ty::Adt(adt, _) if cx.tcx.is_diagnostic_item(sym::FsOpenOptions, adt.did()) => {}, - _ => return, - } - - let mut append = None; - let mut write = None; + if let ExprKind::MethodCall(name, recv, [_], _) = expr.kind + && name.ident.name == sym::open + && !expr.span.from_expansion() + && is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv).peel_refs(), sym::FsOpenOptions) + { + let mut append = false; + let mut write = None; + peel_hir_expr_while(recv, |e| { + if let ExprKind::MethodCall(name, recv, args, call_span) = e.kind + && !e.span.from_expansion() + { + if let [arg] = args + && let ExprKind::Lit(lit) = peel_blocks(arg).kind + && matches!(lit.node, LitKind::Bool(true)) + && !arg.span.from_expansion() + && !lit.span.from_expansion() + { + match name.ident.name { + sym::append => append = true, + sym::write + if let Some(range) = call_span.map_range(cx, |_, text, range| { + if text.get(..range.start)?.ends_with('.') { + Some(range.start - 1..range.end) + } else { + None + } + }) => + { + write = Some(call_span.with_lo(range.start)); + }, + _ => {}, + } + } + Some(recv) + } else { + None + } + }); - while let Some((name, recv, args, _, span)) = method_call(receiver) { - if name == sym::append { - append = index_if_arg_is_boolean(args, span); - } else if name == sym::write { - write = index_if_arg_is_boolean(args, span); + if append && let Some(write_span) = write { + span_lint_and_sugg( + cx, + INEFFECTIVE_OPEN_OPTIONS, + write_span, + "unnecessary use of `.write(true)` because there is `.append(true)`", + "remove `.write(true)`", + String::new(), + Applicability::MachineApplicable, + ); } - receiver = recv; - } - - if let Some(write_span) = write - && append.is_some() - { - span_lint_and_sugg( - cx, - INEFFECTIVE_OPEN_OPTIONS, - write_span, - "unnecessary use of `.write(true)` because there is `.append(true)`", - "remove `.write(true)`", - String::new(), - Applicability::MachineApplicable, - ); } } } diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 96a6dee58852..234ce1603a86 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -32,7 +32,6 @@ // (Currently there is no way to opt into sysroot crates without `extern crate`.) extern crate pulldown_cmark; extern crate rustc_abi; -extern crate rustc_arena; extern crate rustc_ast; extern crate rustc_ast_pretty; extern crate rustc_attr_data_structures; @@ -55,7 +54,6 @@ extern crate rustc_session; extern crate rustc_span; extern crate rustc_target; extern crate rustc_trait_selection; -extern crate smallvec; extern crate thin_vec; #[macro_use] @@ -201,7 +199,6 @@ mod lifetimes; mod lines_filter_map_ok; mod literal_representation; mod literal_string_with_formatting_args; -mod loops; mod macro_metavars_in_unsafe; mod macro_use; mod main_recursion; @@ -227,11 +224,8 @@ mod manual_rotate; mod manual_slice_size_calculation; mod manual_string_new; mod manual_strip; -mod map_unit_fn; mod match_result_ok; -mod matches; mod mem_replace; -mod methods; mod min_ident_chars; mod minmax; mod misc; @@ -410,7 +404,6 @@ mod zombie_processes; use clippy_config::{Conf, get_configuration_metadata, sanitize_explanation}; use clippy_utils::macros::FormatArgsStorage; -use rustc_data_structures::fx::FxHashSet; use rustc_lint::Lint; use utils::attr_collector::{AttrCollector, AttrStorage}; @@ -440,7 +433,7 @@ pub fn explain(name: &str) -> i32 { /// /// Used in `./src/driver.rs`. #[expect(clippy::too_many_lines)] -pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Conf) { +pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Conf) -> FormatArgsStorage { for (old_name, new_name) in deprecated_lints::RENAMED { store.register_renamed(old_name, new_name); } @@ -503,9 +496,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co store.register_late_pass(|_| Box::new(non_octal_unix_permissions::NonOctalUnixPermissions)); store.register_early_pass(|| Box::new(unnecessary_self_imports::UnnecessarySelfImports)); store.register_late_pass(move |_| Box::new(approx_const::ApproxConstant::new(conf))); - let format_args = format_args_storage.clone(); - store.register_late_pass(move |_| Box::new(methods::Methods::new(conf, format_args.clone()))); - store.register_late_pass(move |_| Box::new(matches::Matches::new(conf))); store.register_late_pass(move |_| Box::new(manual_non_exhaustive::ManualNonExhaustive::new(conf))); store.register_late_pass(move |_| Box::new(manual_strip::ManualStrip::new(conf))); store.register_early_pass(move || Box::new(redundant_static_lifetimes::RedundantStaticLifetimes::new(conf))); @@ -524,7 +514,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co store.register_late_pass(move |_| Box::new(index_refutable_slice::IndexRefutableSlice::new(conf))); store.register_late_pass(|_| Box::::default()); store.register_late_pass(|_| Box::new(unit_types::UnitTypes)); - store.register_late_pass(move |_| Box::new(loops::Loops::new(conf))); store.register_late_pass(|_| Box::::default()); store.register_late_pass(move |_| Box::new(lifetimes::Lifetimes::new(conf))); store.register_late_pass(|_| Box::new(entry::HashMapPass)); @@ -584,7 +573,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co store.register_late_pass(|_| Box::new(question_mark_used::QuestionMarkUsed)); store.register_early_pass(|| Box::new(suspicious_operation_groupings::SuspiciousOperationGroupings)); store.register_late_pass(|_| Box::new(suspicious_trait_impl::SuspiciousImpl)); - store.register_late_pass(|_| Box::new(map_unit_fn::MapUnit)); store.register_late_pass(|_| Box::new(inherent_impl::MultipleInherentImpl)); store.register_late_pass(|_| Box::new(neg_cmp_op_on_partial_ord::NoNegCompOpForPartialOrd)); store.register_late_pass(|_| Box::new(unwrap::Unwrap)); @@ -831,4 +819,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co store.register_late_pass(|_| Box::new(infallible_try_from::InfallibleTryFrom)); store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny)); // add lints here, do not remove this comment, it's used in `new_lint` + + format_args_storage } diff --git a/clippy_lints/src/loops/empty_loop.rs b/clippy_lints/src/loops/empty_loop.rs deleted file mode 100644 index e809987d75a0..000000000000 --- a/clippy_lints/src/loops/empty_loop.rs +++ /dev/null @@ -1,29 +0,0 @@ -use super::EMPTY_LOOP; -use clippy_utils::diagnostics::span_lint_and_help; -use clippy_utils::{is_in_panic_handler, is_no_std_crate, sym}; - -use rustc_hir::{Block, Expr, ItemKind, Node}; -use rustc_lint::LateContext; - -pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, loop_block: &Block<'_>) { - let parent_hir_id = cx.tcx.parent_hir_id(expr.hir_id); - if let Node::Item(parent_node) = cx.tcx.hir_node(parent_hir_id) - && matches!(parent_node.kind, ItemKind::Fn { .. }) - && let attrs = cx.tcx.hir_attrs(parent_hir_id) - && attrs.iter().any(|attr| attr.has_name(sym::rustc_intrinsic)) - { - // Intrinsic functions are expanded into an empty loop when lowering the AST - // to simplify the job of later passes which might expect any function to have a body. - return; - } - - if loop_block.stmts.is_empty() && loop_block.expr.is_none() && !is_in_panic_handler(cx, expr) { - let msg = "empty `loop {}` wastes CPU cycles"; - let help = if is_no_std_crate(cx) { - "you should either use `panic!()` or add a call pausing or sleeping the thread to the loop body" - } else { - "you should either use `panic!()` or add `std::thread::sleep(..);` to the loop body" - }; - span_lint_and_help(cx, EMPTY_LOOP, expr.span, msg, None, help); - } -} diff --git a/clippy_lints/src/loops/iter_next_loop.rs b/clippy_lints/src/loops/iter_next_loop.rs deleted file mode 100644 index b8a263817d29..000000000000 --- a/clippy_lints/src/loops/iter_next_loop.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::ITER_NEXT_LOOP; -use clippy_utils::diagnostics::span_lint; -use clippy_utils::is_trait_method; -use rustc_hir::Expr; -use rustc_lint::LateContext; -use rustc_span::sym; - -pub(super) fn check(cx: &LateContext<'_>, arg: &Expr<'_>) { - if is_trait_method(cx, arg, sym::Iterator) { - span_lint( - cx, - ITER_NEXT_LOOP, - arg.span, - "you are iterating over `Iterator::next()` which is an Option; this will compile but is \ - probably not what you want", - ); - } -} diff --git a/clippy_lints/src/loops/mod.rs b/clippy_lints/src/loops/mod.rs deleted file mode 100644 index 01c36b8cb12f..000000000000 --- a/clippy_lints/src/loops/mod.rs +++ /dev/null @@ -1,929 +0,0 @@ -mod char_indices_as_byte_indices; -mod empty_loop; -mod explicit_counter_loop; -mod explicit_into_iter_loop; -mod explicit_iter_loop; -mod for_kv_map; -mod infinite_loop; -mod iter_next_loop; -mod manual_find; -mod manual_flatten; -mod manual_memcpy; -mod manual_slice_fill; -mod manual_while_let_some; -mod missing_spin_loop; -mod mut_range_bound; -mod needless_range_loop; -mod never_loop; -mod same_item_push; -mod single_element_loop; -mod unused_enumerate_index; -mod utils; -mod while_float; -mod while_immutable_condition; -mod while_let_loop; -mod while_let_on_iterator; - -use clippy_config::Conf; -use clippy_utils::msrvs::Msrv; -use clippy_utils::{higher, sym}; -use rustc_ast::Label; -use rustc_hir::{Expr, ExprKind, LoopSource, Pat}; -use rustc_lint::{LateContext, LateLintPass}; -use rustc_session::impl_lint_pass; -use rustc_span::Span; -use utils::{IncrementVisitor, InitializeVisitor, make_iterator_snippet}; - -declare_clippy_lint! { - /// ### What it does - /// Checks for for-loops that manually copy items between - /// slices that could be optimized by having a memcpy. - /// - /// ### Why is this bad? - /// It is not as fast as a memcpy. - /// - /// ### Example - /// ```no_run - /// # let src = vec![1]; - /// # let mut dst = vec![0; 65]; - /// for i in 0..src.len() { - /// dst[i + 64] = src[i]; - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// # let src = vec![1]; - /// # let mut dst = vec![0; 65]; - /// dst[64..(src.len() + 64)].clone_from_slice(&src[..]); - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MANUAL_MEMCPY, - perf, - "manually copying items between slices" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for looping over the range of `0..len` of some - /// collection just to get the values by index. - /// - /// ### Why is this bad? - /// Just iterating the collection itself makes the intent - /// more clear and is probably faster because it eliminates - /// the bounds check that is done when indexing. - /// - /// ### Example - /// ```no_run - /// let vec = vec!['a', 'b', 'c']; - /// for i in 0..vec.len() { - /// println!("{}", vec[i]); - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// let vec = vec!['a', 'b', 'c']; - /// for i in vec { - /// println!("{}", i); - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub NEEDLESS_RANGE_LOOP, - style, - "for-looping over a range of indices where an iterator over items would do" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for loops on `x.iter()` where `&x` will do, and - /// suggests the latter. - /// - /// ### Why is this bad? - /// Readability. - /// - /// ### Known problems - /// False negatives. We currently only warn on some known - /// types. - /// - /// ### Example - /// ```no_run - /// // with `y` a `Vec` or slice: - /// # let y = vec![1]; - /// for x in y.iter() { - /// // .. - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// # let y = vec![1]; - /// for x in &y { - /// // .. - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub EXPLICIT_ITER_LOOP, - pedantic, - "for-looping over `_.iter()` or `_.iter_mut()` when `&_` or `&mut _` would do" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for loops on `y.into_iter()` where `y` will do, and - /// suggests the latter. - /// - /// ### Why is this bad? - /// Readability. - /// - /// ### Example - /// ```no_run - /// # let y = vec![1]; - /// // with `y` a `Vec` or slice: - /// for x in y.into_iter() { - /// // .. - /// } - /// ``` - /// can be rewritten to - /// ```no_run - /// # let y = vec![1]; - /// for x in y { - /// // .. - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub EXPLICIT_INTO_ITER_LOOP, - pedantic, - "for-looping over `_.into_iter()` when `_` would do" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for loops on `x.next()`. - /// - /// ### Why is this bad? - /// `next()` returns either `Some(value)` if there was a - /// value, or `None` otherwise. The insidious thing is that `Option<_>` - /// implements `IntoIterator`, so that possibly one value will be iterated, - /// leading to some hard to find bugs. No one will want to write such code - /// [except to win an Underhanded Rust - /// Contest](https://www.reddit.com/r/rust/comments/3hb0wm/underhanded_rust_contest/cu5yuhr). - /// - /// ### Example - /// ```ignore - /// for x in y.next() { - /// .. - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub ITER_NEXT_LOOP, - correctness, - "for-looping over `_.next()` which is probably not intended" -} - -declare_clippy_lint! { - /// ### What it does - /// Detects `loop + match` combinations that are easier - /// written as a `while let` loop. - /// - /// ### Why is this bad? - /// The `while let` loop is usually shorter and more - /// readable. - /// - /// ### Example - /// ```rust,no_run - /// let y = Some(1); - /// loop { - /// let x = match y { - /// Some(x) => x, - /// None => break, - /// }; - /// // .. - /// } - /// ``` - /// Use instead: - /// ```rust,no_run - /// let y = Some(1); - /// while let Some(x) = y { - /// // .. - /// }; - /// ``` - #[clippy::version = "pre 1.29.0"] - pub WHILE_LET_LOOP, - complexity, - "`loop { if let { ... } else break }`, which can be written as a `while let` loop" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks `for` loops over slices with an explicit counter - /// and suggests the use of `.enumerate()`. - /// - /// ### Why is this bad? - /// Using `.enumerate()` makes the intent more clear, - /// declutters the code and may be faster in some instances. - /// - /// ### Example - /// ```no_run - /// # let v = vec![1]; - /// # fn bar(bar: usize, baz: usize) {} - /// let mut i = 0; - /// for item in &v { - /// bar(i, *item); - /// i += 1; - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// # let v = vec![1]; - /// # fn bar(bar: usize, baz: usize) {} - /// for (i, item) in v.iter().enumerate() { bar(i, *item); } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub EXPLICIT_COUNTER_LOOP, - complexity, - "for-looping with an explicit counter when `_.enumerate()` would do" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for empty `loop` expressions. - /// - /// ### Why is this bad? - /// These busy loops burn CPU cycles without doing - /// anything. It is _almost always_ a better idea to `panic!` than to have - /// a busy loop. - /// - /// If panicking isn't possible, think of the environment and either: - /// - block on something - /// - sleep the thread for some microseconds - /// - yield or pause the thread - /// - /// For `std` targets, this can be done with - /// [`std::thread::sleep`](https://doc.rust-lang.org/std/thread/fn.sleep.html) - /// or [`std::thread::yield_now`](https://doc.rust-lang.org/std/thread/fn.yield_now.html). - /// - /// For `no_std` targets, doing this is more complicated, especially because - /// `#[panic_handler]`s can't panic. To stop/pause the thread, you will - /// probably need to invoke some target-specific intrinsic. Examples include: - /// - [`x86_64::instructions::hlt`](https://docs.rs/x86_64/0.12.2/x86_64/instructions/fn.hlt.html) - /// - [`cortex_m::asm::wfi`](https://docs.rs/cortex-m/0.6.3/cortex_m/asm/fn.wfi.html) - /// - /// ### Example - /// ```no_run - /// loop {} - /// ``` - #[clippy::version = "pre 1.29.0"] - pub EMPTY_LOOP, - suspicious, - "empty `loop {}`, which should block or sleep" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for `while let` expressions on iterators. - /// - /// ### Why is this bad? - /// Readability. A simple `for` loop is shorter and conveys - /// the intent better. - /// - /// ### Example - /// ```ignore - /// while let Some(val) = iter.next() { - /// .. - /// } - /// ``` - /// - /// Use instead: - /// ```ignore - /// for val in &mut iter { - /// .. - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub WHILE_LET_ON_ITERATOR, - style, - "using a `while let` loop instead of a for loop on an iterator" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for iterating a map (`HashMap` or `BTreeMap`) and - /// ignoring either the keys or values. - /// - /// ### Why is this bad? - /// Readability. There are `keys` and `values` methods that - /// can be used to express that don't need the values or keys. - /// - /// ### Example - /// ```ignore - /// for (k, _) in &map { - /// .. - /// } - /// ``` - /// - /// could be replaced by - /// - /// ```ignore - /// for k in map.keys() { - /// .. - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub FOR_KV_MAP, - style, - "looping on a map using `iter` when `keys` or `values` would do" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for loops that will always `break`, `return` or - /// `continue` an outer loop. - /// - /// ### Why is this bad? - /// This loop never loops, all it does is obfuscating the - /// code. - /// - /// ### Example - /// ```no_run - /// loop { - /// ..; - /// break; - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub NEVER_LOOP, - correctness, - "any loop that will always `break` or `return`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for loops with a range bound that is a mutable variable. - /// - /// ### Why is this bad? - /// One might think that modifying the mutable variable changes the loop bounds. It doesn't. - /// - /// ### Known problems - /// False positive when mutation is followed by a `break`, but the `break` is not immediately - /// after the mutation: - /// - /// ```no_run - /// let mut x = 5; - /// for _ in 0..x { - /// x += 1; // x is a range bound that is mutated - /// ..; // some other expression - /// break; // leaves the loop, so mutation is not an issue - /// } - /// ``` - /// - /// False positive on nested loops ([#6072](https://github.com/rust-lang/rust-clippy/issues/6072)) - /// - /// ### Example - /// ```no_run - /// let mut foo = 42; - /// for i in 0..foo { - /// foo -= 1; - /// println!("{i}"); // prints numbers from 0 to 41, not 0 to 21 - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MUT_RANGE_BOUND, - suspicious, - "for loop over a range where one of the bounds is a mutable variable" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks whether variables used within while loop condition - /// can be (and are) mutated in the body. - /// - /// ### Why is this bad? - /// If the condition is unchanged, entering the body of the loop - /// will lead to an infinite loop. - /// - /// ### Known problems - /// If the `while`-loop is in a closure, the check for mutation of the - /// condition variables in the body can cause false negatives. For example when only `Upvar` `a` is - /// in the condition and only `Upvar` `b` gets mutated in the body, the lint will not trigger. - /// - /// ### Example - /// ```no_run - /// let i = 0; - /// while i > 10 { - /// println!("let me loop forever!"); - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub WHILE_IMMUTABLE_CONDITION, - correctness, - "variables used within while expression are not mutated in the body" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for while loops comparing floating point values. - /// - /// ### Why is this bad? - /// If you increment floating point values, errors can compound, - /// so, use integers instead if possible. - /// - /// ### Known problems - /// The lint will catch all while loops comparing floating point - /// values without regarding the increment. - /// - /// ### Example - /// ```no_run - /// let mut x = 0.0; - /// while x < 42.0 { - /// x += 1.0; - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// let mut x = 0; - /// while x < 42 { - /// x += 1; - /// } - /// ``` - #[clippy::version = "1.80.0"] - pub WHILE_FLOAT, - nursery, - "while loops comparing floating point values" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks whether a for loop is being used to push a constant - /// value into a Vec. - /// - /// ### Why is this bad? - /// This kind of operation can be expressed more succinctly with - /// `vec![item; SIZE]` or `vec.resize(NEW_SIZE, item)` and using these alternatives may also - /// have better performance. - /// - /// ### Example - /// ```no_run - /// let item1 = 2; - /// let item2 = 3; - /// let mut vec: Vec = Vec::new(); - /// for _ in 0..20 { - /// vec.push(item1); - /// } - /// for _ in 0..30 { - /// vec.push(item2); - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// let item1 = 2; - /// let item2 = 3; - /// let mut vec: Vec = vec![item1; 20]; - /// vec.resize(20 + 30, item2); - /// ``` - #[clippy::version = "1.47.0"] - pub SAME_ITEM_PUSH, - style, - "the same item is pushed inside of a for loop" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks whether a for loop has a single element. - /// - /// ### Why is this bad? - /// There is no reason to have a loop of a - /// single element. - /// - /// ### Example - /// ```no_run - /// let item1 = 2; - /// for item in &[item1] { - /// println!("{}", item); - /// } - /// ``` - /// - /// Use instead: - /// ```no_run - /// let item1 = 2; - /// let item = &item1; - /// println!("{}", item); - /// ``` - #[clippy::version = "1.49.0"] - pub SINGLE_ELEMENT_LOOP, - complexity, - "there is no reason to have a single element loop" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for unnecessary `if let` usage in a for loop - /// where only the `Some` or `Ok` variant of the iterator element is used. - /// - /// ### Why is this bad? - /// It is verbose and can be simplified - /// by first calling the `flatten` method on the `Iterator`. - /// - /// ### Example - /// - /// ```no_run - /// let x = vec![Some(1), Some(2), Some(3)]; - /// for n in x { - /// if let Some(n) = n { - /// println!("{}", n); - /// } - /// } - /// ``` - /// Use instead: - /// ```no_run - /// let x = vec![Some(1), Some(2), Some(3)]; - /// for n in x.into_iter().flatten() { - /// println!("{}", n); - /// } - /// ``` - #[clippy::version = "1.52.0"] - pub MANUAL_FLATTEN, - complexity, - "for loops over `Option`s or `Result`s with a single expression can be simplified" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for empty spin loops - /// - /// ### Why is this bad? - /// The loop body should have something like `thread::park()` or at least - /// `std::hint::spin_loop()` to avoid needlessly burning cycles and conserve - /// energy. Perhaps even better use an actual lock, if possible. - /// - /// ### Known problems - /// This lint doesn't currently trigger on `while let` or - /// `loop { match .. { .. } }` loops, which would be considered idiomatic in - /// combination with e.g. `AtomicBool::compare_exchange_weak`. - /// - /// ### Example - /// - /// ```ignore - /// use core::sync::atomic::{AtomicBool, Ordering}; - /// let b = AtomicBool::new(true); - /// // give a ref to `b` to another thread,wait for it to become false - /// while b.load(Ordering::Acquire) {}; - /// ``` - /// Use instead: - /// ```rust,no_run - ///# use core::sync::atomic::{AtomicBool, Ordering}; - ///# let b = AtomicBool::new(true); - /// while b.load(Ordering::Acquire) { - /// std::hint::spin_loop() - /// } - /// ``` - #[clippy::version = "1.61.0"] - pub MISSING_SPIN_LOOP, - perf, - "An empty busy waiting loop" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for manual implementations of Iterator::find - /// - /// ### Why is this bad? - /// It doesn't affect performance, but using `find` is shorter and easier to read. - /// - /// ### Example - /// - /// ```no_run - /// fn example(arr: Vec) -> Option { - /// for el in arr { - /// if el == 1 { - /// return Some(el); - /// } - /// } - /// None - /// } - /// ``` - /// Use instead: - /// ```no_run - /// fn example(arr: Vec) -> Option { - /// arr.into_iter().find(|&el| el == 1) - /// } - /// ``` - #[clippy::version = "1.64.0"] - pub MANUAL_FIND, - complexity, - "manual implementation of `Iterator::find`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for uses of the `enumerate` method where the index is unused (`_`) - /// - /// ### Why is this bad? - /// The index from `.enumerate()` is immediately dropped. - /// - /// ### Example - /// ```rust - /// let v = vec![1, 2, 3, 4]; - /// for (_, x) in v.iter().enumerate() { - /// println!("{x}"); - /// } - /// ``` - /// Use instead: - /// ```rust - /// let v = vec![1, 2, 3, 4]; - /// for x in v.iter() { - /// println!("{x}"); - /// } - /// ``` - #[clippy::version = "1.75.0"] - pub UNUSED_ENUMERATE_INDEX, - style, - "using `.enumerate()` and immediately dropping the index" -} - -declare_clippy_lint! { - /// ### What it does - /// Looks for loops that check for emptiness of a `Vec` in the condition and pop an element - /// in the body as a separate operation. - /// - /// ### Why is this bad? - /// Such loops can be written in a more idiomatic way by using a while-let loop and directly - /// pattern matching on the return value of `Vec::pop()`. - /// - /// ### Example - /// ```no_run - /// let mut numbers = vec![1, 2, 3, 4, 5]; - /// while !numbers.is_empty() { - /// let number = numbers.pop().unwrap(); - /// // use `number` - /// } - /// ``` - /// Use instead: - /// ```no_run - /// let mut numbers = vec![1, 2, 3, 4, 5]; - /// while let Some(number) = numbers.pop() { - /// // use `number` - /// } - /// ``` - #[clippy::version = "1.71.0"] - pub MANUAL_WHILE_LET_SOME, - style, - "checking for emptiness of a `Vec` in the loop condition and popping an element in the body" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for infinite loops in a function where the return type is not `!` - /// and lint accordingly. - /// - /// ### Why restrict this? - /// Making the return type `!` serves as documentation that the function does not return. - /// If the function is not intended to loop infinitely, then this lint may detect a bug. - /// - /// ### Example - /// ```no_run,ignore - /// fn run_forever() { - /// loop { - /// // do something - /// } - /// } - /// ``` - /// If infinite loops are as intended: - /// ```no_run,ignore - /// fn run_forever() -> ! { - /// loop { - /// // do something - /// } - /// } - /// ``` - /// Otherwise add a `break` or `return` condition: - /// ```no_run,ignore - /// fn run_forever() { - /// loop { - /// // do something - /// if condition { - /// break; - /// } - /// } - /// } - /// ``` - #[clippy::version = "1.76.0"] - pub INFINITE_LOOP, - restriction, - "possibly unintended infinite loop" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for manually filling a slice with a value. - /// - /// ### Why is this bad? - /// Using the `fill` method is more idiomatic and concise. - /// - /// ### Example - /// ```no_run - /// let mut some_slice = [1, 2, 3, 4, 5]; - /// for i in 0..some_slice.len() { - /// some_slice[i] = 0; - /// } - /// ``` - /// Use instead: - /// ```no_run - /// let mut some_slice = [1, 2, 3, 4, 5]; - /// some_slice.fill(0); - /// ``` - #[clippy::version = "1.86.0"] - pub MANUAL_SLICE_FILL, - style, - "manually filling a slice with a value" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for usage of a character position yielded by `.chars().enumerate()` in a context where a **byte index** is expected, - /// such as an argument to a specific `str` method or indexing into a `str` or `String`. - /// - /// ### Why is this bad? - /// A character (more specifically, a Unicode scalar value) that is yielded by `str::chars` can take up multiple bytes, - /// so a character position does not necessarily have the same byte index at which the character is stored. - /// Thus, using the character position where a byte index is expected can unexpectedly return wrong values - /// or panic when the string consists of multibyte characters. - /// - /// For example, the character `a` in `äa` is stored at byte index 2 but has the character position 1. - /// Using the character position 1 to index into the string will lead to a panic as it is in the middle of the first character. - /// - /// Instead of `.chars().enumerate()`, the correct iterator to use is `.char_indices()`, which yields byte indices. - /// - /// This pattern is technically fine if the strings are known to only use the ASCII subset, - /// though in those cases it would be better to use `bytes()` directly to make the intent clearer, - /// but there is also no downside to just using `.char_indices()` directly and supporting non-ASCII strings. - /// - /// You may also want to read the [chapter on strings in the Rust Book](https://doc.rust-lang.org/book/ch08-02-strings.html) - /// which goes into this in more detail. - /// - /// ### Example - /// ```no_run - /// # let s = "..."; - /// for (idx, c) in s.chars().enumerate() { - /// let _ = s[idx..]; // ⚠️ Panics for strings consisting of multibyte characters - /// } - /// ``` - /// Use instead: - /// ```no_run - /// # let s = "..."; - /// for (idx, c) in s.char_indices() { - /// let _ = s[idx..]; - /// } - /// ``` - #[clippy::version = "1.88.0"] - pub CHAR_INDICES_AS_BYTE_INDICES, - correctness, - "using the character position yielded by `.chars().enumerate()` in a context where a byte index is expected" -} - -pub struct Loops { - msrv: Msrv, - enforce_iter_loop_reborrow: bool, -} -impl Loops { - pub fn new(conf: &'static Conf) -> Self { - Self { - msrv: conf.msrv, - enforce_iter_loop_reborrow: conf.enforce_iter_loop_reborrow, - } - } -} - -impl_lint_pass!(Loops => [ - MANUAL_MEMCPY, - MANUAL_FLATTEN, - NEEDLESS_RANGE_LOOP, - EXPLICIT_ITER_LOOP, - EXPLICIT_INTO_ITER_LOOP, - ITER_NEXT_LOOP, - WHILE_LET_LOOP, - EXPLICIT_COUNTER_LOOP, - EMPTY_LOOP, - WHILE_LET_ON_ITERATOR, - FOR_KV_MAP, - NEVER_LOOP, - MUT_RANGE_BOUND, - WHILE_IMMUTABLE_CONDITION, - WHILE_FLOAT, - SAME_ITEM_PUSH, - SINGLE_ELEMENT_LOOP, - MISSING_SPIN_LOOP, - MANUAL_FIND, - MANUAL_WHILE_LET_SOME, - UNUSED_ENUMERATE_INDEX, - INFINITE_LOOP, - MANUAL_SLICE_FILL, - CHAR_INDICES_AS_BYTE_INDICES, -]); - -impl<'tcx> LateLintPass<'tcx> for Loops { - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - let for_loop = higher::ForLoop::hir(expr); - if let Some(higher::ForLoop { - pat, - arg, - body, - loop_id, - span, - label, - }) = for_loop - { - // we don't want to check expanded macros - // this check is not at the top of the function - // since higher::for_loop expressions are marked as expansions - if body.span.from_expansion() { - return; - } - self.check_for_loop(cx, pat, arg, body, expr, span, label); - if let ExprKind::Block(block, _) = body.kind { - never_loop::check(cx, block, loop_id, span, for_loop.as_ref()); - } - } - - // we don't want to check expanded macros - if expr.span.from_expansion() { - return; - } - - // check for never_loop - if let ExprKind::Loop(block, ..) = expr.kind { - never_loop::check(cx, block, expr.hir_id, expr.span, None); - } - - // check for `loop { if let {} else break }` that could be `while let` - // (also matches an explicit "match" instead of "if let") - // (even if the "match" or "if let" is used for declaration) - if let ExprKind::Loop(block, label, LoopSource::Loop, _) = expr.kind { - // also check for empty `loop {}` statements, skipping those in #[panic_handler] - empty_loop::check(cx, expr, block); - while_let_loop::check(cx, expr, block); - infinite_loop::check(cx, expr, block, label); - } - - while_let_on_iterator::check(cx, expr); - - if let Some(higher::While { condition, body, span }) = higher::While::hir(expr) { - while_immutable_condition::check(cx, condition, body); - while_float::check(cx, condition); - missing_spin_loop::check(cx, condition, body); - manual_while_let_some::check(cx, condition, body, span); - } - } -} - -impl Loops { - #[allow(clippy::too_many_arguments)] - fn check_for_loop<'tcx>( - &self, - cx: &LateContext<'tcx>, - pat: &'tcx Pat<'_>, - arg: &'tcx Expr<'_>, - body: &'tcx Expr<'_>, - expr: &'tcx Expr<'_>, - span: Span, - label: Option