From 0cbb909e1983ff318dd6e75a1fec004a707aeb0d Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sat, 7 Feb 2026 09:48:18 -0500 Subject: [PATCH 01/11] `clippy_dev`: Don't store strings in token search patterns. --- clippy_dev/src/edit_lints.rs | 4 ++-- clippy_dev/src/parse.rs | 27 ++++++++++++--------- clippy_dev/src/parse/cursor.rs | 44 ++++++++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index 2ea3a84bdc71..59013ca820d9 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -333,7 +333,7 @@ fn uplift_update_fn<'a>( while let Some(ident) = cursor.find_any_ident() { match cursor.get_text(ident) { "mod" - if remove_mod && cursor.match_all(&[cursor::Pat::Ident(old_name), cursor::Pat::Semi], &mut []) => + if remove_mod && cursor.match_ident(old_name).is_some() && cursor.match_pat(cursor::Pat::Semi) => { dst.push_str(&src[copy_pos as usize..ident.pos as usize]); dst.push_str(new_name); @@ -343,7 +343,7 @@ fn uplift_update_fn<'a>( } changed = true; }, - "clippy" if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::Ident(old_name)], &mut []) => { + "clippy" if cursor.match_pat(cursor::Pat::DoubleColon) && cursor.match_ident(old_name).is_some() => { dst.push_str(&src[copy_pos as usize..ident.pos as usize]); dst.push_str(new_name); copy_pos = cursor.pos(); diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index ff6c63f61d86..d4e7fdfdba52 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -1,6 +1,6 @@ pub mod cursor; -use self::cursor::{Capture, Cursor}; +use self::cursor::{Capture, Cursor, IdentPat}; use crate::utils::{ErrAction, File, Scoped, expect_action, slice_groups_mut, walk_dir_no_dot_or_target}; use core::cell::Cell; use core::fmt::{self, Display, Write as _}; @@ -352,16 +352,17 @@ impl<'cx> ParseCxImpl<'cx> { #[allow(clippy::enum_glob_use)] use cursor::Pat::*; #[rustfmt::skip] - static LINT_DECL_TOKENS: &[cursor::Pat<'_>] = &[ + static LINT_DECL_TOKENS: &[cursor::Pat] = &[ // !{ /// docs Bang, OpenBrace, AnyComment, // #[clippy::version = "version"] - Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket, + Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, + Ident(IdentPat::version), Eq, LitStr, CloseBracket, // pub NAME, GROUP, - Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, + Ident(IdentPat::r#pub), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, ]; #[rustfmt::skip] - static PASS_DECL_TOKENS: &[cursor::Pat<'_>] = &[ + static PASS_DECL_TOKENS: &[cursor::Pat] = &[ // !( NAME <'lt> => [ Bang, OpenParen, CaptureDocLines, CaptureIdent, CaptureOptLifetimeArg, FatArrow, OpenBracket, ]; @@ -453,22 +454,26 @@ impl<'cx> ParseCxImpl<'cx> { fn parse_deprecated_lints(&mut self, data: &mut LintData<'cx>) { #[allow(clippy::enum_glob_use)] use cursor::Pat::*; + #[rustfmt::skip] - static DECL_TOKENS: &[cursor::Pat<'_>] = &[ + static DECL_TOKENS: &[cursor::Pat] = &[ // #[clippy::version = "version"] - Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket, + Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, + Ident(IdentPat::version), Eq, CaptureLitStr, CloseBracket, // ("first", "second"), OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma, ]; #[rustfmt::skip] - static DEPRECATED_TOKENS: &[cursor::Pat<'_>] = &[ + static DEPRECATED_TOKENS: &[cursor::Pat] = &[ // !{ DEPRECATED(DEPRECATED_VERSION) = [ - Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket, + Bang, OpenBrace, Ident(IdentPat::DEPRECATED), OpenParen, + Ident(IdentPat::DEPRECATED_VERSION), CloseParen, Eq, OpenBracket, ]; #[rustfmt::skip] - static RENAMED_TOKENS: &[cursor::Pat<'_>] = &[ + static RENAMED_TOKENS: &[cursor::Pat] = &[ // !{ RENAMED(RENAMED_VERSION) = [ - Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket, + Bang, OpenBrace, Ident(IdentPat::RENAMED), OpenParen, + Ident(IdentPat::RENAMED_VERSION), CloseParen, Eq, OpenBracket, ]; let file = data.deprecated_file; diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index af840532c27b..5a30931040a3 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -8,10 +8,10 @@ use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; /// `DoubleColon` will consume the first `:` and then fail to match, leaving the cursor at /// the `*`. #[derive(Clone, Copy)] -pub enum Pat<'a> { +pub enum Pat { + Ident(IdentPat), /// Matches any number of comments and doc comments. AnyComment, - Ident(&'a str), CaptureDocLines, CaptureIdent, LitStr, @@ -35,6 +35,36 @@ pub enum Pat<'a> { Semi, } +macro_rules! ident_or_lit { + ($ident:ident) => { + stringify!($ident) + }; + ($_ident:ident $lit:literal) => { + $lit + }; +} +macro_rules! decl_ident_pats { + ($($ident:ident $(= $s:literal)?,)*) => { + #[allow(non_camel_case_types, clippy::upper_case_acronyms)] + #[derive(Clone, Copy)] + pub enum IdentPat { $($ident),* } + impl IdentPat { + pub fn as_str(self) -> &'static str { + match self { $(Self::$ident => ident_or_lit!($ident $($s)?)),* } + } + } + } +} +decl_ident_pats! { + DEPRECATED, + DEPRECATED_VERSION, + RENAMED, + RENAMED_VERSION, + clippy, + r#pub = "pub", + version, +} + #[derive(Clone, Copy)] pub struct Capture { pub pos: u32, @@ -116,7 +146,7 @@ impl<'txt> Cursor<'txt> { /// For each capture made by the pattern one item will be taken from the capture /// sequence with the result placed inside. #[expect(clippy::too_many_lines)] - fn match_impl(&mut self, pat: Pat<'_>, captures: &mut slice::IterMut<'_, Capture>) -> bool { + fn match_impl(&mut self, pat: Pat, captures: &mut slice::IterMut<'_, Capture>) -> bool { loop { match (pat, self.next_token.kind) { #[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/6697 @@ -149,7 +179,7 @@ impl<'txt> Cursor<'txt> { self.step(); return true; }, - (Pat::Ident(x), TokenKind::Ident) if x == self.peek_text() => { + (Pat::Ident(x), TokenKind::Ident) if x.as_str() == self.peek_text() => { self.step(); return true; }, @@ -319,7 +349,7 @@ impl<'txt> Cursor<'txt> { /// Not generally suitable for multi-token patterns or patterns that can match /// nothing. #[must_use] - pub fn find_pat(&mut self, pat: Pat<'_>) -> bool { + pub fn find_pat(&mut self, pat: Pat) -> bool { let mut capture = [].iter_mut(); while !self.match_impl(pat, &mut capture) { self.step(); @@ -340,7 +370,7 @@ impl<'txt> Cursor<'txt> { /// /// If the match fails the cursor will be positioned at the first failing token. #[must_use] - pub fn match_all(&mut self, pats: &[Pat<'_>], captures: &mut [Capture]) -> bool { + pub fn match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> bool { let mut captures = captures.iter_mut(); pats.iter().all(|&p| self.match_impl(p, &mut captures)) } @@ -351,7 +381,7 @@ impl<'txt> Cursor<'txt> { /// If the pattern attempts to capture anything this will panic. If the match fails /// the cursor will be positioned at the first failing token. #[must_use] - pub fn match_pat(&mut self, pat: Pat<'_>) -> bool { + pub fn match_pat(&mut self, pat: Pat) -> bool { self.match_impl(pat, &mut [].iter_mut()) } } From 0db03c09558a7ceacc8871400e0c6432d69f614d Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sun, 8 Feb 2026 03:07:40 -0500 Subject: [PATCH 02/11] `clippy_dev`: Remove `CaptureOptLifetimeArg` pattern. --- clippy_dev/src/lib.rs | 1 + clippy_dev/src/parse.rs | 16 ++++----- clippy_dev/src/parse/cursor.rs | 66 +++++++++++++++------------------- 3 files changed, 37 insertions(+), 46 deletions(-) diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs index b707c2cea6e5..800b923a6533 100644 --- a/clippy_dev/src/lib.rs +++ b/clippy_dev/src/lib.rs @@ -1,4 +1,5 @@ #![feature( + bool_to_result, exit_status_error, new_range, os_str_slice, diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index d4e7fdfdba52..2bc30b61d346 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -354,22 +354,18 @@ impl<'cx> ParseCxImpl<'cx> { #[rustfmt::skip] static LINT_DECL_TOKENS: &[cursor::Pat] = &[ // !{ /// docs - Bang, OpenBrace, AnyComment, + Bang, OpenBrace, AnyComments, // #[clippy::version = "version"] Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, Ident(IdentPat::version), Eq, LitStr, CloseBracket, // pub NAME, GROUP, - Ident(IdentPat::r#pub), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, - ]; - #[rustfmt::skip] - static PASS_DECL_TOKENS: &[cursor::Pat] = &[ - // !( NAME <'lt> => [ - Bang, OpenParen, CaptureDocLines, CaptureIdent, CaptureOptLifetimeArg, FatArrow, OpenBracket, + Ident(IdentPat::r#pub), CaptureIdent, Comma, AnyComments, CaptureIdent, Comma, ]; let mut cursor = Cursor::new(&file.contents); let mut captures = [Capture::EMPTY; 3]; while let Some(mac_name) = cursor.find_any_ident() { + captures[1] = Capture::EMPTY; match cursor.get_text(mac_name) { "declare_clippy_lint" if cursor.match_all(LINT_DECL_TOKENS, &mut captures) && cursor.find_pat(CloseBrace) => @@ -387,7 +383,11 @@ impl<'cx> ParseCxImpl<'cx> { .is_none() ); }, - mac @ ("declare_lint_pass" | "impl_lint_pass") if cursor.match_all(PASS_DECL_TOKENS, &mut captures) => { + mac @ ("declare_lint_pass" | "impl_lint_pass") + if cursor.match_all(&[Bang, OpenParen, CaptureDocLines, CaptureIdent], &mut captures) + && cursor.opt_match_all(&[Lt, CaptureLifetime, Gt], &mut captures[2..]) + && cursor.match_all(&[FatArrow, OpenBracket], &mut captures) => + { let mac = if matches!(mac, "declare_lint_pass") { LintPassMac::Declare } else { diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 5a30931040a3..c7ad710f8d20 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -1,4 +1,4 @@ -use core::slice; +use core::{ptr, slice}; use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; /// A token pattern used for searching and matching by the [`Cursor`]. @@ -9,12 +9,11 @@ use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; /// the `*`. #[derive(Clone, Copy)] pub enum Pat { - Ident(IdentPat), /// Matches any number of comments and doc comments. - AnyComment, + AnyComments, CaptureDocLines, CaptureIdent, - LitStr, + CaptureLifetime, CaptureLitStr, Bang, CloseBrace, @@ -24,13 +23,14 @@ pub enum Pat { DoubleColon, Eq, FatArrow, + Gt, + Ident(IdentPat), Lifetime, + LitStr, Lt, - Gt, OpenBrace, OpenBracket, OpenParen, - CaptureOptLifetimeArg, Pound, Semi, } @@ -145,14 +145,13 @@ impl<'txt> Cursor<'txt> { /// /// For each capture made by the pattern one item will be taken from the capture /// sequence with the result placed inside. - #[expect(clippy::too_many_lines)] fn match_impl(&mut self, pat: Pat, captures: &mut slice::IterMut<'_, Capture>) -> bool { loop { match (pat, self.next_token.kind) { #[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/6697 (_, TokenKind::Whitespace) | ( - Pat::AnyComment, + Pat::AnyComments, TokenKind::BlockComment { terminated: true, .. } | TokenKind::LineComment { .. }, ) => self.step(), (Pat::Bang, TokenKind::Bang) @@ -199,34 +198,6 @@ impl<'txt> Cursor<'txt> { } return false; }, - (Pat::CaptureOptLifetimeArg, TokenKind::Lt) => { - self.step(); - loop { - match self.next_token.kind { - TokenKind::Lifetime { .. } => break, - TokenKind::Whitespace => self.step(), - _ => return false, - } - } - *captures.next().unwrap() = Capture { - pos: self.pos, - len: self.next_token.len, - }; - self.step(); - loop { - match self.next_token.kind { - TokenKind::Gt => break, - TokenKind::Whitespace => self.step(), - _ => return false, - } - } - self.step(); - return true; - }, - (Pat::CaptureOptLifetimeArg, _) => { - *captures.next().unwrap() = Capture { pos: 0, len: 0 }; - return true; - }, #[rustfmt::skip] ( Pat::CaptureLitStr, @@ -237,7 +208,8 @@ impl<'txt> Cursor<'txt> { .. }, ) - | (Pat::CaptureIdent, TokenKind::Ident) => { + | (Pat::CaptureIdent, TokenKind::Ident) + | (Pat::CaptureLifetime, TokenKind::Lifetime { .. }) => { *captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len }; self.step(); return true; @@ -263,7 +235,7 @@ impl<'txt> Cursor<'txt> { *captures.next().unwrap() = Capture::EMPTY; return true; }, - (Pat::AnyComment, _) => return true, + (Pat::AnyComments, _) => return true, _ => return false, } } @@ -375,6 +347,24 @@ impl<'txt> Cursor<'txt> { pats.iter().all(|&p| self.match_impl(p, &mut captures)) } + /// Attempts to match a sequence of patterns at the current position. Returns whether + /// all patterns were successfully matched. + /// + /// Captures will be written to the given slice in the order they're matched. If a + /// capture is matched, but there are no more capture slots this will panic. If the + /// match is completed without filling all the capture slots they will be left + /// unmodified. + /// + /// If the match fails the cursor will be positioned at the first failing token. + #[must_use] + pub fn opt_match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> bool { + let mut captures = captures.iter_mut(); + pats.iter() + .try_for_each(|p| self.match_impl(*p, &mut captures).ok_or(p)) + .err() + .is_none_or(|p| ptr::addr_eq(pats.as_ptr(), p)) + } + /// Attempts to match a single pattern at the current position. Returns whether the /// pattern was successfully matched. /// From 658533cbb30bd36cce8ec78d92b917d24a113a7b Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sun, 8 Feb 2026 18:35:53 -0500 Subject: [PATCH 03/11] `clippy_dev`: Cleanup `Cursor` interface. --- clippy_dev/src/edit_lints.rs | 17 +-- clippy_dev/src/generate.rs | 3 +- clippy_dev/src/lib.rs | 1 + clippy_dev/src/new_lint.rs | 4 +- clippy_dev/src/parse.rs | 20 ++-- clippy_dev/src/parse/cursor.rs | 192 +++++++++++++++------------------ 6 files changed, 112 insertions(+), 125 deletions(-) diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index 59013ca820d9..9307fb41b6ea 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -330,11 +330,9 @@ fn uplift_update_fn<'a>( let mut copy_pos = 0u32; let mut changed = false; let mut cursor = Cursor::new(src); - while let Some(ident) = cursor.find_any_ident() { + while let Some(ident) = cursor.find_capture_ident() { match cursor.get_text(ident) { - "mod" - if remove_mod && cursor.match_ident(old_name).is_some() && cursor.match_pat(cursor::Pat::Semi) => - { + "mod" if remove_mod && cursor.eat_ident(old_name) && cursor.eat_semi() => { dst.push_str(&src[copy_pos as usize..ident.pos as usize]); dst.push_str(new_name); copy_pos = cursor.pos(); @@ -343,7 +341,7 @@ fn uplift_update_fn<'a>( } changed = true; }, - "clippy" if cursor.match_pat(cursor::Pat::DoubleColon) && cursor.match_ident(old_name).is_some() => { + "clippy" if cursor.eat_double_colon() && cursor.eat_ident(old_name) => { dst.push_str(&src[copy_pos as usize..ident.pos as usize]); dst.push_str(new_name); copy_pos = cursor.pos(); @@ -393,8 +391,11 @@ fn rename_update_fn<'a>( }, // mod lint_name "mod" => { - if rename_mod && let Some(pos) = cursor.match_ident(old_name) { - dst.push_str(&src[copy_pos as usize..pos as usize]); + if rename_mod + && let Some(mod_name) = cursor.capture_ident() + && cursor.get_text(mod_name) == old_name + { + dst.push_str(&src[copy_pos as usize..mod_name.pos as usize]); dst.push_str(new_name); copy_pos = cursor.pos(); changed = true; @@ -403,7 +404,7 @@ fn rename_update_fn<'a>( // lint_name:: name if rename_mod && name == old_name => { let name_end = cursor.pos(); - if cursor.match_pat(cursor::Pat::DoubleColon) { + if cursor.eat_double_colon() { dst.push_str(&src[copy_pos as usize..match_start as usize]); dst.push_str(new_name); copy_pos = name_end; diff --git a/clippy_dev/src/generate.rs b/clippy_dev/src/generate.rs index c35e718fa791..83c6e7fe2323 100644 --- a/clippy_dev/src/generate.rs +++ b/clippy_dev/src/generate.rs @@ -73,8 +73,7 @@ impl LintData<'_> { &mut |_, src, dst| { let mut cursor = Cursor::new(src); assert!( - cursor.find_ident("declare_with_version").is_some() - && cursor.find_ident("declare_with_version").is_some(), + cursor.find_ident("declare_with_version") && cursor.find_ident("declare_with_version"), "error reading deprecated lints" ); dst.push_str(&src[..cursor.pos() as usize]); diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs index 800b923a6533..2e75913f63f6 100644 --- a/clippy_dev/src/lib.rs +++ b/clippy_dev/src/lib.rs @@ -1,6 +1,7 @@ #![feature( bool_to_result, exit_status_error, + macro_metavar_expr_concat, new_range, os_str_slice, os_string_truncate, diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index 4e44cad472ae..50a6537f8a89 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -533,9 +533,9 @@ fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) { let mut decl_end = None; let mut cursor = Cursor::new(contents); let mut captures = [Capture::EMPTY]; - while let Some(name) = cursor.find_any_ident() { + while let Some(name) = cursor.find_capture_ident() { match cursor.get_text(name) { - "declare_clippy_lint" if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_pat(CloseBrace) => { + "declare_clippy_lint" if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_close_brace() => { decl_end = Some(cursor.pos()); }, "impl" if cursor.match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) => { diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 2bc30b61d346..6010da42c19b 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -364,11 +364,11 @@ impl<'cx> ParseCxImpl<'cx> { let mut cursor = Cursor::new(&file.contents); let mut captures = [Capture::EMPTY; 3]; - while let Some(mac_name) = cursor.find_any_ident() { + while let Some(mac_name) = cursor.find_capture_ident() { captures[1] = Capture::EMPTY; match cursor.get_text(mac_name) { "declare_clippy_lint" - if cursor.match_all(LINT_DECL_TOKENS, &mut captures) && cursor.find_pat(CloseBrace) => + if cursor.match_all(LINT_DECL_TOKENS, &mut captures) && cursor.find_close_brace() => { assert!( data.lints @@ -401,14 +401,14 @@ impl<'cx> ParseCxImpl<'cx> { let lints = self.str_list_buf.with(|buf| { // Parses a comma separated list of paths and converts each path // to a string with whitespace removed. - while !cursor.match_pat(CloseBracket) { + while !cursor.eat_close_bracket() { buf.push(self.str_buf.with(|buf| { - if cursor.match_pat(DoubleColon) { + if cursor.eat_double_colon() { buf.push_str("::"); } let capture = cursor.capture_ident()?; buf.push_str(cursor.get_text(capture)); - while cursor.match_pat(DoubleColon) { + while cursor.eat_double_colon() { buf.push_str("::"); let capture = cursor.capture_ident()?; buf.push_str(cursor.get_text(capture)); @@ -416,8 +416,8 @@ impl<'cx> ParseCxImpl<'cx> { Some(self.arena.alloc_str(buf)) })?); - if !cursor.match_pat(Comma) { - if !cursor.match_pat(CloseBracket) { + if !cursor.eat_comma() { + if !cursor.eat_close_bracket() { return None; } break; @@ -482,11 +482,11 @@ impl<'cx> ParseCxImpl<'cx> { // First instance is the macro definition. assert!( - cursor.find_ident("declare_with_version").is_some(), + cursor.find_ident("declare_with_version"), "error reading deprecated lints" ); - if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(DEPRECATED_TOKENS, &mut []) { + if cursor.find_ident("declare_with_version") && cursor.match_all(DEPRECATED_TOKENS, &mut []) { while cursor.match_all(DECL_TOKENS, &mut captures) { assert!( data.lints @@ -504,7 +504,7 @@ impl<'cx> ParseCxImpl<'cx> { panic!("error reading deprecated lints"); } - if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(RENAMED_TOKENS, &mut []) { + if cursor.find_ident("declare_with_version") && cursor.match_all(RENAMED_TOKENS, &mut []) { while cursor.match_all(DECL_TOKENS, &mut captures) { assert!( data.lints diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index c7ad710f8d20..2fffaa201582 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -16,7 +16,6 @@ pub enum Pat { CaptureLifetime, CaptureLitStr, Bang, - CloseBrace, CloseBracket, CloseParen, Comma, @@ -126,12 +125,6 @@ impl<'txt> Cursor<'txt> { self.pos } - /// Gets whether the cursor has exhausted its input. - #[must_use] - pub fn at_end(&self) -> bool { - self.next_token.kind == TokenKind::Eof - } - /// Advances the cursor to the next token. If the stream is exhausted this will set /// the next token to [`TokenKind::Eof`]. pub fn step(&mut self) { @@ -154,8 +147,10 @@ impl<'txt> Cursor<'txt> { Pat::AnyComments, TokenKind::BlockComment { terminated: true, .. } | TokenKind::LineComment { .. }, ) => self.step(), + (Pat::AnyComments, _) => return true, + + (Pat::Ident(x), TokenKind::Ident) if x.as_str() == self.peek_text() => break, (Pat::Bang, TokenKind::Bang) - | (Pat::CloseBrace, TokenKind::CloseBrace) | (Pat::CloseBracket, TokenKind::CloseBracket) | (Pat::CloseParen, TokenKind::CloseParen) | (Pat::Comma, TokenKind::Comma) @@ -174,30 +169,17 @@ impl<'txt> Cursor<'txt> { kind: LiteralKind::Str { terminated: true } | LiteralKind::RawStr { .. }, .. }, - ) => { - self.step(); - return true; - }, - (Pat::Ident(x), TokenKind::Ident) if x.as_str() == self.peek_text() => { - self.step(); - return true; - }, - (Pat::DoubleColon, TokenKind::Colon) => { + ) => break, + + (Pat::DoubleColon, TokenKind::Colon) if self.inner.as_str().starts_with(':') => { self.step(); - if matches!(self.next_token.kind, TokenKind::Colon) { - self.step(); - return true; - } - return false; + break; }, - (Pat::FatArrow, TokenKind::Eq) => { + (Pat::FatArrow, TokenKind::Eq) if self.inner.as_str().starts_with('>') => { self.step(); - if matches!(self.next_token.kind, TokenKind::Gt) { - self.step(); - return true; - } - return false; + break; }, + #[rustfmt::skip] ( Pat::CaptureLitStr, @@ -214,6 +196,7 @@ impl<'txt> Cursor<'txt> { self.step(); return true; }, + (Pat::CaptureDocLines, TokenKind::LineComment { doc_style: Some(_) }) => { let pos = self.pos; loop { @@ -231,43 +214,26 @@ impl<'txt> Cursor<'txt> { }; return true; }, + (Pat::CaptureDocLines, _) => { *captures.next().unwrap() = Capture::EMPTY; return true; }, - (Pat::AnyComments, _) => return true, _ => return false, } } - } - /// Consumes all tokens until the specified identifier is found and returns its - /// position. Returns `None` if the identifier could not be found. - /// - /// The cursor will be positioned immediately after the identifier, or at the end if - /// it is not. - pub fn find_ident(&mut self, ident: &str) -> Option { - loop { - match self.next_token.kind { - TokenKind::Ident if self.peek_text() == ident => { - let pos = self.pos; - self.step(); - return Some(pos); - }, - TokenKind::Eof => return None, - _ => self.step(), - } - } + self.step(); + true } - /// Consumes all tokens until the next identifier is found and captures it. Returns - /// `None` if no identifier could be found. - /// - /// The cursor will be positioned immediately after the identifier, or at the end if - /// it is not. - pub fn find_any_ident(&mut self) -> Option { + /// Consumes and captures the next non-whitespace token if it's an identifier. Returns + /// `None` otherwise. + #[must_use] + pub fn capture_ident(&mut self) -> Option { loop { match self.next_token.kind { + TokenKind::Whitespace => self.step(), TokenKind::Ident => { let res = Capture { pos: self.pos, @@ -276,60 +242,29 @@ impl<'txt> Cursor<'txt> { self.step(); return Some(res); }, - TokenKind::Eof => return None, - _ => self.step(), - } - } - } - - /// Consume the returns the position of the next non-whitespace token if it's the - /// specified identifier. Returns `None` otherwise. - pub fn match_ident(&mut self, s: &str) -> Option { - loop { - match self.next_token.kind { - TokenKind::Ident if s == self.peek_text() => { - let pos = self.pos; - self.step(); - return Some(pos); - }, - TokenKind::Whitespace => self.step(), _ => return None, } } } - /// Consumes and captures the next non-whitespace token if it's an identifier. Returns - /// `None` otherwise. - pub fn capture_ident(&mut self) -> Option { + /// Consumes all tokens up to and including the next identifier. Returns either the + /// captured identifier or `None` if one was not found. + #[must_use] + pub fn find_capture_ident(&mut self) -> Option { loop { match self.next_token.kind { + TokenKind::Eof => return None, TokenKind::Ident => { - let pos = self.pos; - let len = self.next_token.len; + let res = Capture { + pos: self.pos, + len: self.next_token.len, + }; self.step(); - return Some(Capture { pos, len }); + return Some(res); }, - TokenKind::Whitespace => self.step(), - _ => return None, - } - } - } - - /// Continually attempt to match the pattern on subsequent tokens until a match is - /// found. Returns whether the pattern was successfully matched. - /// - /// Not generally suitable for multi-token patterns or patterns that can match - /// nothing. - #[must_use] - pub fn find_pat(&mut self, pat: Pat) -> bool { - let mut capture = [].iter_mut(); - while !self.match_impl(pat, &mut capture) { - self.step(); - if self.at_end() { - return false; + _ => self.step(), } } - true } /// Attempts to match a sequence of patterns at the current position. Returns whether @@ -364,14 +299,65 @@ impl<'txt> Cursor<'txt> { .err() .is_none_or(|p| ptr::addr_eq(pats.as_ptr(), p)) } +} - /// Attempts to match a single pattern at the current position. Returns whether the - /// pattern was successfully matched. - /// - /// If the pattern attempts to capture anything this will panic. If the match fails - /// the cursor will be positioned at the first failing token. - #[must_use] - pub fn match_pat(&mut self, pat: Pat) -> bool { - self.match_impl(pat, &mut [].iter_mut()) +macro_rules! mk_tk_methods { + ($( + [$desc:literal] + $name:ident(&mut $self:tt $($params:tt)*) + { $pat:pat $(if $guard:expr)? $(=> $extra:block)? } + )*) => { + #[allow(dead_code)] + impl Cursor<'_> {$( + #[doc = "Consumes the next non-whitespace token if it's "] + #[doc = $desc] + #[doc = " and returns whether the token was found."] + #[must_use] + pub fn ${concat(eat_, $name)}(&mut $self $($params)*) -> bool { + loop { + match $self.next_token.kind { + TokenKind::Whitespace => $self.step(), + $pat $(if $guard)? => { + $self.step(); + return true; + }, + _ => return false, + } + } + } + + #[doc = "Consumes all tokens up to and including "] + #[doc = $desc] + #[doc = " and returns whether the token was found."] + #[must_use] + pub fn ${concat(find_, $name)}(&mut $self $($params)*) -> bool { + loop { + match $self.next_token.kind { + TokenKind::Eof => return false, + $pat $(if $guard)? => { + $self.step(); + return true; + }, + _ => $self.step(), + } + } + } + )*} + } +} +mk_tk_methods! { + ["`}`"] + close_brace(&mut self) { TokenKind::CloseBrace } + ["`]`"] + close_bracket(&mut self) { TokenKind::CloseBracket } + ["`,`"] + comma(&mut self) { TokenKind::Comma } + ["`::`"] + double_colon(&mut self) { + TokenKind::Colon if self.inner.as_str().starts_with(':') => { self.step(); } } + ["the specified identifier"] + ident(&mut self, s: &str) { TokenKind::Ident if self.peek_text() == s } + ["`;`"] + semi(&mut self) { TokenKind::Semi } } From 54f008657fa9cc82cf1bc3f612bbe0c02b2d152d Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sun, 8 Feb 2026 21:02:52 -0500 Subject: [PATCH 04/11] `clippy_dev`: Move path parsing to it's own function. --- clippy_dev/src/parse.rs | 28 +++++--------------- clippy_dev/src/parse/cursor.rs | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 6010da42c19b..609b12e4f1f0 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -399,27 +399,13 @@ impl<'cx> ParseCxImpl<'cx> { let lt = if lt.is_empty() { None } else { Some(lt) }; let lints = self.str_list_buf.with(|buf| { - // Parses a comma separated list of paths and converts each path - // to a string with whitespace removed. - while !cursor.eat_close_bracket() { - buf.push(self.str_buf.with(|buf| { - if cursor.eat_double_colon() { - buf.push_str("::"); - } - let capture = cursor.capture_ident()?; - buf.push_str(cursor.get_text(capture)); - while cursor.eat_double_colon() { - buf.push_str("::"); - let capture = cursor.capture_ident()?; - buf.push_str(cursor.get_text(capture)); - } - Some(self.arena.alloc_str(buf)) - })?); - + loop { + match cursor.capture_opt_path(&mut self.str_buf, self.arena) { + Ok(None) => break, + Ok(Some(p)) => buf.push(p), + Err(()) => panic!("error parsing path"), + } if !cursor.eat_comma() { - if !cursor.eat_close_bracket() { - return None; - } break; } } @@ -433,7 +419,7 @@ impl<'cx> ParseCxImpl<'cx> { }); if let Some(lints) = lints - && cursor.match_all(&[CloseParen, Semi], &mut []) + && cursor.match_all(&[CloseBracket, CloseParen, Semi], &mut []) { data.lint_passes.push(LintPass { docs, diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 2fffaa201582..5ce595d0ec1b 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -1,4 +1,6 @@ +use super::StrBuf; use core::{ptr, slice}; +use rustc_arena::DroplessArena; use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; /// A token pattern used for searching and matching by the [`Cursor`]. @@ -267,6 +269,51 @@ impl<'txt> Cursor<'txt> { } } + /// Consumes and captures the text of a path without any internal whitespace. Returns + /// `Err` if the path ends with `::`, and `None` if no path component exists at the + /// current position. + /// + /// Only paths containing identifiers separated by `::` with a possible leading `::`. + /// Generic arguments and qualified paths are not considered. + pub fn capture_opt_path(&mut self, buf: &mut StrBuf, arena: &'txt DroplessArena) -> Result, ()> { + #[derive(Clone, Copy)] + enum State { + Start, + Sep, + Ident, + } + + buf.with(|buf| { + let start = self.pos; + let mut state = State::Start; + loop { + match (state, self.next_token.kind) { + (_, TokenKind::Whitespace) => self.step(), + (State::Start | State::Ident, TokenKind::Colon) if self.inner.first() == ':' => { + state = State::Sep; + buf.push_str("::"); + self.step(); + self.step(); + }, + (State::Start | State::Sep, TokenKind::Ident) => { + state = State::Ident; + buf.push_str(self.peek_text()); + self.step(); + }, + (State::Ident, _) => break, + (State::Start, _) => return Ok(None), + (State::Sep, _) => return Err(()), + } + } + let text = self.text[start as usize..self.pos as usize].trim(); + Ok(Some(if text.len() == buf.len() { + text + } else { + arena.alloc_str(buf) + })) + }) + } + /// Attempts to match a sequence of patterns at the current position. Returns whether /// all patterns were successfully matched. /// From dffad16f6444cff552708f9892dc65434636d0eb Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Thu, 12 Feb 2026 02:38:45 -0500 Subject: [PATCH 05/11] `clippy_dev`: Store the span of each lint's name. --- clippy_dev/src/edit_lints.rs | 103 ++++++++++++++++++++------------- clippy_dev/src/fmt.rs | 2 +- clippy_dev/src/generate.rs | 14 ++--- clippy_dev/src/parse.rs | 81 ++++++++++++++++---------- clippy_dev/src/parse/cursor.rs | 9 ++- 5 files changed, 130 insertions(+), 79 deletions(-) diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index 9307fb41b6ea..4ced81f5b1d3 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -1,5 +1,7 @@ use crate::parse::cursor::{self, Capture, Cursor}; -use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, RenamedLint}; +use crate::parse::{ + ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, ParsedLints, RenamedLint, SourceFile, Span, +}; use crate::utils::{ ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, @@ -27,18 +29,28 @@ pub fn deprecate<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, name eprintln!("error: failed to find lint `{name}`"); return; }; - let Lint::Active(prev_lint) = mem::replace( + let prev_lint = mem::replace( lint.get_mut(), - Lint::Deprecated(DeprecatedLint { - reason, - version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), - }), - ) else { + Lint { + name_sp: Span::new(data.deprecated_file, 0..0), + data: LintData::Deprecated(DeprecatedLint { + reason, + version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), + }), + }, + ); + let LintData::Active(prev_lint_data) = prev_lint.data else { eprintln!("error: `{name}` is already deprecated"); return; }; - remove_lint_declaration(name, &prev_lint, &data, &mut FileUpdater::default()); + remove_lint_declaration( + name, + prev_lint.name_sp.file, + &prev_lint_data, + &data, + &mut FileUpdater::default(), + ); data.gen_decls(UpdateMode::Change); println!("info: `{name}` has successfully been deprecated"); println!("note: you must run `cargo uitest` to update the test results"); @@ -53,19 +65,24 @@ pub fn uplift<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam eprintln!("error: failed to find lint `{old_name}`"); return; }; - let Lint::Active(prev_lint) = mem::replace( + + let prev_lint = mem::replace( lint.get_mut(), - Lint::Renamed(RenamedLint { - new_name: LintName::new_rustc(new_name), - version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), - }), - ) else { + Lint { + name_sp: Span::new(data.deprecated_file, 0..0), + data: LintData::Renamed(RenamedLint { + new_name: LintName::new_rustc(new_name), + version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), + }), + }, + ); + let LintData::Active(prev_lint_data) = prev_lint.data else { eprintln!("error: `{old_name}` is already deprecated"); return; }; let mut updater = FileUpdater::default(); - let remove_mod = remove_lint_declaration(old_name, &prev_lint, &data, &mut updater); + let remove_mod = remove_lint_declaration(old_name, prev_lint.name_sp.file, &prev_lint_data, &data, &mut updater); let mut update_fn = uplift_update_fn(old_name, new_name, remove_mod); for e in walk_dir_no_dot_or_target(".") { let e = expect_action(e, ErrAction::Read, "."); @@ -103,32 +120,38 @@ pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam eprintln!("error: failed to find lint `{old_name}`"); return; }; - let Lint::Active(prev_lint) = mem::replace( + + let prev_lint = mem::replace( lint.get_mut(), - Lint::Renamed(RenamedLint { - new_name: LintName::new_clippy(new_name), - version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), - }), - ) else { + Lint { + name_sp: Span::new(data.deprecated_file, 0..0), + data: LintData::Renamed(RenamedLint { + new_name: LintName::new_clippy(new_name), + version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), + }), + }, + ); + if !matches!(prev_lint.data, LintData::Active(_)) { eprintln!("error: `{old_name}` is already deprecated"); return; }; + let prev_file = prev_lint.name_sp.file; let mut rename_mod = false; if let Entry::Vacant(e) = data.lints.entry(new_name) { - if let Some((path, file)) = prev_lint.file.path.get().rsplit_once(path::MAIN_SEPARATOR) + if let Some((path, file)) = prev_file.path.get().rsplit_once(path::MAIN_SEPARATOR) && let Some(file) = file.strip_suffix(".rs") && file == old_name { let new_path = cx .str_buf .alloc_display(cx.arena, format_args!("{path}{}{new_name}.rs", path::MAIN_SEPARATOR)); - if try_rename_file(prev_lint.file.path.get(), new_path) { + if try_rename_file(prev_file.path.get(), new_path) { rename_mod = true; - prev_lint.file.path.set(new_path); + prev_file.path.set(new_path); } } - e.insert(Lint::Active(prev_lint)); + e.insert(prev_lint); rename_test_files(old_name, new_name, &create_ignored_prefixes(old_name, &data)); } else { println!("Renamed `{old_name}` to `{new_name}`"); @@ -153,22 +176,22 @@ pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam /// Removes a lint's declaration and test files. Returns whether the module containing the /// lint was deleted. -fn remove_lint_declaration(name: &str, lint: &ActiveLint<'_>, data: &LintData<'_>, updater: &mut FileUpdater) -> bool { - let delete_mod = if data.lints.iter().all(|(_, l)| { - if let Lint::Active(l) = l { - l.file != lint.file - } else { - true - } - }) { - delete_file_if_exists(lint.file.path.get()) +fn remove_lint_declaration( + name: &str, + lint_file: &SourceFile<'_>, + lint_data: &ActiveLint<'_>, + data: &ParsedLints<'_>, + updater: &mut FileUpdater, +) -> bool { + let delete_mod = if data.lints.iter().all(|(_, l)| l.name_sp.file != lint_file) { + delete_file_if_exists(lint_file.path.get()) } else { - updater.update_file(lint.file.path.get(), &mut |_, src, dst| -> UpdateStatus { - let mut start = &src[..lint.declaration_range.start as usize]; + updater.update_file(lint_file.path.get(), &mut |_, src, dst| -> UpdateStatus { + let mut start = &src[..lint_data.decl_range.start as usize]; if start.ends_with("\n\n") { start = &start[..start.len() - 1]; } - let mut end = &src[lint.declaration_range.end as usize..]; + let mut end = &src[lint_data.decl_range.end as usize..]; if end.starts_with("\n\n") { end = &end[1..]; } @@ -187,10 +210,10 @@ fn remove_lint_declaration(name: &str, lint: &ActiveLint<'_>, data: &LintData<'_ /// /// This is needed because rustc doesn't allow a lint to be renamed to a lint that has /// also been renamed. -fn update_rename_targets<'cx>(data: &mut LintData<'cx>, old_name: &str, new_name: LintName<'cx>) { +fn update_rename_targets<'cx>(data: &mut ParsedLints<'cx>, old_name: &str, new_name: LintName<'cx>) { let old_name = LintName::new_clippy(old_name); for lint in data.lints.values_mut() { - if let Lint::Renamed(lint) = lint + if let LintData::Renamed(lint) = &mut lint.data && lint.new_name == old_name { lint.new_name = new_name; @@ -199,7 +222,7 @@ fn update_rename_targets<'cx>(data: &mut LintData<'cx>, old_name: &str, new_name } /// Creates a list of prefixes to ignore when -fn create_ignored_prefixes<'cx>(name: &str, data: &LintData<'cx>) -> Vec<&'cx str> { +fn create_ignored_prefixes<'cx>(name: &str, data: &ParsedLints<'cx>) -> Vec<&'cx str> { data.lints .keys() .copied() diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index 8a7150052f03..7b2b3b3bd2d9 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -344,7 +344,7 @@ pub fn run(update_mode: UpdateMode) { let mut lints = data.mk_file_to_lint_decl_map(); let mut ranges = VecBuf::with_capacity(256); for passes in data.iter_passes_by_file_mut() { - let file = passes[0].file; + let file = passes[0].decl_sp.file; let mut lints = lints.remove(file); let lints = lints.as_deref_mut().unwrap_or_default(); updater.update_loaded_file_checked("cargo dev fmt", update_mode, file, &mut |_, src, dst| { diff --git a/clippy_dev/src/generate.rs b/clippy_dev/src/generate.rs index 83c6e7fe2323..f5f256fdc865 100644 --- a/clippy_dev/src/generate.rs +++ b/clippy_dev/src/generate.rs @@ -1,5 +1,5 @@ use crate::parse::cursor::Cursor; -use crate::parse::{Lint, LintData, LintPass, VecBuf}; +use crate::parse::{LintData, LintPass, ParsedLints, VecBuf}; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, slice_groups, update_text_region_fn}; use core::range::Range; use itertools::Itertools; @@ -13,7 +13,7 @@ const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev u const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html"; -impl LintData<'_> { +impl ParsedLints<'_> { #[expect(clippy::too_many_lines)] pub fn gen_decls(&self, update_mode: UpdateMode) { let mut updater = FileUpdater::default(); @@ -39,10 +39,10 @@ impl LintData<'_> { let mut deprecated = Vec::with_capacity(lints.len() / 8); let mut renamed = Vec::with_capacity(lints.len() / 8); for &(name, lint) in &lints { - match lint { - Lint::Active(lint) => active.push((name, lint.file.path_as_krate_mod())), - Lint::Deprecated(lint) => deprecated.push((name, lint)), - Lint::Renamed(lint) => renamed.push((name, lint)), + match &lint.data { + LintData::Active(_) => active.push((name, lint.name_sp.file.path_as_krate_mod())), + LintData::Deprecated(lint) => deprecated.push((name, lint)), + LintData::Renamed(lint) => renamed.push((name, lint)), } } active.sort_by_key(|&(_, path)| path); @@ -265,7 +265,7 @@ pub fn gen_sorted_lints_file( ) { ranges.with(|ranges| { ranges.extend(lints.iter().map(|&(_, x)| x)); - ranges.extend(passes.iter().map(|x| x.decl_range)); + ranges.extend(passes.iter().map(|x| x.decl_sp.range)); ranges.sort_unstable_by_key(|x| x.start); lints.sort_unstable_by_key(|&(x, _)| x); diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 609b12e4f1f0..92edee2b3792 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -69,6 +69,17 @@ impl Hash for SourceFile<'_> { } } +#[derive(Clone, Copy)] +pub struct Span<'cx> { + pub file: &'cx SourceFile<'cx>, + pub range: Range, +} +impl<'cx> Span<'cx> { + pub fn new(file: &'cx SourceFile<'cx>, range: Range) -> Self { + Self { file, range } + } +} + pub struct ParseCxImpl<'cx> { pub arena: &'cx DroplessArena, pub source_files: &'cx TypedArena>, @@ -218,9 +229,8 @@ impl Display for LintName<'_> { } pub struct ActiveLint<'cx> { - pub file: &'cx SourceFile<'cx>, pub group: &'cx str, - pub declaration_range: Range, + pub decl_range: Range, } pub struct DeprecatedLint<'cx> { @@ -233,12 +243,17 @@ pub struct RenamedLint<'cx> { pub version: &'cx str, } -pub enum Lint<'cx> { +pub enum LintData<'cx> { Active(ActiveLint<'cx>), Deprecated(DeprecatedLint<'cx>), Renamed(RenamedLint<'cx>), } +pub struct Lint<'cx> { + pub name_sp: Span<'cx>, + pub data: LintData<'cx>, +} + #[derive(Clone, Copy)] pub enum LintPassMac { Declare, @@ -260,27 +275,26 @@ pub struct LintPass<'cx> { pub name: &'cx str, pub lt: Option<&'cx str>, pub mac: LintPassMac, - pub file: &'cx SourceFile<'cx>, - pub decl_range: Range, + pub decl_sp: Span<'cx>, pub lints: &'cx mut [&'cx str], } -pub struct LintData<'cx> { +pub struct ParsedLints<'cx> { pub lints: FxHashMap<&'cx str, Lint<'cx>>, pub lint_passes: Vec>, pub deprecated_file: &'cx SourceFile<'cx>, } -impl<'cx> LintData<'cx> { +impl<'cx> ParsedLints<'cx> { #[expect(clippy::mutable_key_type)] pub fn mk_file_to_lint_decl_map(&self) -> FxHashMap<&'cx SourceFile<'cx>, Vec<(&'cx str, Range)>> { #[expect(clippy::default_trait_access)] let mut lints = FxHashMap::with_capacity_and_hasher(500, Default::default()); for (&name, lint) in &self.lints { - if let Lint::Active(lint) = lint { + if let LintData::Active(lint_data) = &lint.data { lints - .entry(lint.file) + .entry(lint.name_sp.file) .or_insert_with(|| Vec::with_capacity(8)) - .push((name, lint.declaration_range)); + .push((name, lint_data.decl_range)); } } lints @@ -288,7 +302,7 @@ impl<'cx> LintData<'cx> { pub fn iter_passes_by_file_mut<'s>(&'s mut self) -> impl Iterator]> { slice_groups_mut(&mut self.lint_passes, |head, tail| { - tail.iter().take_while(|&x| x.file == head.file).count() + tail.iter().take_while(|&x| x.decl_sp.file == head.decl_sp.file).count() }) } } @@ -296,8 +310,8 @@ impl<'cx> LintData<'cx> { impl<'cx> ParseCxImpl<'cx> { /// Finds and parses all lint declarations. #[must_use] - pub fn parse_lint_decls(&mut self) -> LintData<'cx> { - let mut data = LintData { + pub fn parse_lint_decls(&mut self) -> ParsedLints<'cx> { + let mut data = ParsedLints { #[expect(clippy::default_trait_access)] lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()), lint_passes: Vec::with_capacity(400), @@ -348,7 +362,7 @@ impl<'cx> ParseCxImpl<'cx> { } /// Parse a source file looking for `declare_clippy_lint` macro invocations. - fn parse_lint_src_file(&mut self, data: &mut LintData<'cx>, file: &'cx SourceFile<'cx>) { + fn parse_lint_src_file(&mut self, data: &mut ParsedLints<'cx>, file: &'cx SourceFile<'cx>) { #[allow(clippy::enum_glob_use)] use cursor::Pat::*; #[rustfmt::skip] @@ -374,11 +388,13 @@ impl<'cx> ParseCxImpl<'cx> { data.lints .insert( self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[0])), - Lint::Active(ActiveLint { - file, - group: cursor.get_text(captures[1]), - declaration_range: mac_name.pos..cursor.pos(), - }), + Lint { + name_sp: captures[0].mk_sp(file), + data: LintData::Active(ActiveLint { + group: cursor.get_text(captures[1]), + decl_range: mac_name.pos..cursor.pos(), + }), + }, ) .is_none() ); @@ -426,8 +442,7 @@ impl<'cx> ParseCxImpl<'cx> { name, lt, mac, - file, - decl_range: mac_name.pos..cursor.pos(), + decl_sp: Span::new(file, mac_name.pos..cursor.pos()), lints, }); } @@ -437,7 +452,7 @@ impl<'cx> ParseCxImpl<'cx> { } } - fn parse_deprecated_lints(&mut self, data: &mut LintData<'cx>) { + fn parse_deprecated_lints(&mut self, data: &mut ParsedLints<'cx>) { #[allow(clippy::enum_glob_use)] use cursor::Pat::*; @@ -478,10 +493,13 @@ impl<'cx> ParseCxImpl<'cx> { data.lints .insert( self.parse_clippy_lint_name(file, cursor.get_text(captures[1])), - Lint::Deprecated(DeprecatedLint { - reason: self.parse_str_single_line(file, cursor.get_text(captures[2])), - version: self.parse_str_single_line(file, cursor.get_text(captures[0])), - }), + Lint { + name_sp: captures[1].mk_sp(file), + data: LintData::Deprecated(DeprecatedLint { + reason: self.parse_str_single_line(file, cursor.get_text(captures[2])), + version: self.parse_str_single_line(file, cursor.get_text(captures[0])), + }), + }, ) .is_none() ); @@ -496,10 +514,13 @@ impl<'cx> ParseCxImpl<'cx> { data.lints .insert( self.parse_clippy_lint_name(file, cursor.get_text(captures[1])), - Lint::Renamed(RenamedLint { - new_name: self.parse_lint_name(file, cursor.get_text(captures[2])), - version: self.parse_str_single_line(file, cursor.get_text(captures[0])), - }), + Lint { + name_sp: captures[1].mk_sp(file), + data: LintData::Renamed(RenamedLint { + new_name: self.parse_lint_name(file, cursor.get_text(captures[2])), + version: self.parse_str_single_line(file, cursor.get_text(captures[0])), + }), + }, ) .is_none() ); diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 5ce595d0ec1b..76cbb070883e 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -1,4 +1,4 @@ -use super::StrBuf; +use super::{SourceFile, Span, StrBuf}; use core::{ptr, slice}; use rustc_arena::DroplessArena; use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; @@ -73,6 +73,13 @@ pub struct Capture { } impl Capture { pub const EMPTY: Self = Self { pos: 0, len: 0 }; + + pub fn mk_sp<'cx>(self, file: &'cx SourceFile<'cx>) -> Span<'cx> { + Span { + file, + range: self.pos..self.pos + self.len, + } + } } /// A unidirectional cursor over a token stream that is lexed on demand. From d3e8e81a326529e25f1502d830c0f879a636d108 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Sun, 15 Feb 2026 05:03:43 -0500 Subject: [PATCH 06/11] `clippy_dev`: Move some things from `parse` to `utils`. --- clippy_dev/src/edit_lints.rs | 8 +- clippy_dev/src/fmt.rs | 3 +- clippy_dev/src/generate.rs | 4 +- clippy_dev/src/parse.rs | 164 +-------------------------------- clippy_dev/src/utils.rs | 174 ++++++++++++++++++++++++++++++++++- 5 files changed, 179 insertions(+), 174 deletions(-) diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index 4ced81f5b1d3..078e0f2c6cd8 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -1,10 +1,8 @@ use crate::parse::cursor::{self, Capture, Cursor}; -use crate::parse::{ - ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, ParsedLints, RenamedLint, SourceFile, Span, -}; +use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, ParsedLints, RenamedLint}; use crate::utils::{ - ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, - expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, + ErrAction, FileUpdater, SourceFile, Span, 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_lexer::TokenKind; diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index 7b2b3b3bd2d9..b6aa0eb3749d 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -1,8 +1,7 @@ use crate::generate::gen_sorted_lints_file; use crate::new_parse_cx; -use crate::parse::VecBuf; use crate::utils::{ - ErrAction, FileUpdater, UpdateMode, UpdateStatus, expect_action, run_with_output, split_args_for_threads, + ErrAction, FileUpdater, UpdateMode, UpdateStatus, VecBuf, expect_action, run_with_output, split_args_for_threads, walk_dir_no_dot_or_target, }; use itertools::Itertools; diff --git a/clippy_dev/src/generate.rs b/clippy_dev/src/generate.rs index f5f256fdc865..7fb6c848e874 100644 --- a/clippy_dev/src/generate.rs +++ b/clippy_dev/src/generate.rs @@ -1,6 +1,6 @@ use crate::parse::cursor::Cursor; -use crate::parse::{LintData, LintPass, ParsedLints, VecBuf}; -use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, slice_groups, update_text_region_fn}; +use crate::parse::{LintData, LintPass, ParsedLints}; +use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, VecBuf, slice_groups, update_text_region_fn}; use core::range::Range; use itertools::Itertools; use std::collections::HashSet; diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 92edee2b3792..95fb22accc07 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -1,85 +1,15 @@ pub mod cursor; use self::cursor::{Capture, Cursor, IdentPat}; -use crate::utils::{ErrAction, File, Scoped, expect_action, slice_groups_mut, walk_dir_no_dot_or_target}; -use core::cell::Cell; -use core::fmt::{self, Display, Write as _}; -use core::hash::{Hash, Hasher}; -use core::ptr; +use crate::utils::{ + ErrAction, Scoped, SourceFile, Span, StrBuf, VecBuf, expect_action, slice_groups_mut, walk_dir_no_dot_or_target, +}; +use core::fmt::{self, Display}; use core::range::Range; use rustc_arena::{DroplessArena, TypedArena}; use rustc_data_structures::fx::FxHashMap; -use std::str::pattern::Pattern; use std::{fs, path}; -#[derive(Eq)] -pub struct SourceFile<'cx> { - // `cargo dev rename_lint` needs to be able to rename files. - pub path: Cell<&'cx str>, - pub contents: String, -} -impl<'cx> SourceFile<'cx> { - pub fn load(path: &'cx str) -> Self { - let mut contents = String::new(); - File::open_read(path).read_append_to_string(&mut contents); - SourceFile { - path: Cell::new(path), - contents, - } - } - - /// Splits the file's path into the crate it's a part of and the module it implements. - /// - /// Only supports paths in the form `CRATE_NAME/src/PATH/TO/FILE.rs` using the current - /// platform's path separator. The module path returned will use the current platform's - /// path separator. - pub fn path_as_krate_mod(&self) -> (&'cx str, &'cx str) { - let path = self.path.get(); - let Some((krate, path)) = path.split_once(path::MAIN_SEPARATOR) else { - return ("", ""); - }; - let module = if let Some(path) = path.strip_prefix("src") - && let Some(path) = path.strip_prefix(path::MAIN_SEPARATOR) - && let Some(path) = path.strip_suffix(".rs") - { - if path == "lib" { - "" - } else if let Some(path) = path.strip_suffix("mod") - && let Some(path) = path.strip_suffix(path::MAIN_SEPARATOR) - { - path - } else { - path - } - } else { - "" - }; - (krate, module) - } -} -impl PartialEq> for SourceFile<'_> { - fn eq(&self, other: &SourceFile<'_>) -> bool { - // We should only be creating one source file per path. - ptr::addr_eq(self, other) - } -} -impl Hash for SourceFile<'_> { - fn hash(&self, state: &mut H) { - ptr::hash(self, state); - } -} - -#[derive(Clone, Copy)] -pub struct Span<'cx> { - pub file: &'cx SourceFile<'cx>, - pub range: Range, -} -impl<'cx> Span<'cx> { - pub fn new(file: &'cx SourceFile<'cx>, range: Range) -> Self { - Self { file, range } - } -} - pub struct ParseCxImpl<'cx> { pub arena: &'cx DroplessArena, pub source_files: &'cx TypedArena>, @@ -100,92 +30,6 @@ pub fn new_parse_cx<'env, T>(f: impl for<'cx> FnOnce(&'cx mut Scoped<'cx, 'env, })) } -/// A string used as a temporary buffer used to avoid allocating for short lived strings. -pub struct StrBuf(String); -impl StrBuf { - /// Creates a new buffer with the specified initial capacity. - pub fn with_capacity(cap: usize) -> Self { - Self(String::with_capacity(cap)) - } - - /// Allocates the result of formatting the given value onto the arena. - pub fn alloc_display<'cx>(&mut self, arena: &'cx DroplessArena, value: impl Display) -> &'cx str { - self.0.clear(); - write!(self.0, "{value}").expect("`Display` impl returned an error"); - arena.alloc_str(&self.0) - } - - /// Allocates the string onto the arena with all ascii characters converted to - /// lowercase. - pub fn alloc_ascii_lower<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str { - self.0.clear(); - self.0.push_str(s); - self.0.make_ascii_lowercase(); - arena.alloc_str(&self.0) - } - - /// Collects all elements into the buffer and allocates that onto the arena. - pub fn alloc_collect<'cx, I>(&mut self, arena: &'cx DroplessArena, iter: I) -> &'cx str - where - I: IntoIterator, - String: Extend, - { - self.0.clear(); - self.0.extend(iter); - if self.0.is_empty() { - "" - } else { - arena.alloc_str(&self.0) - } - } - - /// Allocates the result of replacing all instances the pattern with the given string - /// onto the arena. - pub fn alloc_replaced<'cx>( - &mut self, - arena: &'cx DroplessArena, - s: &str, - pat: impl Pattern, - replacement: &str, - ) -> &'cx str { - let mut parts = s.split(pat); - let Some(first) = parts.next() else { - return ""; - }; - self.0.clear(); - self.0.push_str(first); - for part in parts { - self.0.push_str(replacement); - self.0.push_str(part); - } - if self.0.is_empty() { - "" - } else { - arena.alloc_str(&self.0) - } - } - - /// Performs an operation with the freshly cleared buffer. - pub fn with(&mut self, f: impl FnOnce(&mut String) -> T) -> T { - self.0.clear(); - f(&mut self.0) - } -} - -pub struct VecBuf(Vec); -impl VecBuf { - /// Creates a new buffer with the specified initial capacity. - pub fn with_capacity(cap: usize) -> Self { - Self(Vec::with_capacity(cap)) - } - - /// Performs an operation with the freshly cleared buffer. - pub fn with(&mut self, f: impl FnOnce(&mut Vec) -> R) -> R { - self.0.clear(); - f(&mut self.0) - } -} - #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum LintTool { Rustc, diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index 797eed7d5934..4c4fb7b81493 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -1,20 +1,22 @@ -use core::fmt::{self, Display}; +use core::cell::Cell; +use core::fmt::{self, Display, Write as _}; +use core::hash::{Hash, Hasher}; use core::marker::PhantomData; -use core::mem; use core::num::NonZero; use core::ops::{Deref, DerefMut}; use core::range::Range; use core::str::FromStr; +use core::str::pattern::Pattern; +use core::{mem, ptr}; +use rustc_arena::DroplessArena; use std::ffi::OsStr; use std::fs::{self, OpenOptions}; use std::io::{self, Read as _, Seek as _, SeekFrom, Write}; -use std::path::{Path, PathBuf}; +use std::path::{self, Path, PathBuf}; use std::process::{self, Command, Stdio}; use std::{env, thread}; use walkdir::WalkDir; -use crate::parse::SourceFile; - pub struct Scoped<'inner, 'outer: 'inner, T>(T, PhantomData<&'inner mut T>, PhantomData<&'outer mut ()>); impl Scoped<'_, '_, T> { pub fn new(value: T) -> Self { @@ -698,3 +700,165 @@ pub fn slice_groups_mut( } I { slice, split_idx } } + +/// A string used as a temporary buffer used to avoid allocating for short lived strings. +pub struct StrBuf(String); +impl StrBuf { + /// Creates a new buffer with the specified initial capacity. + pub fn with_capacity(cap: usize) -> Self { + Self(String::with_capacity(cap)) + } + + /// Allocates the result of formatting the given value onto the arena. + pub fn alloc_display<'cx>(&mut self, arena: &'cx DroplessArena, value: impl Display) -> &'cx str { + self.0.clear(); + write!(self.0, "{value}").expect("`Display` impl returned an error"); + if self.0.is_empty() { + "" + } else { + arena.alloc_str(&self.0) + } + } + + /// Allocates the string onto the arena with all ascii characters converted to + /// lowercase. + pub fn alloc_ascii_lower<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str { + self.0.clear(); + self.0.push_str(s); + self.0.make_ascii_lowercase(); + if self.0.is_empty() { + "" + } else { + arena.alloc_str(&self.0) + } + } + + /// Collects all elements into the buffer and allocates that onto the arena. + pub fn alloc_collect<'cx, I>(&mut self, arena: &'cx DroplessArena, iter: I) -> &'cx str + where + I: IntoIterator, + String: Extend, + { + self.0.clear(); + self.0.extend(iter); + if self.0.is_empty() { + "" + } else { + arena.alloc_str(&self.0) + } + } + + /// Allocates the result of replacing all instances the pattern with the given string + /// onto the arena. + pub fn alloc_replaced<'cx>( + &mut self, + arena: &'cx DroplessArena, + s: &str, + pat: impl Pattern, + replacement: &str, + ) -> &'cx str { + let mut parts = s.split(pat); + let Some(first) = parts.next() else { + return ""; + }; + self.0.clear(); + self.0.push_str(first); + for part in parts { + self.0.push_str(replacement); + self.0.push_str(part); + } + if self.0.is_empty() { + "" + } else { + arena.alloc_str(&self.0) + } + } + + /// Performs an operation with the freshly cleared buffer. + pub fn with(&mut self, f: impl FnOnce(&mut String) -> T) -> T { + self.0.clear(); + f(&mut self.0) + } +} + +pub struct VecBuf(Vec); +impl VecBuf { + /// Creates a new buffer with the specified initial capacity. + pub fn with_capacity(cap: usize) -> Self { + Self(Vec::with_capacity(cap)) + } + + /// Performs an operation with the freshly cleared buffer. + pub fn with(&mut self, f: impl FnOnce(&mut Vec) -> R) -> R { + self.0.clear(); + f(&mut self.0) + } +} + +#[derive(Eq)] +pub struct SourceFile<'cx> { + // `cargo dev rename_lint` needs to be able to rename files. + pub path: Cell<&'cx str>, + pub contents: String, +} +impl<'cx> SourceFile<'cx> { + pub fn load(path: &'cx str) -> Self { + let mut contents = String::new(); + File::open_read(path).read_append_to_string(&mut contents); + SourceFile { + path: Cell::new(path), + contents, + } + } + + /// Splits the file's path into the crate it's a part of and the module it implements. + /// + /// Only supports paths in the form `CRATE_NAME/src/PATH/TO/FILE.rs` using the current + /// platform's path separator. The module path returned will use the current platform's + /// path separator. + pub fn path_as_krate_mod(&self) -> (&'cx str, &'cx str) { + let path = self.path.get(); + let Some((krate, path)) = path.split_once(path::MAIN_SEPARATOR) else { + return ("", ""); + }; + let module = if let Some(path) = path.strip_prefix("src") + && let Some(path) = path.strip_prefix(path::MAIN_SEPARATOR) + && let Some(path) = path.strip_suffix(".rs") + { + if path == "lib" { + "" + } else if let Some(path) = path.strip_suffix("mod") + && let Some(path) = path.strip_suffix(path::MAIN_SEPARATOR) + { + path + } else { + path + } + } else { + "" + }; + (krate, module) + } +} +impl PartialEq> for SourceFile<'_> { + fn eq(&self, other: &SourceFile<'_>) -> bool { + // We should only be creating one source file per path. + ptr::addr_eq(self, other) + } +} +impl Hash for SourceFile<'_> { + fn hash(&self, state: &mut H) { + ptr::hash(self, state); + } +} + +#[derive(Clone, Copy)] +pub struct Span<'cx> { + pub file: &'cx SourceFile<'cx>, + pub range: Range, +} +impl<'cx> Span<'cx> { + pub fn new(file: &'cx SourceFile<'cx>, range: Range) -> Self { + Self { file, range } + } +} From fd3567a5d37a4490ac4ea0652a5b5a8d1531e24d Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Mon, 9 Feb 2026 09:31:20 -0500 Subject: [PATCH 07/11] `clippy_dev`: Use `annotate_snippets` to report all parsing errors together. --- clippy_dev/Cargo.toml | 3 + clippy_dev/src/diag.rs | 153 +++++++++++++ clippy_dev/src/edit_lints.rs | 54 ++--- clippy_dev/src/fmt.rs | 1 + clippy_dev/src/lib.rs | 6 +- clippy_dev/src/main.rs | 14 +- clippy_dev/src/new_lint.rs | 10 +- clippy_dev/src/parse.rs | 395 ++++++++++++++++++++------------- clippy_dev/src/parse/cursor.rs | 116 ++++++++-- clippy_dev/src/utils.rs | 16 +- 10 files changed, 559 insertions(+), 209 deletions(-) create mode 100644 clippy_dev/src/diag.rs diff --git a/clippy_dev/Cargo.toml b/clippy_dev/Cargo.toml index 49dc5c9f10a8..469fe6b8a2bb 100644 --- a/clippy_dev/Cargo.toml +++ b/clippy_dev/Cargo.toml @@ -5,10 +5,13 @@ version = "0.0.1" edition = "2024" [dependencies] +annotate-snippets = { version = "0.12.10", features = ["simd"] } +anstream = "0.6.20" 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.6" opener = "0.8" rustc-literal-escaper = "0.0.7" walkdir = "2.3" diff --git a/clippy_dev/src/diag.rs b/clippy_dev/src/diag.rs new file mode 100644 index 000000000000..17fbf74fdd45 --- /dev/null +++ b/clippy_dev/src/diag.rs @@ -0,0 +1,153 @@ +use crate::Span; +use annotate_snippets::renderer::{DEFAULT_TERM_WIDTH, Renderer}; +use annotate_snippets::{Annotation, AnnotationKind, Group, Level, Origin, Snippet}; +use core::panic::Location; +use std::borrow::Cow; +use std::io::Write as _; +use std::process; + +pub struct DiagCx { + out: anstream::Stderr, + renderer: Renderer, + has_err: bool, +} +impl Default for DiagCx { + fn default() -> Self { + let width = termize::dimensions().map_or(DEFAULT_TERM_WIDTH, |(w, _)| w); + Self { + out: anstream::stderr(), + renderer: Renderer::styled().term_width(width), + has_err: false, + } + } +} +impl Drop for DiagCx { + fn drop(&mut self) { + if self.has_err { + self.emit_err(&[ + Group::with_title( + Level::ERROR + .with_name("internal error") + .primary_title("errors were found, but it was assumed none occurred"), + ), + Group::with_title(Level::NOTE.secondary_title("any produced results may be incorrect")), + ]); + process::exit(1); + } + } +} +impl DiagCx { + pub fn exit_on_err(&self) { + if self.has_err { + process::exit(1); + } + } + + #[track_caller] + pub fn exit_assume_err(&mut self) -> ! { + if !self.has_err { + self.emit_err(&[ + Group::with_title( + Level::ERROR + .with_name("internal error") + .primary_title("errors were expected, but is was assumed one would occur"), + ), + mk_loc_group(), + ]); + } + process::exit(1); + } +} + +fn sp_to_snip(kind: AnnotationKind, sp: Span<'_>) -> Snippet<'_, Annotation<'_>> { + let line_starts = sp.file.line_starts(); + let first_line = match line_starts.binary_search(&sp.range.start) { + Ok(x) => x, + // Note: `Err(0)` isn't possible since `0` is always the first start. + Err(x) => x - 1, + }; + let start = line_starts[first_line] as usize; + let last_line = match line_starts.binary_search(&sp.range.end) { + Ok(x) => x, + Err(x) => x - 1, + }; + let end = line_starts + .get(last_line + 1) + .map_or(sp.file.contents.len(), |&x| x as usize); + Snippet::source(&sp.file.contents[start..end]) + .line_start(first_line + 1) + .path(sp.file.path.get()) + .annotation(kind.span((sp.range.start as usize - start..sp.range.end as usize - start).into())) +} + +fn mk_spanned_primary<'a>(level: Level<'a>, sp: Span<'a>, msg: impl Into>) -> Group<'a> { + level + .primary_title(msg.into()) + .element(sp_to_snip(AnnotationKind::Primary, sp)) +} + +fn mk_spanned_secondary<'a>(level: Level<'a>, sp: Span<'a>, msg: impl Into>) -> Group<'a> { + level + .secondary_title(msg.into()) + .element(sp_to_snip(AnnotationKind::Context, sp)) +} + +#[track_caller] +fn mk_loc_group() -> Group<'static> { + let loc = Location::caller(); + Level::INFO.secondary_title("error created here").element( + Origin::path(loc.file()) + .line(loc.line() as usize) + .char_column(loc.column() as usize), + ) +} + +impl DiagCx { + fn emit_err(&mut self, groups: &[Group<'_>]) { + let mut s = self.renderer.render(groups); + s.push('\n'); + self.out.write_all(s.as_bytes()).unwrap(); + self.has_err = true; + } + + #[track_caller] + pub fn emit_spanned_err<'a>(&mut self, sp: Span<'a>, msg: impl Into>) { + self.emit_err(&[mk_spanned_primary(Level::ERROR, sp, msg.into()), mk_loc_group()]); + } + + #[track_caller] + pub fn emit_spanless_err<'a>(&mut self, msg: impl Into>) { + self.emit_err(&[ + Group::with_title(Level::ERROR.primary_title(msg.into())), + mk_loc_group(), + ]); + } + + #[track_caller] + pub fn emit_already_deprecated(&mut self, name: &str) { + self.emit_spanless_err(format!("lint `{name}` is already deprecated")); + } + + #[track_caller] + pub fn emit_duplicate_lint(&mut self, sp: Span<'_>, first_sp: Span<'_>) { + self.emit_err(&[ + mk_spanned_primary(Level::ERROR, sp, "duplicate lint name declared"), + mk_spanned_secondary(Level::NOTE, first_sp, "previous declaration here"), + mk_loc_group(), + ]); + } + + #[track_caller] + pub fn emit_not_clippy_lint_name(&mut self, sp: Span<'_>) { + self.emit_err(&[ + mk_spanned_primary(Level::ERROR, sp, "not a clippy lint name"), + Group::with_title(Level::HELP.secondary_title("add the `clippy::` tool prefix")), + mk_loc_group(), + ]); + } + + #[track_caller] + pub fn emit_unknown_lint(&mut self, name: &str) { + self.emit_spanless_err(format!("unknown lint `{name}`")); + } +} diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index 078e0f2c6cd8..cd1ae61f88e5 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -1,9 +1,10 @@ use crate::parse::cursor::{self, Capture, Cursor}; use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, ParsedLints, RenamedLint}; use crate::utils::{ - ErrAction, FileUpdater, SourceFile, Span, 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 crate::{SourceFile, Span}; use core::mem; use rustc_lexer::TokenKind; use std::collections::hash_map::Entry; @@ -22,10 +23,9 @@ use std::{fs, path}; /// If a file path could not read from or written to pub fn deprecate<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, name: &'env str, reason: &'env str) { let mut data = cx.parse_lint_decls(); - let Entry::Occupied(mut lint) = data.lints.entry(name) else { - eprintln!("error: failed to find lint `{name}`"); - return; + cx.dcx.emit_unknown_lint(name); + cx.dcx.exit_assume_err(); }; let prev_lint = mem::replace( lint.get_mut(), @@ -38,9 +38,10 @@ pub fn deprecate<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, name }, ); let LintData::Active(prev_lint_data) = prev_lint.data else { - eprintln!("error: `{name}` is already deprecated"); - return; + cx.dcx.emit_already_deprecated(name); + cx.dcx.exit_assume_err(); }; + cx.dcx.exit_on_err(); remove_lint_declaration( name, @@ -56,14 +57,10 @@ pub fn deprecate<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, name pub fn uplift<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_name: &'env str, new_name: &'env str) { let mut data = cx.parse_lint_decls(); - - update_rename_targets(&mut data, old_name, LintName::new_rustc(new_name)); - let Entry::Occupied(mut lint) = data.lints.entry(old_name) else { - eprintln!("error: failed to find lint `{old_name}`"); - return; + cx.dcx.emit_unknown_lint(old_name); + cx.dcx.exit_assume_err(); }; - let prev_lint = mem::replace( lint.get_mut(), Lint { @@ -75,10 +72,12 @@ pub fn uplift<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam }, ); let LintData::Active(prev_lint_data) = prev_lint.data else { - eprintln!("error: `{old_name}` is already deprecated"); - return; + cx.dcx.emit_already_deprecated(old_name); + cx.dcx.exit_assume_err(); }; + cx.dcx.exit_on_err(); + update_rename_targets(&mut data, old_name, LintName::new_rustc(new_name)); let mut updater = FileUpdater::default(); let remove_mod = remove_lint_declaration(old_name, prev_lint.name_sp.file, &prev_lint_data, &data, &mut updater); let mut update_fn = uplift_update_fn(old_name, new_name, remove_mod); @@ -109,16 +108,11 @@ pub fn uplift<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam /// * If `old_name` doesn't name an existing lint. /// * If `old_name` names a deprecated or renamed lint. pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_name: &'env str, new_name: &'env str) { - let mut updater = FileUpdater::default(); let mut data = cx.parse_lint_decls(); - - update_rename_targets(&mut data, old_name, LintName::new_clippy(new_name)); - let Entry::Occupied(mut lint) = data.lints.entry(old_name) else { - eprintln!("error: failed to find lint `{old_name}`"); - return; + cx.dcx.emit_unknown_lint(old_name); + cx.dcx.exit_assume_err(); }; - let prev_lint = mem::replace( lint.get_mut(), Lint { @@ -130,10 +124,12 @@ pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam }, ); if !matches!(prev_lint.data, LintData::Active(_)) { - eprintln!("error: `{old_name}` is already deprecated"); - return; - }; + cx.dcx.emit_already_deprecated(old_name); + } + cx.dcx.exit_on_err(); + update_rename_targets(&mut data, old_name, LintName::new_clippy(new_name)); + let mut updater = FileUpdater::default(); let prev_file = prev_lint.name_sp.file; let mut rename_mod = false; if let Entry::Vacant(e) = data.lints.entry(new_name) { @@ -401,7 +397,9 @@ fn rename_update_fn<'a>( match text { // clippy::line_name or clippy::lint-name "clippy" => { - if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures) + if cursor + .match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures) + .is_ok() && cursor.get_text(captures[0]) == old_name { dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); @@ -464,7 +462,9 @@ fn rename_update_fn<'a>( }, // ::lint_name TokenKind::Colon - if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures) + if cursor + .match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures) + .is_ok() && cursor.get_text(captures[0]) == old_name => { dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index b6aa0eb3749d..b9c8fdc80b33 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -336,6 +336,7 @@ pub fn run(update_mode: UpdateMode) { new_parse_cx(|cx| { let mut data = cx.parse_lint_decls(); + cx.dcx.exit_on_err(); let mut updater = FileUpdater::default(); diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs index 2e75913f63f6..21563982c60c 100644 --- a/clippy_dev/src/lib.rs +++ b/clippy_dev/src/lib.rs @@ -1,5 +1,4 @@ #![feature( - bool_to_result, exit_status_error, macro_metavar_expr_concat, new_range, @@ -22,6 +21,7 @@ extern crate rustc_data_structures; #[expect(unused_extern_crates, reason = "required to link to rustc crates")] extern crate rustc_driver; extern crate rustc_lexer; +extern crate termize; pub mod dogfood; pub mod edit_lints; @@ -33,9 +33,11 @@ pub mod serve; pub mod setup; pub mod sync; +mod diag; mod generate; mod parse; mod utils; +pub use self::diag::DiagCx; pub use self::parse::{ParseCx, new_parse_cx}; -pub use self::utils::{ClippyInfo, UpdateMode}; +pub use self::utils::{ClippyInfo, SourceFile, Span, UpdateMode}; diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs index 5fe2295a1e21..9e0f2fa14cb0 100644 --- a/clippy_dev/src/main.rs +++ b/clippy_dev/src/main.rs @@ -26,9 +26,11 @@ fn main() { allow_no_vcs, } => dogfood::dogfood(fix, allow_dirty, allow_staged, allow_no_vcs), DevCommand::Fmt { check } => fmt::run(UpdateMode::from_check(check)), - DevCommand::UpdateLints { check } => { - new_parse_cx(|cx| cx.parse_lint_decls().gen_decls(UpdateMode::from_check(check))); - }, + DevCommand::UpdateLints { check } => new_parse_cx(|cx| { + let data = cx.parse_lint_decls(); + cx.dcx.exit_on_err(); + data.gen_decls(UpdateMode::from_check(check)); + }), DevCommand::NewLint { pass, name, @@ -36,7 +38,11 @@ fn main() { r#type, msrv, } => match new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv) { - Ok(()) => new_parse_cx(|cx| cx.parse_lint_decls().gen_decls(UpdateMode::Change)), + Ok(()) => new_parse_cx(|cx| { + let data = cx.parse_lint_decls(); + cx.dcx.exit_on_err(); + data.gen_decls(UpdateMode::Change); + }), Err(e) => eprintln!("Unable to create lint: {e}"), }, DevCommand::Setup(SetupCommand { subcommand }) => match subcommand { diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index 50a6537f8a89..804f558012f3 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -535,10 +535,16 @@ fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) { let mut captures = [Capture::EMPTY]; while let Some(name) = cursor.find_capture_ident() { match cursor.get_text(name) { - "declare_clippy_lint" if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_close_brace() => { + "declare_clippy_lint" + if cursor.match_all(&[Bang, OpenBrace], &mut []).is_ok() && cursor.find_close_brace() => + { decl_end = Some(cursor.pos()); }, - "impl" if cursor.match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) => { + "impl" + if cursor + .match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) + .is_ok() => + { match cursor.get_text(captures[0]) { "LateLintPass" => context = Some("LateContext"), "EarlyLintPass" => context = Some("EarlyContext"), diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 95fb22accc07..e589485dc8f4 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -1,13 +1,13 @@ pub mod cursor; use self::cursor::{Capture, Cursor, IdentPat}; -use crate::utils::{ - ErrAction, Scoped, SourceFile, Span, StrBuf, VecBuf, expect_action, slice_groups_mut, walk_dir_no_dot_or_target, -}; +use crate::utils::{ErrAction, Scoped, StrBuf, VecBuf, expect_action, slice_groups_mut, walk_dir_no_dot_or_target}; +use crate::{DiagCx, SourceFile, Span}; use core::fmt::{self, Display}; use core::range::Range; use rustc_arena::{DroplessArena, TypedArena}; use rustc_data_structures::fx::FxHashMap; +use std::collections::hash_map::{Entry, VacantEntry}; use std::{fs, path}; pub struct ParseCxImpl<'cx> { @@ -15,6 +15,7 @@ pub struct ParseCxImpl<'cx> { pub source_files: &'cx TypedArena>, pub str_buf: StrBuf, pub str_list_buf: VecBuf<&'cx str>, + pub dcx: DiagCx, } pub type ParseCx<'cx> = &'cx mut ParseCxImpl<'cx>; @@ -27,6 +28,7 @@ pub fn new_parse_cx<'env, T>(f: impl for<'cx> FnOnce(&'cx mut Scoped<'cx, 'env, source_files: &source_files, str_buf: StrBuf::with_capacity(128), str_list_buf: VecBuf::with_capacity(128), + dcx: DiagCx::default(), })) } @@ -43,25 +45,29 @@ impl LintTool { Self::Clippy => "clippy::", } } + + pub fn from_prefix(s: &str) -> Option { + (s == "clippy").then_some(Self::Clippy) + } } #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct LintName<'cx> { - pub name: &'cx str, pub tool: LintTool, + pub name: &'cx str, } impl<'cx> LintName<'cx> { pub fn new_rustc(name: &'cx str) -> Self { Self { - name, tool: LintTool::Rustc, + name, } } pub fn new_clippy(name: &'cx str) -> Self { Self { - name, tool: LintTool::Clippy, + name, } } } @@ -149,11 +155,26 @@ impl<'cx> ParsedLints<'cx> { tail.iter().take_while(|&x| x.decl_sp.file == head.decl_sp.file).count() }) } + + #[track_caller] + fn get_vacant_lint<'a>( + &'a mut self, + dcx: &mut DiagCx, + name: &'cx str, + name_sp: Span<'cx>, + ) -> Option>> { + match self.lints.entry(name) { + Entry::Vacant(e) => Some(e), + Entry::Occupied(e) => { + dcx.emit_duplicate_lint(name_sp, e.get().name_sp); + None + }, + } + } } impl<'cx> ParseCxImpl<'cx> { /// Finds and parses all lint declarations. - #[must_use] pub fn parse_lint_decls(&mut self) -> ParsedLints<'cx> { let mut data = ParsedLints { #[expect(clippy::default_trait_access)] @@ -209,83 +230,87 @@ impl<'cx> ParseCxImpl<'cx> { fn parse_lint_src_file(&mut self, data: &mut ParsedLints<'cx>, file: &'cx SourceFile<'cx>) { #[allow(clippy::enum_glob_use)] use cursor::Pat::*; - #[rustfmt::skip] - static LINT_DECL_TOKENS: &[cursor::Pat] = &[ - // !{ /// docs - Bang, OpenBrace, AnyComments, - // #[clippy::version = "version"] - Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, - Ident(IdentPat::version), Eq, LitStr, CloseBracket, - // pub NAME, GROUP, - Ident(IdentPat::r#pub), CaptureIdent, Comma, AnyComments, CaptureIdent, Comma, - ]; let mut cursor = Cursor::new(&file.contents); let mut captures = [Capture::EMPTY; 3]; while let Some(mac_name) = cursor.find_capture_ident() { - captures[1] = Capture::EMPTY; + if !cursor.eat_bang() { + continue; + } match cursor.get_text(mac_name) { - "declare_clippy_lint" - if cursor.match_all(LINT_DECL_TOKENS, &mut captures) && cursor.find_close_brace() => - { - assert!( - data.lints - .insert( - self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[0])), - Lint { - name_sp: captures[0].mk_sp(file), - data: LintData::Active(ActiveLint { - group: cursor.get_text(captures[1]), - decl_range: mac_name.pos..cursor.pos(), - }), - }, - ) - .is_none() - ); + "declare_clippy_lint" => { + #[rustfmt::skip] + static DECL_START: &[cursor::Pat] = &[ + // { /// docs + OpenBrace, AnyComments, + // #[clippy::version = "version"] + Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, + Ident(IdentPat::version), Eq, CaptureLitStr, CloseBracket, + // pub NAME, GROUP, "desc", + Ident(IdentPat::r#pub), CaptureIdent, Comma, + AnyComments, CaptureIdent, Comma, AnyComments, LitStr, + ]; + #[rustfmt::skip] + static OPTION: &[cursor::Pat] = &[ + // @option = value + AnyComments, At, AnyIdent, Eq, Lit, + ]; + + if let Err(expected) = cursor + .match_all(DECL_START, &mut captures) + .and_then(|()| { + (!cursor.eat_comma()).ok_or(()).or_else(|()| { + cursor.eat_list(|cursor| cursor.match_all(OPTION, &mut []).map(|()| true)) + }) + }) + .and_then(|()| cursor.eat_close_brace().ok_or("`}`")) + { + cursor.emit_unexpected(&mut self.dcx, file, expected); + } else if let name = self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[1])) + && let name_sp = captures[1].mk_sp(file) + && let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp) + { + let _ = self.parse_version(cursor.get_text(captures[0]), captures[0].mk_sp(file)); + e.insert(Lint { + name_sp, + data: LintData::Active(ActiveLint { + group: cursor.get_text(captures[2]), + decl_range: mac_name.pos..cursor.pos(), + }), + }); + } }, - mac @ ("declare_lint_pass" | "impl_lint_pass") - if cursor.match_all(&[Bang, OpenParen, CaptureDocLines, CaptureIdent], &mut captures) - && cursor.opt_match_all(&[Lt, CaptureLifetime, Gt], &mut captures[2..]) - && cursor.match_all(&[FatArrow, OpenBracket], &mut captures) => - { - let mac = if matches!(mac, "declare_lint_pass") { - LintPassMac::Declare - } else { - LintPassMac::Impl - }; - let docs = cursor.get_text(captures[0]); - let name = cursor.get_text(captures[1]); - let lt = cursor.get_text(captures[2]); - let lt = if lt.is_empty() { None } else { Some(lt) }; - - let lints = self.str_list_buf.with(|buf| { - loop { - match cursor.capture_opt_path(&mut self.str_buf, self.arena) { - Ok(None) => break, - Ok(Some(p)) => buf.push(p), - Err(()) => panic!("error parsing path"), - } - if !cursor.eat_comma() { - break; - } - } - - // The arena panics when allocating a size of zero. - Some(if buf.is_empty() { - &mut [] - } else { - self.arena.alloc_slice(buf) + mac @ ("declare_lint_pass" | "impl_lint_pass") => { + let mut has_lt = false; + let mut lints: &mut [_] = &mut []; + if let Err(expected) = cursor + .match_all(&[OpenParen, CaptureDocLines, CaptureIdent], &mut captures) + .and_then(|()| cursor.opt_match_all(&[Lt, CaptureLifetime, Gt], &mut captures[2..])) + .and_then(|res| { + has_lt = res; + cursor.match_all(&[FatArrow, OpenBracket], &mut []) + }) + .and_then(|()| { + cursor.capture_list(&mut self.str_list_buf, self.arena, |cursor| { + cursor.capture_opt_path(&mut self.str_buf, self.arena) + }) + }) + .and_then(|res| { + lints = res; + cursor.match_all(&[CloseBracket, CloseParen, Semi], &mut []) }) - }); - - if let Some(lints) = lints - && cursor.match_all(&[CloseBracket, CloseParen, Semi], &mut []) { + cursor.emit_unexpected(&mut self.dcx, file, expected); + } else { data.lint_passes.push(LintPass { - docs, - name, - lt, - mac, + docs: cursor.get_text(captures[0]), + name: cursor.get_text(captures[1]), + lt: has_lt.then(|| cursor.get_text(captures[2])), + mac: if matches!(mac, "declare_lint_pass") { + LintPassMac::Declare + } else { + LintPassMac::Impl + }, decl_sp: Span::new(file, mac_name.pos..cursor.pos()), lints, }); @@ -305,8 +330,8 @@ impl<'cx> ParseCxImpl<'cx> { // #[clippy::version = "version"] Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, Ident(IdentPat::version), Eq, CaptureLitStr, CloseBracket, - // ("first", "second"), - OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma, + // ("first", "second") + OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, ]; #[rustfmt::skip] static DEPRECATED_TOKENS: &[cursor::Pat] = &[ @@ -325,113 +350,163 @@ impl<'cx> ParseCxImpl<'cx> { let mut cursor = Cursor::new(&file.contents); let mut captures = [Capture::EMPTY; 3]; - // First instance is the macro definition. - assert!( - cursor.find_ident("declare_with_version"), - "error reading deprecated lints" - ); - - if cursor.find_ident("declare_with_version") && cursor.match_all(DEPRECATED_TOKENS, &mut []) { - while cursor.match_all(DECL_TOKENS, &mut captures) { - assert!( - data.lints - .insert( - self.parse_clippy_lint_name(file, cursor.get_text(captures[1])), - Lint { - name_sp: captures[1].mk_sp(file), - data: LintData::Deprecated(DeprecatedLint { - reason: self.parse_str_single_line(file, cursor.get_text(captures[2])), - version: self.parse_str_single_line(file, cursor.get_text(captures[0])), - }), - }, + if let Err(expected) = cursor + .find_ident("declare_with_version") + .ok_or("`declare_with_version`") + .and_then(|()| { + cursor + .find_ident("declare_with_version") + .ok_or("`declare_with_version`") + }) + .and_then(|()| cursor.match_all(DEPRECATED_TOKENS, &mut [])) + .and_then(|()| { + cursor.eat_list(|cursor| { + let parsed = cursor.opt_match_all(DECL_TOKENS, &mut captures)?; + if parsed + && let [version, name, reason] = captures + && let name_sp = name.mk_sp(file) + && let (Some(version), Some(name), Some(reason)) = ( + self.parse_version(cursor.get_text(version), version.mk_sp(file)), + self.parse_clippy_lint_name(cursor.get_text(name), name_sp), + self.parse_str_lit(cursor.get_text(reason), reason.mk_sp(file)), ) - .is_none() - ); - } - } else { - panic!("error reading deprecated lints"); - } - - if cursor.find_ident("declare_with_version") && cursor.match_all(RENAMED_TOKENS, &mut []) { - while cursor.match_all(DECL_TOKENS, &mut captures) { - assert!( - data.lints - .insert( - self.parse_clippy_lint_name(file, cursor.get_text(captures[1])), - Lint { - name_sp: captures[1].mk_sp(file), - data: LintData::Renamed(RenamedLint { - new_name: self.parse_lint_name(file, cursor.get_text(captures[2])), - version: self.parse_str_single_line(file, cursor.get_text(captures[0])), - }), - }, + && let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp) + { + e.insert(Lint { + name_sp, + data: LintData::Deprecated(DeprecatedLint { reason, version }), + }); + } + Ok(parsed) + }) + }) + .and_then(|()| { + cursor + .find_ident("declare_with_version") + .ok_or("`declare_with_version`") + }) + .and_then(|()| cursor.match_all(RENAMED_TOKENS, &mut [])) + .and_then(|()| { + cursor.eat_list(|cursor| { + let parsed = cursor.opt_match_all(DECL_TOKENS, &mut captures)?; + if parsed + && let [version, name, new_name] = captures + && let name_sp = name.mk_sp(file) + && let (Some(version), Some(name), Some(new_name)) = ( + self.parse_version(cursor.get_text(version), version.mk_sp(file)), + self.parse_clippy_lint_name(cursor.get_text(name), name_sp), + self.parse_lint_name(cursor.get_text(new_name), new_name.mk_sp(file)), ) - .is_none() - ); - } - } else { - panic!("error reading renamed lints"); + && let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp) + { + e.insert(Lint { + name_sp, + data: LintData::Renamed(RenamedLint { new_name, version }), + }); + } + Ok(parsed) + }) + }) + { + cursor.emit_unexpected(&mut self.dcx, file, expected); } } - /// Removes the line splices and surrounding quotes from a string literal - fn parse_str_lit(&mut self, s: &'cx str) -> &'cx str { - let (s, is_raw) = if let Some(s) = s.strip_prefix("r") { - (s.trim_matches('#'), true) + /// Removes the line splices and surrounding quotes from a string literal. + fn parse_str_lit(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<&'cx str> { + let (s, is_raw, sp_base) = if let Some(trimmed) = s.strip_prefix("r") { + let trimmed = trimmed.trim_start_matches('#'); + #[expect(clippy::cast_possible_truncation)] + let sp_base = (s.len() - trimmed.len() + 1) as u32; + (trimmed.trim_end_matches('#'), true, sp_base) } else { - (s, false) + (s, false, 1) }; + let sp_base = sp.range.start + sp_base; let s = s .strip_prefix('"') .and_then(|s| s.strip_suffix('"')) .unwrap_or_else(|| panic!("expected quoted string, found `{s}`")); + let mut is_ok = true; if is_raw { - s + rustc_literal_escaper::check_raw_str(s, |range, c| { + if c.is_err_and(|e| e.is_fatal()) { + #[expect(clippy::cast_possible_truncation)] + let range = range.start as u32 + sp_base..range.end as u32 + sp_base; + self.dcx + .emit_spanned_err(Span::new(sp.file, range), "invalid string escape sequence"); + is_ok = false; + } + }); + is_ok.then_some(s) } else { self.str_buf.with(|buf| { - rustc_literal_escaper::unescape_str(s, &mut |_, ch| { - if let Ok(ch) = ch { - buf.push(ch); - } + rustc_literal_escaper::unescape_str(s, |range, c| match c { + Ok(c) => buf.push(c), + Err(e) if e.is_fatal() => { + #[expect(clippy::cast_possible_truncation)] + let range = range.start as u32 + sp_base..range.end as u32 + sp_base; + self.dcx + .emit_spanned_err(Span::new(sp.file, range), "invalid string escape sequence"); + is_ok = false; + }, + Err(_) => {}, }); - if buf == s { - s - } else if buf.is_empty() { - "" - } else { - self.arena.alloc_str(buf) - } + is_ok.then(|| { + if buf == s { + s + } else if buf.is_empty() { + "" + } else { + self.arena.alloc_str(buf) + } + }) }) } } - fn parse_str_single_line(&mut self, file: &SourceFile<'_>, s: &'cx str) -> &'cx str { - let value = self.parse_str_lit(s); - assert!( - !value.contains('\n'), - "error parsing `{}`: `{s}` should be a single line string", - file.path.get(), - ); - value + #[track_caller] + fn parse_lint_name(&mut self, s: &'cx str, sp: Span<'cx>) -> Option> { + let s = self.parse_str_lit(s, sp)?; + let (tool, name) = match s.split_once("::") { + Some((tool, name)) if let Some(tool) = LintTool::from_prefix(tool) => (tool, name), + Some(..) => { + self.dcx.emit_spanned_err(sp, "unknown lint tool"); + return None; + }, + None => (LintTool::Rustc, s), + }; + if name + .bytes() + .all(|c| matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) + { + Some(LintName { tool, name }) + } else { + self.dcx.emit_spanned_err(sp, "unparsable lint name"); + None + } } - fn parse_clippy_lint_name(&mut self, file: &SourceFile<'_>, s: &'cx str) -> &'cx str { - match self.parse_str_single_line(file, s).strip_prefix("clippy::") { - Some(x) => x, - None => panic!( - "error parsing `{}`: `{s}` should be a string starting with `clippy::`", - file.path.get(), - ), + #[track_caller] + fn parse_clippy_lint_name(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<&'cx str> { + let name = self.parse_lint_name(s, sp)?; + if name.tool == LintTool::Clippy { + Some(name.name) + } else { + self.dcx.emit_not_clippy_lint_name(sp); + None } } - fn parse_lint_name(&mut self, file: &SourceFile<'_>, s: &'cx str) -> LintName<'cx> { - let s = self.parse_str_single_line(file, s); - let (name, tool) = match s.strip_prefix("clippy::") { - Some(s) => (s, LintTool::Clippy), - None => (s, LintTool::Rustc), - }; - LintName { name, tool } + #[track_caller] + fn parse_version(&mut self, s: &'cx str, sp: Span<'cx>) -> Option<&'cx str> { + let s = self.parse_str_lit(s, sp)?; + if s.bytes().all(|c| matches!(c, b'0'..=b'9' | b'.')) || matches!(s, "pre 1.29.0" | "CURRENT_RUSTC_VERSION") { + Some(s) + } else { + self.dcx.emit_spanned_err(sp, "unparsable version number"); + None + } } } diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 76cbb070883e..2ee007615a67 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -1,4 +1,5 @@ -use super::{SourceFile, Span, StrBuf}; +use crate::utils::{StrBuf, VecBuf}; +use crate::{DiagCx, SourceFile, Span}; use core::{ptr, slice}; use rustc_arena::DroplessArena; use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; @@ -17,6 +18,8 @@ pub enum Pat { CaptureIdent, CaptureLifetime, CaptureLitStr, + AnyIdent, + At, Bang, CloseBracket, CloseParen, @@ -27,6 +30,7 @@ pub enum Pat { Gt, Ident(IdentPat), Lifetime, + Lit, LitStr, Lt, OpenBrace, @@ -35,6 +39,34 @@ pub enum Pat { Pound, Semi, } +impl Pat { + pub fn desc(self) -> &'static str { + match self { + Self::AnyComments => "comments", + Self::CaptureDocLines => "doc line comments", + Self::AnyIdent | Self::CaptureIdent => "an identifier", + Self::At => "`@`", + Self::Bang => "`!`", + Self::CloseBracket => "`]`", + Self::CloseParen => "`)`", + Self::Comma => "`,`", + Self::DoubleColon => "`::`", + Self::Eq => "`=`", + Self::FatArrow => "`=>`", + Self::Gt => "`>`", + Self::Ident(x) => x.desc(), + Self::Lifetime | Self::CaptureLifetime => "a lifetime", + Self::Lit => "a literal", + Self::LitStr | Self::CaptureLitStr => "a string literal", + Self::Lt => "`<`", + Self::OpenBrace => "`{`", + Self::OpenBracket => "`[`", + Self::OpenParen => "`(`", + Self::Pound => "`#`", + Self::Semi => "`;`", + } + } +} macro_rules! ident_or_lit { ($ident:ident) => { @@ -53,6 +85,10 @@ macro_rules! decl_ident_pats { pub fn as_str(self) -> &'static str { match self { $(Self::$ident => ident_or_lit!($ident $($s)?)),* } } + + pub fn desc(self) -> &'static str { + match self { $(Self::$ident => concat!("`", ident_or_lit!($ident $($s)?), "`")),* } + } } } } @@ -142,6 +178,17 @@ impl<'txt> Cursor<'txt> { self.next_token = self.inner.advance_token(); } + #[track_caller] + pub fn emit_unexpected<'cx>(&self, dcx: &mut DiagCx, file: &'cx SourceFile<'cx>, expected: &'static str) { + let sp = Span::new(file, self.pos..self.pos + self.next_token.len); + let msg = if matches!(self.next_token.kind, TokenKind::Eof) { + "end of file" + } else { + "token" + }; + dcx.emit_spanned_err(sp, format!("unexpected {msg}, expected {expected}")); + } + /// Consumes tokens until the given pattern is either fully matched of fails to match. /// Returns whether the pattern was fully matched. /// @@ -159,12 +206,16 @@ impl<'txt> Cursor<'txt> { (Pat::AnyComments, _) => return true, (Pat::Ident(x), TokenKind::Ident) if x.as_str() == self.peek_text() => break, - (Pat::Bang, TokenKind::Bang) + (Pat::Lit, TokenKind::Ident) if matches!(self.peek_text(), "true" | "false") => break, + (Pat::At, TokenKind::At) + | (Pat::AnyIdent, TokenKind::Ident | TokenKind::RawIdent) + | (Pat::Bang, TokenKind::Bang) | (Pat::CloseBracket, TokenKind::CloseBracket) | (Pat::CloseParen, TokenKind::CloseParen) | (Pat::Comma, TokenKind::Comma) | (Pat::Eq, TokenKind::Eq) | (Pat::Lifetime, TokenKind::Lifetime { .. }) + | (Pat::Lit, TokenKind::Literal { .. }) | (Pat::Lt, TokenKind::Lt) | (Pat::Gt, TokenKind::Gt) | (Pat::OpenBrace, TokenKind::OpenBrace) @@ -223,11 +274,11 @@ impl<'txt> Cursor<'txt> { }; return true; }, - (Pat::CaptureDocLines, _) => { *captures.next().unwrap() = Capture::EMPTY; return true; }, + _ => return false, } } @@ -282,7 +333,11 @@ impl<'txt> Cursor<'txt> { /// /// Only paths containing identifiers separated by `::` with a possible leading `::`. /// Generic arguments and qualified paths are not considered. - pub fn capture_opt_path(&mut self, buf: &mut StrBuf, arena: &'txt DroplessArena) -> Result, ()> { + pub fn capture_opt_path( + &mut self, + buf: &mut StrBuf, + arena: &'txt DroplessArena, + ) -> Result, &'static str> { #[derive(Clone, Copy)] enum State { Start, @@ -309,7 +364,7 @@ impl<'txt> Cursor<'txt> { }, (State::Ident, _) => break, (State::Start, _) => return Ok(None), - (State::Sep, _) => return Err(()), + (State::Sep, _) => return Err(Pat::AnyIdent.desc()), } } let text = self.text[start as usize..self.pos as usize].trim(); @@ -321,6 +376,36 @@ impl<'txt> Cursor<'txt> { }) } + pub fn eat_list(&mut self, mut f: impl FnMut(&mut Self) -> Result) -> Result<(), &'static str> { + while f(self)? { + if !self.eat_comma() { + break; + } + } + Ok(()) + } + + pub fn capture_list<'cx, T: Copy>( + &mut self, + buf: &mut VecBuf, + arena: &'cx DroplessArena, + mut f: impl FnMut(&mut Self) -> Result, &'static str>, + ) -> Result<&'cx mut [T], &'static str> { + buf.with(|buf| { + while let Some(x) = f(self)? { + buf.push(x); + if !self.eat_comma() { + break; + } + } + Ok(if buf.is_empty() { + &mut [] + } else { + arena.alloc_slice(buf) + }) + }) + } + /// Attempts to match a sequence of patterns at the current position. Returns whether /// all patterns were successfully matched. /// @@ -330,10 +415,10 @@ impl<'txt> Cursor<'txt> { /// unmodified. /// /// If the match fails the cursor will be positioned at the first failing token. - #[must_use] - pub fn match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> bool { + pub fn match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> Result<(), &'static str> { let mut captures = captures.iter_mut(); - pats.iter().all(|&p| self.match_impl(p, &mut captures)) + pats.iter() + .try_for_each(|&pat| self.match_impl(pat, &mut captures).ok_or(pat.desc())) } /// Attempts to match a sequence of patterns at the current position. Returns whether @@ -345,13 +430,16 @@ impl<'txt> Cursor<'txt> { /// unmodified. /// /// If the match fails the cursor will be positioned at the first failing token. - #[must_use] - pub fn opt_match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> bool { + pub fn opt_match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> Result { let mut captures = captures.iter_mut(); - pats.iter() + match pats + .iter() .try_for_each(|p| self.match_impl(*p, &mut captures).ok_or(p)) - .err() - .is_none_or(|p| ptr::addr_eq(pats.as_ptr(), p)) + { + Ok(()) => Ok(true), + Err(p) if ptr::addr_eq(pats.as_ptr(), p) => Ok(false), + Err(p) => Err(p.desc()), + } } } @@ -400,6 +488,8 @@ macro_rules! mk_tk_methods { } } mk_tk_methods! { + ["`!`"] + bang(&mut self) { TokenKind::Bang } ["`}`"] close_brace(&mut self) { TokenKind::CloseBrace } ["`]`"] diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index 4c4fb7b81493..58833be15f59 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -1,4 +1,4 @@ -use core::cell::Cell; +use core::cell::{Cell, OnceCell}; use core::fmt::{self, Display, Write as _}; use core::hash::{Hash, Hasher}; use core::marker::PhantomData; @@ -8,6 +8,7 @@ use core::range::Range; use core::str::FromStr; use core::str::pattern::Pattern; use core::{mem, ptr}; +use memchr::memchr_iter; use rustc_arena::DroplessArena; use std::ffi::OsStr; use std::fs::{self, OpenOptions}; @@ -799,18 +800,31 @@ impl VecBuf { pub struct SourceFile<'cx> { // `cargo dev rename_lint` needs to be able to rename files. pub path: Cell<&'cx str>, + pub line_starts: OnceCell>, pub contents: String, } impl<'cx> SourceFile<'cx> { + #[must_use] pub fn load(path: &'cx str) -> Self { let mut contents = String::new(); File::open_read(path).read_append_to_string(&mut contents); SourceFile { path: Cell::new(path), + line_starts: OnceCell::new(), contents, } } + pub fn line_starts(&self) -> &[u32] { + self.line_starts.get_or_init(|| { + let mut line_starts = Vec::with_capacity(self.contents.len() / 32 + 4); + line_starts.push(0); + #[expect(clippy::cast_possible_truncation)] + line_starts.extend(memchr_iter(b'\n', self.contents.as_bytes()).map(|x| x as u32 + 1)); + line_starts + }) + } + /// Splits the file's path into the crate it's a part of and the module it implements. /// /// Only supports paths in the form `CRATE_NAME/src/PATH/TO/FILE.rs` using the current From 9c14b1b1bd762616751b36c567781ce4b87bec8e Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Mon, 16 Feb 2026 05:22:50 -0500 Subject: [PATCH 08/11] `clippy_dev`: Move config parsing to the new parsing framework. --- clippy_config/src/conf.rs | 14 +- clippy_dev/src/fmt.rs | 248 +++------------------------------ clippy_dev/src/generate.rs | 31 ++++- clippy_dev/src/parse.rs | 93 +++++++++++++ clippy_dev/src/parse/cursor.rs | 112 +++++++++++++-- 5 files changed, 253 insertions(+), 245 deletions(-) diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index d910eb47752b..e38670dccac5 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -602,7 +602,12 @@ define_Conf! { #[lints(inconsistent_struct_constructor)] check_inconsistent_struct_field_initializers: bool = false, /// Whether to also run the listed lints on private items. - #[lints(missing_errors_doc, missing_panics_doc, missing_safety_doc, unnecessary_safety_doc)] + #[lints( + missing_errors_doc, + missing_panics_doc, + missing_safety_doc, + unnecessary_safety_doc, + )] check_private_items: bool = false, /// The maximum cognitive complexity a function can have #[lints(cognitive_complexity)] @@ -709,7 +714,12 @@ define_Conf! { #[lints(large_futures)] future_size_threshold: u64 = 16 * 1024, /// A list of paths to types that should be treated as if they do not contain interior mutability - #[lints(borrow_interior_mutable_const, declare_interior_mutable_const, ifs_same_cond, mutable_key_type)] + #[lints( + borrow_interior_mutable_const, + declare_interior_mutable_const, + ifs_same_cond, + mutable_key_type, + )] ignore_interior_mutability: Vec = Vec::from(["bytes::Bytes".into()]), /// Sets the scope ("crate", "file", or "module") in which duplicate inherent `impl` blocks for the same type are linted. #[lints(multiple_inherent_impl)] diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index b9c8fdc80b33..77c53b11d03d 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -4,232 +4,10 @@ use crate::utils::{ ErrAction, FileUpdater, UpdateMode, UpdateStatus, VecBuf, expect_action, run_with_output, split_args_for_threads, walk_dir_no_dot_or_target, }; -use itertools::Itertools; -use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize}; use std::fmt::Write; -use std::fs; -use std::io::{self, Read}; -use std::ops::ControlFlow; -use std::path::PathBuf; +use std::io::Read; use std::process::{self, Command, Stdio}; -pub enum Error { - Io(io::Error), - Parse(PathBuf, usize, String), - CheckFailed, -} - -impl From for Error { - fn from(error: io::Error) -> Self { - Self::Io(error) - } -} - -impl Error { - fn display(&self) { - match self { - Self::CheckFailed => { - eprintln!("Formatting check failed!\nRun `cargo dev fmt` to update."); - }, - Self::Io(err) => { - eprintln!("error: {err}"); - }, - Self::Parse(path, line, msg) => { - eprintln!("error parsing `{}:{line}`: {msg}", path.display()); - }, - } - } -} - -struct ClippyConf<'a> { - name: &'a str, - attrs: &'a str, - lints: Vec<&'a str>, - field: &'a str, -} - -fn offset_to_line(text: &str, offset: usize) -> usize { - match text.split('\n').try_fold((1usize, 0usize), |(line, pos), s| { - let pos = pos + s.len() + 1; - if pos > offset { - ControlFlow::Break(line) - } else { - ControlFlow::Continue((line + 1, pos)) - } - }) { - ControlFlow::Break(x) | ControlFlow::Continue((x, _)) => x, - } -} - -/// Formats the configuration list in `clippy_config/src/conf.rs` -#[expect(clippy::too_many_lines)] -fn fmt_conf(check: bool) -> Result<(), Error> { - #[derive(Clone, Copy)] - enum State { - Start, - Docs, - Pound, - OpenBracket, - Attr(u32), - Lints, - EndLints, - Field, - } - - let path = "clippy_config/src/conf.rs"; - let text = fs::read_to_string(path)?; - - let (pre, conf) = text - .split_once("define_Conf! {\n") - .expect("can't find config definition"); - let (conf, post) = conf.split_once("\n}\n").expect("can't find config definition"); - let conf_offset = pre.len() + 15; - - let mut pos = 0u32; - let mut attrs_start = 0; - let mut attrs_end = 0; - let mut field_start = 0; - let mut lints = Vec::new(); - let mut name = ""; - let mut fields = Vec::new(); - let mut state = State::Start; - - for (i, t) in tokenize(conf, FrontmatterAllowed::No) - .map(|x| { - let start = pos; - pos += x.len; - (start as usize, x) - }) - .filter(|(_, t)| !matches!(t.kind, TokenKind::Whitespace)) - { - match (state, t.kind) { - (State::Start, TokenKind::LineComment { doc_style: Some(_) }) => { - attrs_start = i; - attrs_end = i + t.len as usize; - state = State::Docs; - }, - (State::Start, TokenKind::Pound) => { - attrs_start = i; - attrs_end = i; - state = State::Pound; - }, - (State::Docs, TokenKind::LineComment { doc_style: Some(_) }) => attrs_end = i + t.len as usize, - (State::Docs, TokenKind::Pound) => state = State::Pound, - (State::Pound, TokenKind::OpenBracket) => state = State::OpenBracket, - (State::OpenBracket, TokenKind::Ident) => { - state = if conf[i..i + t.len as usize] == *"lints" { - State::Lints - } else { - State::Attr(0) - }; - }, - (State::Attr(0), TokenKind::CloseBracket) => { - attrs_end = i + 1; - state = State::Docs; - }, - (State::Attr(x), TokenKind::OpenParen | TokenKind::OpenBracket | TokenKind::OpenBrace) => { - state = State::Attr(x + 1); - }, - (State::Attr(x), TokenKind::CloseParen | TokenKind::CloseBracket | TokenKind::CloseBrace) => { - state = State::Attr(x - 1); - }, - (State::Lints, TokenKind::Ident) => lints.push(&conf[i..i + t.len as usize]), - (State::Lints, TokenKind::CloseBracket) => state = State::EndLints, - (State::EndLints | State::Docs, TokenKind::Ident) => { - field_start = i; - name = &conf[i..i + t.len as usize]; - state = State::Field; - }, - (State::Field, TokenKind::LineComment { doc_style: Some(_) }) => { - #[expect(clippy::drain_collect)] - fields.push(ClippyConf { - name, - attrs: &conf[attrs_start..attrs_end], - lints: lints.drain(..).collect(), - field: conf[field_start..i].trim_end(), - }); - attrs_start = i; - attrs_end = i + t.len as usize; - state = State::Docs; - }, - (State::Field, TokenKind::Pound) => { - #[expect(clippy::drain_collect)] - fields.push(ClippyConf { - name, - attrs: &conf[attrs_start..attrs_end], - lints: lints.drain(..).collect(), - field: conf[field_start..i].trim_end(), - }); - attrs_start = i; - attrs_end = i; - state = State::Pound; - }, - (State::Field | State::Attr(_), _) - | (State::Lints, TokenKind::Comma | TokenKind::OpenParen | TokenKind::CloseParen) => {}, - _ => { - return Err(Error::Parse( - PathBuf::from(path), - offset_to_line(&text, conf_offset + i), - format!("unexpected token `{}`", &conf[i..i + t.len as usize]), - )); - }, - } - } - - if !matches!(state, State::Field) { - return Err(Error::Parse( - PathBuf::from(path), - offset_to_line(&text, conf_offset + conf.len()), - "incomplete field".into(), - )); - } - fields.push(ClippyConf { - name, - attrs: &conf[attrs_start..attrs_end], - lints, - field: conf[field_start..].trim_end(), - }); - - for field in &mut fields { - field.lints.sort_unstable(); - } - fields.sort_by_key(|x| x.name); - - let new_text = format!( - "{pre}define_Conf! {{\n{}}}\n{post}", - fields.iter().format_with("", |field, f| { - if field.lints.is_empty() { - f(&format_args!(" {}\n {}\n", field.attrs, field.field)) - } else if field.lints.iter().map(|x| x.len() + 2).sum::() < 120 - 14 { - f(&format_args!( - " {}\n #[lints({})]\n {}\n", - field.attrs, - field.lints.iter().join(", "), - field.field, - )) - } else { - f(&format_args!( - " {}\n #[lints({}\n )]\n {}\n", - field.attrs, - field - .lints - .iter() - .format_with("", |x, f| f(&format_args!("\n {x},"))), - field.field, - )) - } - }) - ); - - if text != new_text { - if check { - return Err(Error::CheckFailed); - } - fs::write(path, new_text)?; - } - Ok(()) -} - /// Format the symbols list fn fmt_syms(update_mode: UpdateMode) { FileUpdater::default().update_file_checked( @@ -329,21 +107,17 @@ fn run_rustfmt(update_mode: UpdateMode) { // the "main" function of cargo dev fmt pub fn run(update_mode: UpdateMode) { fmt_syms(update_mode); - if let Err(e) = fmt_conf(update_mode.is_check()) { - e.display(); - process::exit(1); - } - new_parse_cx(|cx| { - let mut data = cx.parse_lint_decls(); + let mut lint_data = cx.parse_lint_decls(); + let mut conf_data = cx.parse_conf_mac(); cx.dcx.exit_on_err(); let mut updater = FileUpdater::default(); #[expect(clippy::mutable_key_type)] - let mut lints = data.mk_file_to_lint_decl_map(); + let mut lints = lint_data.mk_file_to_lint_decl_map(); let mut ranges = VecBuf::with_capacity(256); - for passes in data.iter_passes_by_file_mut() { + for passes in lint_data.iter_passes_by_file_mut() { let file = passes[0].decl_sp.file; let mut lints = lints.remove(file); let lints = lints.as_deref_mut().unwrap_or_default(); @@ -358,6 +132,18 @@ pub fn run(update_mode: UpdateMode) { UpdateStatus::from_changed(src != dst) }); } + + updater.update_loaded_file_checked( + "cargo dev fmt", + update_mode, + conf_data.decl_sp.file, + &mut |_, src, dst| { + dst.push_str(&src[..conf_data.decl_sp.range.start as usize]); + conf_data.gen_mac(src, dst); + dst.push_str(&src[conf_data.decl_sp.range.end as usize..]); + UpdateStatus::from_changed(src != dst) + }, + ); }); run_rustfmt(update_mode); diff --git a/clippy_dev/src/generate.rs b/clippy_dev/src/generate.rs index 7fb6c848e874..3f42fcfbc4ae 100644 --- a/clippy_dev/src/generate.rs +++ b/clippy_dev/src/generate.rs @@ -1,5 +1,5 @@ use crate::parse::cursor::Cursor; -use crate::parse::{LintData, LintPass, ParsedLints}; +use crate::parse::{ConfDef, LintData, LintPass, ParsedLints}; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, VecBuf, slice_groups, update_text_region_fn}; use core::range::Range; use itertools::Itertools; @@ -218,6 +218,35 @@ impl LintPass<'_> { } } +impl ConfDef<'_> { + pub fn gen_mac(&mut self, src: &str, dst: &mut String) { + self.opts.sort_unstable_by_key(|o| o.name); + dst.push_str("define_Conf! {"); + for opt in &mut self.opts { + let pre_lints = src[opt.decl_range.start as usize..opt.lints_range.start as usize].trim_end(); + if !pre_lints.is_empty() { + dst.push_str("\n "); + dst.push_str(pre_lints); + } + let pre_name_text = if opt.lints.is_empty() { + "\n " + } else { + opt.lints.sort_unstable(); + dst.push_str("\n #[lints("); + let fmt = write_list(opt.lints.iter().copied(), 80 - 14, " ", dst); + match fmt { + ListFmt::SingleLine => ")]\n ", + ListFmt::MultiLine => "\n )]\n ", + } + }; + dst.push_str(pre_name_text); + dst.push_str(src[opt.lints_range.end as usize..opt.decl_range.end as usize].trim()); + dst.push(','); + } + dst.push_str("\n}"); + } +} + fn write_comment_lines(s: &str, prefix: &str, dst: &mut String) -> bool { let mut has_doc = false; for line in s.split('\n') { diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index e589485dc8f4..7ba3beec2328 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -173,7 +173,100 @@ impl<'cx> ParsedLints<'cx> { } } +pub struct ConfOpt<'cx> { + pub name: &'cx str, + pub decl_range: Range, + pub lints: &'cx mut [&'cx str], + pub lints_range: Range, +} + +pub struct ConfDef<'cx> { + pub decl_sp: Span<'cx>, + pub opts: Vec>, +} + impl<'cx> ParseCxImpl<'cx> { + pub fn parse_conf_mac(&mut self) -> ConfDef<'cx> { + #[allow(clippy::enum_glob_use)] + use cursor::Pat::*; + + let file = &*self.source_files.alloc(SourceFile::load(self.str_buf.alloc_collect( + self.arena, + [ + "clippy_config", + path::MAIN_SEPARATOR_STR, + "src", + path::MAIN_SEPARATOR_STR, + "conf.rs", + ], + ))); + + let mut data = ConfDef { + decl_sp: Span::new(file, 0..0), + opts: Vec::with_capacity(100), + }; + let mut cursor = Cursor::new(&file.contents); + let mut captures = [Capture::EMPTY; 1]; + + if let Err(expected) = cursor + .find_mac_call("define_Conf") + .ok_or("`define_Conf!`") + .and_then(|name| { + data.decl_sp.range.start = name.pos; + cursor.eat_open_brace().ok_or("`{`") + }) + .and_then(|()| { + cursor.eat_list(|cursor| { + let docs = cursor.capture_doc_lines(); + let mut lints: &mut [_] = &mut []; + let mut lints_range = None; + let mut started = docs.len != 0; + while let Some((attr_start, name)) = cursor.capture_opt_attr_start()? { + started = true; + if cursor.get_text(name) == "lints" { + cursor + .eat_open_paren() + .ok_or("`(`") + .and_then(|()| { + cursor.capture_list(&mut self.str_list_buf, self.arena, |cursor| { + Ok(cursor.capture_ident().map(|x| cursor.get_text(x))) + }) + }) + .and_then(|res| { + lints = res; + cursor.match_all(&[CloseParen, CloseBracket], &mut []) + })?; + lints_range = Some(attr_start..cursor.pos()); + } else { + cursor.find_close_bracket().ok_or("`]`")?; + } + } + match cursor.opt_match_all(&[CaptureIdent, Colon], &mut captures) { + Ok(true) => {}, + Ok(false) if started => return Err("an identifier"), + Ok(false) => return Ok(false), + Err(e) => return Err(e), + } + cursor.find_eq().ok_or("`=`")?; + cursor.eat_list_item(); + data.opts.push(ConfOpt { + name: cursor.get_text(captures[0]), + decl_range: docs.pos..cursor.pos(), + lints, + lints_range: lints_range.unwrap_or(captures[0].pos..captures[0].pos), + }); + Ok(true) + }) + }) + .and_then(|()| cursor.eat_close_brace().ok_or("`}`")) + { + cursor.emit_unexpected(&mut self.dcx, file, expected); + } + + data.decl_sp.range.end = cursor.pos(); + data + } + /// Finds and parses all lint declarations. pub fn parse_lint_decls(&mut self) -> ParsedLints<'cx> { let mut data = ParsedLints { diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 2ee007615a67..5b27ef97da02 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -2,7 +2,7 @@ use crate::utils::{StrBuf, VecBuf}; use crate::{DiagCx, SourceFile, Span}; use core::{ptr, slice}; use rustc_arena::DroplessArena; -use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; +use rustc_lexer::{self as lex, DocStyle, LiteralKind, Token, TokenKind}; /// A token pattern used for searching and matching by the [`Cursor`]. /// @@ -23,6 +23,7 @@ pub enum Pat { Bang, CloseBracket, CloseParen, + Colon, Comma, DoubleColon, Eq, @@ -49,6 +50,7 @@ impl Pat { Self::Bang => "`!`", Self::CloseBracket => "`]`", Self::CloseParen => "`)`", + Self::Colon => "`:`", Self::Comma => "`,`", Self::DoubleColon => "`::`", Self::Eq => "`=`", @@ -212,6 +214,7 @@ impl<'txt> Cursor<'txt> { | (Pat::Bang, TokenKind::Bang) | (Pat::CloseBracket, TokenKind::CloseBracket) | (Pat::CloseParen, TokenKind::CloseParen) + | (Pat::Colon, TokenKind::Colon) | (Pat::Comma, TokenKind::Comma) | (Pat::Eq, TokenKind::Eq) | (Pat::Lifetime, TokenKind::Lifetime { .. }) @@ -257,17 +260,14 @@ impl<'txt> Cursor<'txt> { return true; }, - (Pat::CaptureDocLines, TokenKind::LineComment { doc_style: Some(_) }) => { + ( + Pat::CaptureDocLines, + TokenKind::LineComment { + doc_style: Some(DocStyle::Outer), + }, + ) => { let pos = self.pos; - loop { - self.step(); - if !matches!( - self.next_token.kind, - TokenKind::Whitespace | TokenKind::LineComment { doc_style: Some(_) } - ) { - break; - } - } + self.eat_doc_lines(); *captures.next().unwrap() = Capture { pos, len: self.pos - pos, @@ -327,6 +327,18 @@ impl<'txt> Cursor<'txt> { } } + /// Finds the next call to a macro with the specified name and returns it's captured + /// name. + #[must_use] + pub fn find_mac_call(&mut self, name: &str) -> Option { + while let Some(mac) = self.find_capture_ident() { + if self.eat_bang() && self.get_text(mac) == name { + return Some(mac); + } + } + None + } + /// Consumes and captures the text of a path without any internal whitespace. Returns /// `Err` if the path ends with `::`, and `None` if no path component exists at the /// current position. @@ -406,6 +418,74 @@ impl<'txt> Cursor<'txt> { }) } + /// Consumes all doc line comments until another non-whitespace token is found. + pub fn eat_doc_lines(&mut self) { + while matches!( + self.next_token.kind, + TokenKind::Whitespace + | TokenKind::LineComment { + doc_style: Some(DocStyle::Outer) + } + ) { + self.step(); + } + } + + /// Consumes and captures all doc line comments until another non-whitespace token is + /// found. + pub fn capture_doc_lines(&mut self) -> Capture { + loop { + match self.next_token.kind { + TokenKind::Whitespace => self.step(), + TokenKind::LineComment { + doc_style: Some(DocStyle::Outer), + } => { + let pos = self.pos; + self.step(); + self.eat_doc_lines(); + return Capture { + pos, + len: self.pos - pos, + }; + }, + _ => return Capture { pos: self.pos, len: 0 }, + } + } + } + + /// Consumes and captures the next outer attribute. Returns `Err` is the attribute + /// could not be parsed and `None` if the next token does not start an attribute. + pub fn capture_opt_attr_start(&mut self) -> Result, &'static str> { + if !self.eat_pound() { + return Ok(None); + } + let start = self.pos - 1; + self.eat_open_bracket() + .ok_or("`[`") + .and_then(|()| self.capture_ident().ok_or("an identifier")) + .map(|name| Some((start, name))) + } + + /// Consumes everything until the end of a list item indicated by either an unwrapped + /// comma or an unmatched closing delimiter. + pub fn eat_list_item(&mut self) { + let mut depth: u32 = 0; + loop { + match self.next_token.kind { + TokenKind::OpenBrace | TokenKind::OpenBracket | TokenKind::OpenParen => depth += 1, + TokenKind::CloseBrace | TokenKind::CloseBracket | TokenKind::CloseParen if depth > 0 => depth -= 1, + TokenKind::Comma if depth > 0 => {}, + TokenKind::Eof + | TokenKind::Comma + | TokenKind::CloseBrace + | TokenKind::CloseBracket + | TokenKind::CloseParen => break, + _ => {}, + } + self.step(); + } + } + /// Attempts to match a sequence of patterns at the current position. Returns whether /// all patterns were successfully matched. /// @@ -500,8 +580,18 @@ mk_tk_methods! { double_colon(&mut self) { TokenKind::Colon if self.inner.as_str().starts_with(':') => { self.step(); } } + ["`=`"] + eq(&mut self) { TokenKind::Eq } ["the specified identifier"] ident(&mut self, s: &str) { TokenKind::Ident if self.peek_text() == s } + ["`{`"] + open_brace(&mut self) { TokenKind::OpenBrace } + ["`[`"] + open_bracket(&mut self) { TokenKind::OpenBracket } + ["`(`"] + open_paren(&mut self) { TokenKind::OpenParen } + ["`#`"] + pound(&mut self) { TokenKind::Pound } ["`;`"] semi(&mut self) { TokenKind::Semi } } From 7bd0f6134659c9e80260e295b4d5d7f3e7207bc7 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Mon, 20 Oct 2025 17:17:39 -0400 Subject: [PATCH 09/11] `clippy_dev`: Panic immediately instead of returning errors in `add_lint`. --- clippy_dev/src/main.rs | 8 +-- clippy_dev/src/new_lint.rs | 133 +++++++++++-------------------------- clippy_dev/src/utils.rs | 47 ++++++++++--- 3 files changed, 80 insertions(+), 108 deletions(-) diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs index 9e0f2fa14cb0..7f8543e9f2df 100644 --- a/clippy_dev/src/main.rs +++ b/clippy_dev/src/main.rs @@ -37,13 +37,13 @@ fn main() { category, r#type, msrv, - } => match new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv) { - Ok(()) => new_parse_cx(|cx| { + } => { + new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv); + new_parse_cx(|cx| { let data = cx.parse_lint_decls(); cx.dcx.exit_on_err(); data.gen_decls(UpdateMode::Change); - }), - Err(e) => eprintln!("Unable to create lint: {e}"), + }); }, DevCommand::Setup(SetupCommand { subcommand }) => match subcommand { SetupSubcommand::Intellij { remove, repo_path } => { diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index 804f558012f3..f2d57e42d451 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -1,10 +1,9 @@ use crate::parse::cursor::{self, Capture, Cursor}; -use crate::utils::Version; +use crate::utils::{File, Version, create_new_dir}; use clap::ValueEnum; use indoc::{formatdoc, writedoc}; use std::fmt::{self, Write as _}; -use std::fs::{self, OpenOptions}; -use std::io::{self, Write as _}; +use std::fs::OpenOptions; use std::path::{Path, PathBuf}; #[derive(Clone, Copy, PartialEq, ValueEnum)] @@ -30,35 +29,12 @@ struct LintData<'a> { ty: Option<&'a str>, } -trait Context { - fn context>(self, text: C) -> Self; -} - -impl Context for io::Result { - fn context>(self, text: C) -> Self { - match self { - Ok(t) => Ok(t), - Err(e) => { - let message = format!("{}: {e}", text.as_ref()); - Err(io::Error::other(message)) - }, - } - } -} - /// Creates the files required to implement and test a new lint and runs `update_lints`. /// /// # Errors /// /// This function errors out if the files couldn't be created or written to. -pub fn create( - clippy_version: Version, - pass: Pass, - name: &str, - category: &str, - mut ty: Option<&str>, - msrv: bool, -) -> io::Result<()> { +pub fn create(clippy_version: Version, pass: Pass, name: &str, category: &str, mut ty: Option<&str>, msrv: bool) { if category == "cargo" && ty.is_none() { // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo` ty = Some("cargo"); @@ -72,11 +48,11 @@ pub fn create( ty, }; - create_lint(&lint, msrv).context("Unable to create lint implementation")?; - create_test(&lint, msrv).context("Unable to create a test for the new lint")?; + create_lint(&lint, msrv); + create_test(&lint, msrv); if lint.ty.is_none() { - add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?; + add_lint(&lint, msrv); } if pass == Pass::Early { @@ -86,45 +62,36 @@ pub fn create( an early pass, as they lack many features and utilities" ); } - - Ok(()) } -fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> { +fn create_lint(lint: &LintData<'_>, enable_msrv: bool) { if let Some(ty) = lint.ty { - create_lint_for_ty(lint, enable_msrv, ty) + create_lint_for_ty(lint, enable_msrv, ty); } else { - let lint_contents = get_lint_file_contents(lint, enable_msrv); - let lint_path = format!("clippy_lints/src/{}.rs", lint.name); - write_file(&lint_path, lint_contents.as_bytes())?; - println!("Generated lint file: `{lint_path}`"); - - Ok(()) + let path = format!("clippy_lints/src/{}.rs", lint.name); + File::create_new(&path).write(get_lint_file_contents(lint, enable_msrv)); + println!("Generated lint file: `{path}`"); } } -fn create_test(lint: &LintData<'_>, msrv: bool) -> io::Result<()> { - fn create_project_layout>( - lint_name: &str, - location: P, - case: &str, - hint: &str, - msrv: bool, - ) -> io::Result<()> { +fn create_test(lint: &LintData<'_>, msrv: bool) { + fn create_project_layout>(lint_name: &str, location: P, case: &str, hint: &str, msrv: bool) { let mut path = location.into().join(case); - fs::create_dir(&path)?; - write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?; + create_new_dir(&path); - path.push("src"); - fs::create_dir(&path)?; - write_file(path.join("main.rs"), get_test_file_contents(lint_name, msrv))?; + path.push("Cargo.toml"); + File::create_new(&path).write(get_manifest_contents(lint_name, hint)); + path.pop(); - Ok(()) + path.push("src"); + create_new_dir(&path); + path.push("main.rs"); + File::create_new(&path).write(get_test_file_contents(lint_name, msrv)); } if lint.category == "cargo" { let test_dir = format!("tests/ui-cargo/{}", lint.name); - fs::create_dir(&test_dir)?; + create_new_dir(&test_dir); create_project_layout( lint.name, @@ -132,30 +99,28 @@ fn create_test(lint: &LintData<'_>, msrv: bool) -> io::Result<()> { "fail", "Content that triggers the lint goes here", msrv, - )?; + ); create_project_layout( lint.name, &test_dir, "pass", "This file should not trigger the lint", false, - )?; + ); println!("Generated test directories: `{test_dir}/pass`, `{test_dir}/fail`"); } else { let test_path = format!("tests/ui/{}.rs", lint.name); - let test_contents = get_test_file_contents(lint.name, msrv); - write_file(&test_path, test_contents)?; - + File::create_new(&test_path).write(get_test_file_contents(lint.name, msrv)); println!("Generated test file: `{test_path}`"); } - - Ok(()) } -fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> { +fn add_lint(lint: &LintData<'_>, enable_msrv: bool) { let path = "clippy_lints/src/lib.rs"; - let mut lib_rs = fs::read_to_string(path).context("reading")?; + let mut file = File::open(&path, OpenOptions::new().read(true).write(true)); + let mut lib_rs = String::new(); + file.read_append_to_string(&mut lib_rs); let module_name = lint.name; let camel_name = to_camel_case(lint.name); @@ -183,19 +148,7 @@ fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> { lib_rs.insert_str(comment_start, &new_lint); - fs::write(path, lib_rs).context("writing") -} - -fn write_file, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> { - fn inner(path: &Path, contents: &[u8]) -> io::Result<()> { - OpenOptions::new() - .write(true) - .create_new(true) - .open(path)? - .write_all(contents) - } - - inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display())) + file.replace_contents(lib_rs); } fn to_camel_case(name: &str) -> String { @@ -376,7 +329,7 @@ fn get_lint_declaration(version: Version, name_upper: &str, category: &str) -> S ) } -fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> { +fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) { match ty { "cargo" => assert_eq!( lint.category, "cargo", @@ -401,7 +354,7 @@ fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::R ); let mod_file_path = ty_dir.join("mod.rs"); - let context_import = setup_mod_file(&mod_file_path, lint)?; + let context_import = setup_mod_file(&mod_file_path, lint); let (pass_lifetimes, msrv_ty, msrv_ref, msrv_cx) = match context_import { "LateContext" => ("<'_>", "Msrv", "", "cx, "), _ => ("", "MsrvStack", "&", ""), @@ -444,20 +397,21 @@ fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::R ); } - write_file(lint_file_path.as_path(), lint_file_contents)?; + File::create_new(&lint_file_path).write(lint_file_contents); println!("Generated lint file: `clippy_lints/src/{ty}/{}.rs`", lint.name); println!( "Be sure to add a call to `{}::check` in `clippy_lints/src/{ty}/mod.rs`!", lint.name ); - - Ok(()) } -fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> { +fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> &'static str { let lint_name_upper = lint.name.to_uppercase(); - let mut file_contents = fs::read_to_string(path)?; + let mut file = File::open(path, OpenOptions::new().read(true).write(true)); + let mut file_contents = String::new(); + file.read_append_to_string(&mut file_contents); + assert!( !file_contents.contains(&format!("pub {lint_name_upper},")), "Lint `{}` already defined in `{}`", @@ -513,15 +467,8 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> // Just add the mod declaration at the top, it'll be fixed by rustfmt file_contents.insert_str(0, &format!("mod {};\n", lint.name)); - let mut file = OpenOptions::new() - .write(true) - .truncate(true) - .open(path) - .context(format!("trying to open: `{}`", path.display()))?; - file.write_all(file_contents.as_bytes()) - .context(format!("writing to file: `{}`", path.display()))?; - - Ok(lint_context) + file.replace_contents(file_contents); + lint_context } // Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index 58833be15f59..a8281a99839c 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -107,6 +107,20 @@ impl<'a> File<'a> { Self::open(path, OpenOptions::new().read(true)) } + /// Creates a new file with the specified contents, panicking on failure. + #[track_caller] + pub fn create_new(path: &'a impl AsRef) -> Self { + let path = path.as_ref(); + Self { + inner: expect_action( + OpenOptions::new().create_new(true).write(true).open(path), + ErrAction::Open, + path, + ), + path, + } + } + /// 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 { @@ -122,21 +136,24 @@ impl<'a> File<'a> { /// Writes the entire contents of the specified buffer to the file, panicking on failure. #[track_caller] - pub fn write(&mut self, data: &[u8]) { - expect_action(self.inner.write_all(data), ErrAction::Write, self.path); + pub fn write(&mut self, data: impl AsRef<[u8]>) { + expect_action(self.inner.write_all(data.as_ref()), ErrAction::Write, self.path); } /// Replaces the entire contents of a file. #[track_caller] - pub fn replace_contents(&mut self, data: &[u8]) { - let res = match self.inner.seek(SeekFrom::Start(0)) { - Ok(_) => { - self.write(data); - self.inner.set_len(data.len() as u64) - }, - Err(e) => Err(e), - }; - expect_action(res, ErrAction::Write, self.path); + pub fn replace_contents(&mut self, data: impl AsRef<[u8]>) { + fn f(file: &mut File<'_>, data: &[u8]) { + let res = match file.inner.seek(SeekFrom::Start(0)) { + Ok(_) => { + file.write(data); + file.inner.set_len(data.len() as u64) + }, + Err(e) => Err(e), + }; + expect_action(res, ErrAction::Write, file.path); + } + f(self, data.as_ref()); } } @@ -465,6 +482,14 @@ pub fn update_text_region_fn( move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert) } +/// Creates a new directory, panicking on failure. +/// +/// This will fail if the parent directory does not exist. +#[track_caller] +pub fn create_new_dir(path: impl AsRef) { + expect_action(fs::create_dir(path.as_ref()), ErrAction::Create, path.as_ref()); +} + #[track_caller] pub fn try_rename_file(old_name: impl AsRef, new_name: impl AsRef) -> bool { #[track_caller] From 3ea80558d7c3ea39afda50cb3ded4d32fdeb6e0e Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Thu, 26 Feb 2026 10:51:51 -0500 Subject: [PATCH 10/11] `clippy_dev`: Parse more parts of `declare_clippy_lint` macro calls. --- clippy_dev/src/edit_lints.rs | 20 +-- clippy_dev/src/fmt.rs | 4 +- clippy_dev/src/generate.rs | 57 ++++--- clippy_dev/src/parse.rs | 151 ++++++++++++------ clippy_dev/src/parse/cursor.rs | 35 ++-- clippy_lints/src/functions/mod.rs | 2 - clippy_lints/src/methods/mod.rs | 56 +++---- .../src/needless_parens_on_range_literals.rs | 54 +++---- clippy_lints/src/option_if_let_else.rs | 4 +- 9 files changed, 235 insertions(+), 148 deletions(-) diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index cd1ae61f88e5..5a0256ea8c92 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -1,5 +1,7 @@ use crate::parse::cursor::{self, Capture, Cursor}; -use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, ParsedLints, RenamedLint}; +use crate::parse::{ + ActiveLintData, DeprecatedLintData, Lint, LintData, LintName, ParseCx, ParsedLints, RenamedLintData, +}; use crate::utils::{ ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, @@ -31,10 +33,8 @@ pub fn deprecate<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, name lint.get_mut(), Lint { name_sp: Span::new(data.deprecated_file, 0..0), - data: LintData::Deprecated(DeprecatedLint { - reason, - version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), - }), + version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), + data: LintData::Deprecated(DeprecatedLintData { reason }), }, ); let LintData::Active(prev_lint_data) = prev_lint.data else { @@ -65,9 +65,9 @@ pub fn uplift<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam lint.get_mut(), Lint { name_sp: Span::new(data.deprecated_file, 0..0), - data: LintData::Renamed(RenamedLint { + version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), + data: LintData::Renamed(RenamedLintData { new_name: LintName::new_rustc(new_name), - version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), }), }, ); @@ -117,9 +117,9 @@ pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam lint.get_mut(), Lint { name_sp: Span::new(data.deprecated_file, 0..0), - data: LintData::Renamed(RenamedLint { + version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), + data: LintData::Renamed(RenamedLintData { new_name: LintName::new_clippy(new_name), - version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), }), }, ); @@ -173,7 +173,7 @@ pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_nam fn remove_lint_declaration( name: &str, lint_file: &SourceFile<'_>, - lint_data: &ActiveLint<'_>, + lint_data: &ActiveLintData<'_>, data: &ParsedLints<'_>, updater: &mut FileUpdater, ) -> bool { diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index 77c53b11d03d..376549114776 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -115,9 +115,9 @@ pub fn run(update_mode: UpdateMode) { let mut updater = FileUpdater::default(); #[expect(clippy::mutable_key_type)] - let mut lints = lint_data.mk_file_to_lint_decl_map(); + let mut lints = lint_data.lints.mk_by_file_map(); let mut ranges = VecBuf::with_capacity(256); - for passes in lint_data.iter_passes_by_file_mut() { + for passes in lint_data.lint_passes.iter_by_file_mut() { let file = passes[0].decl_sp.file; let mut lints = lints.remove(file); let lints = lints.as_deref_mut().unwrap_or_default(); diff --git a/clippy_dev/src/generate.rs b/clippy_dev/src/generate.rs index 3f42fcfbc4ae..12cf34a5d4d0 100644 --- a/clippy_dev/src/generate.rs +++ b/clippy_dev/src/generate.rs @@ -1,5 +1,5 @@ use crate::parse::cursor::Cursor; -use crate::parse::{ConfDef, LintData, LintPass, ParsedLints}; +use crate::parse::{ActiveLint, ConfDef, LintData, LintPass, ParsedLints}; use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, VecBuf, slice_groups, update_text_region_fn}; use core::range::Range; use itertools::Itertools; @@ -41,8 +41,8 @@ impl ParsedLints<'_> { for &(name, lint) in &lints { match &lint.data { LintData::Active(_) => active.push((name, lint.name_sp.file.path_as_krate_mod())), - LintData::Deprecated(lint) => deprecated.push((name, lint)), - LintData::Renamed(lint) => renamed.push((name, lint)), + LintData::Deprecated(data) => deprecated.push((name, lint.version, data.reason)), + LintData::Renamed(data) => renamed.push((name, lint.version, data.new_name)), } } active.sort_by_key(|&(_, path)| path); @@ -78,11 +78,10 @@ impl ParsedLints<'_> { ); dst.push_str(&src[..cursor.pos() as usize]); dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n"); - for &(name, data) in &deprecated { + for &(name, version, reason) in &deprecated { write!( dst, - " #[clippy::version = \"{}\"]\n (\"clippy::{name}\", \"{}\"),\n", - data.version, data.reason, + " #[clippy::version = \"{version}\"]\n (\"clippy::{name}\", \"{reason}\"),\n", ) .unwrap(); } @@ -92,11 +91,10 @@ impl ParsedLints<'_> { declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\ ", ); - for &(name, data) in &renamed { + for &(name, version, new_name) in &renamed { write!( dst, - " #[clippy::version = \"{}\"]\n (\"clippy::{name}\", \"{}\"),\n", - data.version, data.new_name, + " #[clippy::version = \"{version}\"]\n (\"clippy::{name}\", \"{new_name}\"),\n", ) .unwrap(); } @@ -110,7 +108,7 @@ impl ParsedLints<'_> { "tests/ui/deprecated.rs", &mut |_, src, dst| { dst.push_str(GENERATED_FILE_COMMENT); - for &(lint, _) in &deprecated { + for &(lint, _, _) in &deprecated { writeln!(dst, "#![warn(clippy::{lint})] //~ ERROR: lint `clippy::{lint}`").unwrap(); } dst.push_str("\nfn main() {}\n"); @@ -125,12 +123,12 @@ impl ParsedLints<'_> { 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(); + for &(_, _, new_name) in &renamed { + if seen_lints.insert(new_name) { + writeln!(dst, "#![allow({new_name})]").unwrap(); } } - for &(lint, _) in &renamed { + for &(lint, _, _) in &renamed { writeln!(dst, "#![warn(clippy::{lint})] //~ ERROR: lint `clippy::{lint}`").unwrap(); } dst.push_str("\nfn main() {}\n"); @@ -188,6 +186,27 @@ impl ParsedLints<'_> { } } +impl ActiveLint<'_, '_> { + pub fn gen_mac(&self, dst: &mut String) { + dst.push_str("declare_clippy_lint! {"); + write_comment_lines(self.data.docs, "\n ", dst); + dst.extend(["\n #[clippy::version = \"", self.version, "\"]\n pub "]); + + // Lint names are stored in lower case, but the declaration needs to be upper case. + let name_pos = dst.len(); + dst.push_str(self.name); + dst[name_pos..].make_ascii_uppercase(); + dst.push(','); + + write_comment_lines(self.data.group_comments, "\n ", dst); + dst.extend(["\n ", self.data.group, ",\n ", self.data.desc]); + if !self.data.opts.is_empty() { + dst.extend([",\n ", self.data.opts]); + } + dst.push_str("\n}"); + } +} + impl LintPass<'_> { pub fn gen_mac(&self, dst: &mut String) { let mut line_start = dst.len(); @@ -288,23 +307,23 @@ fn write_list<'a>( pub fn gen_sorted_lints_file( src: &str, dst: &mut String, - lints: &mut [(&str, Range)], + lints: &mut [ActiveLint<'_, '_>], passes: &mut [LintPass<'_>], ranges: &mut VecBuf>, ) { ranges.with(|ranges| { - ranges.extend(lints.iter().map(|&(_, x)| x)); + ranges.extend(lints.iter().map(|x| x.data.decl_range)); ranges.extend(passes.iter().map(|x| x.decl_sp.range)); ranges.sort_unstable_by_key(|x| x.start); - lints.sort_unstable_by_key(|&(x, _)| x); + lints.sort_unstable_by_key(|x| x.name); passes.sort_by_key(|x| x.name); let mut ranges = ranges.iter(); let pos = if let Some(range) = ranges.next() { dst.push_str(&src[..range.start as usize]); - for &(_, range) in &*lints { - dst.push_str(&src[range.start as usize..range.end as usize]); + for lint in &*lints { + lint.gen_mac(dst); dst.push_str("\n\n"); } for pass in passes { diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 7ba3beec2328..8286ff542101 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -4,6 +4,7 @@ use self::cursor::{Capture, Cursor, IdentPat}; use crate::utils::{ErrAction, Scoped, StrBuf, VecBuf, expect_action, slice_groups_mut, walk_dir_no_dot_or_target}; use crate::{DiagCx, SourceFile, Span}; use core::fmt::{self, Display}; +use core::ops::{Deref, DerefMut}; use core::range::Range; use rustc_arena::{DroplessArena, TypedArena}; use rustc_data_structures::fx::FxHashMap; @@ -78,29 +79,45 @@ impl Display for LintName<'_> { } } -pub struct ActiveLint<'cx> { - pub group: &'cx str, +pub struct ActiveLintData<'cx> { pub decl_range: Range, + /// The raw text of the documentation comments. May include leading/trailing + /// whitespace and empty lines. + pub docs: &'cx str, + /// The raw text of the line comments. May include leading/trailing whitespace + /// and empty lines. + pub group_comments: &'cx str, + pub group: &'cx str, + /// The raw text of the string literal including the quotation marks. + pub desc: &'cx str, + /// The raw text of any additional `@option` values. Starts at the comma after + /// the description and may include trailing whitespace. + pub opts: &'cx str, } -pub struct DeprecatedLint<'cx> { +pub struct DeprecatedLintData<'cx> { pub reason: &'cx str, - pub version: &'cx str, } -pub struct RenamedLint<'cx> { +pub struct RenamedLintData<'cx> { pub new_name: LintName<'cx>, - pub version: &'cx str, } pub enum LintData<'cx> { - Active(ActiveLint<'cx>), - Deprecated(DeprecatedLint<'cx>), - Renamed(RenamedLint<'cx>), + Active(ActiveLintData<'cx>), + Deprecated(DeprecatedLintData<'cx>), + Renamed(RenamedLintData<'cx>), +} + +pub struct ActiveLint<'a, 'cx> { + pub name: &'cx str, + pub version: &'cx str, + pub data: &'a ActiveLintData<'cx>, } pub struct Lint<'cx> { pub name_sp: Span<'cx>, + pub version: &'cx str, pub data: LintData<'cx>, } @@ -129,41 +146,35 @@ pub struct LintPass<'cx> { pub lints: &'cx mut [&'cx str], } -pub struct ParsedLints<'cx> { - pub lints: FxHashMap<&'cx str, Lint<'cx>>, - pub lint_passes: Vec>, - pub deprecated_file: &'cx SourceFile<'cx>, -} -impl<'cx> ParsedLints<'cx> { +pub struct LintMap<'cx>(FxHashMap<&'cx str, Lint<'cx>>); +impl<'cx> LintMap<'cx> { #[expect(clippy::mutable_key_type)] - pub fn mk_file_to_lint_decl_map(&self) -> FxHashMap<&'cx SourceFile<'cx>, Vec<(&'cx str, Range)>> { + pub fn mk_by_file_map<'s>(&'s self) -> FxHashMap<&'cx SourceFile<'cx>, Vec>> { #[expect(clippy::default_trait_access)] let mut lints = FxHashMap::with_capacity_and_hasher(500, Default::default()); - for (&name, lint) in &self.lints { + for (&name, lint) in &self.0 { if let LintData::Active(lint_data) = &lint.data { lints .entry(lint.name_sp.file) .or_insert_with(|| Vec::with_capacity(8)) - .push((name, lint_data.decl_range)); + .push(ActiveLint { + name, + version: lint.version, + data: lint_data, + }); } } lints } - pub fn iter_passes_by_file_mut<'s>(&'s mut self) -> impl Iterator]> { - slice_groups_mut(&mut self.lint_passes, |head, tail| { - tail.iter().take_while(|&x| x.decl_sp.file == head.decl_sp.file).count() - }) - } - #[track_caller] - fn get_vacant_lint<'a>( - &'a mut self, + fn get_vacant_lint<'s>( + &'s mut self, dcx: &mut DiagCx, name: &'cx str, name_sp: Span<'cx>, - ) -> Option>> { - match self.lints.entry(name) { + ) -> Option>> { + match self.0.entry(name) { Entry::Vacant(e) => Some(e), Entry::Occupied(e) => { dcx.emit_duplicate_lint(name_sp, e.get().name_sp); @@ -172,6 +183,38 @@ impl<'cx> ParsedLints<'cx> { } } } +impl<'cx> Deref for LintMap<'cx> { + type Target = FxHashMap<&'cx str, Lint<'cx>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for LintMap<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub struct LintPasses<'cx>(Vec>); +impl<'cx> LintPasses<'cx> { + pub fn iter_by_file_mut<'s>(&'s mut self) -> impl Iterator]> { + slice_groups_mut(&mut self.0, |head, tail| { + tail.iter().take_while(|&x| x.decl_sp.file == head.decl_sp.file).count() + }) + } +} +impl<'cx> Deref for LintPasses<'cx> { + type Target = [LintPass<'cx>]; + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +pub struct ParsedLints<'cx> { + pub lints: LintMap<'cx>, + pub lint_passes: LintPasses<'cx>, + pub deprecated_file: &'cx SourceFile<'cx>, +} pub struct ConfOpt<'cx> { pub name: &'cx str, @@ -271,8 +314,8 @@ impl<'cx> ParseCxImpl<'cx> { pub fn parse_lint_decls(&mut self) -> ParsedLints<'cx> { let mut data = ParsedLints { #[expect(clippy::default_trait_access)] - lints: FxHashMap::with_capacity_and_hasher(1000, Default::default()), - lint_passes: Vec::with_capacity(400), + lints: LintMap(FxHashMap::with_capacity_and_hasher(1000, Default::default())), + lint_passes: LintPasses(Vec::with_capacity(400)), deprecated_file: self.source_files.alloc(SourceFile::load(self.str_buf.alloc_collect( self.arena, [ @@ -325,7 +368,7 @@ impl<'cx> ParseCxImpl<'cx> { use cursor::Pat::*; let mut cursor = Cursor::new(&file.contents); - let mut captures = [Capture::EMPTY; 3]; + let mut captures = [Capture::EMPTY; 6]; while let Some(mac_name) = cursor.find_capture_ident() { if !cursor.eat_bang() { continue; @@ -335,13 +378,13 @@ impl<'cx> ParseCxImpl<'cx> { #[rustfmt::skip] static DECL_START: &[cursor::Pat] = &[ // { /// docs - OpenBrace, AnyComments, + OpenBrace, CaptureDocLines, // #[clippy::version = "version"] Pound, OpenBracket, Ident(IdentPat::clippy), DoubleColon, Ident(IdentPat::version), Eq, CaptureLitStr, CloseBracket, // pub NAME, GROUP, "desc", Ident(IdentPat::r#pub), CaptureIdent, Comma, - AnyComments, CaptureIdent, Comma, AnyComments, LitStr, + CaptureLineComments, CaptureIdent, Comma, CaptureLitStr, ]; #[rustfmt::skip] static OPTION: &[cursor::Pat] = &[ @@ -349,26 +392,38 @@ impl<'cx> ParseCxImpl<'cx> { AnyComments, At, AnyIdent, Eq, Lit, ]; + let mut opts_text = ""; if let Err(expected) = cursor .match_all(DECL_START, &mut captures) .and_then(|()| { - (!cursor.eat_comma()).ok_or(()).or_else(|()| { - cursor.eat_list(|cursor| cursor.match_all(OPTION, &mut []).map(|()| true)) - }) + if cursor.eat_comma() { + let pos = cursor.pos(); + cursor.eat_list(|cursor| cursor.match_all(OPTION, &mut []).map(|()| true))?; + opts_text = file.contents[pos as usize..cursor.pos() as usize].trim(); + } + Ok(()) }) .and_then(|()| cursor.eat_close_brace().ok_or("`}`")) { cursor.emit_unexpected(&mut self.dcx, file, expected); - } else if let name = self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[1])) - && let name_sp = captures[1].mk_sp(file) - && let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp) + } else if let [docs, version, name, group_comments, group, desc] = captures + && let name_sp = name.mk_sp(file) + && let name = self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(name)) + && let (Some(e), Some(version)) = ( + data.lints.get_vacant_lint(&mut self.dcx, name, name_sp), + self.parse_version(cursor.get_text(version), version.mk_sp(file)), + ) { - let _ = self.parse_version(cursor.get_text(captures[0]), captures[0].mk_sp(file)); e.insert(Lint { name_sp, - data: LintData::Active(ActiveLint { - group: cursor.get_text(captures[2]), + version, + data: LintData::Active(ActiveLintData { decl_range: mac_name.pos..cursor.pos(), + docs: cursor.get_text(docs), + group_comments: cursor.get_text(group_comments), + group: cursor.get_text(group), + desc: cursor.get_text(desc), + opts: opts_text, }), }); } @@ -395,7 +450,7 @@ impl<'cx> ParseCxImpl<'cx> { { cursor.emit_unexpected(&mut self.dcx, file, expected); } else { - data.lint_passes.push(LintPass { + data.lint_passes.0.push(LintPass { docs: cursor.get_text(captures[0]), name: cursor.get_text(captures[1]), lt: has_lt.then(|| cursor.get_text(captures[2])), @@ -463,11 +518,12 @@ impl<'cx> ParseCxImpl<'cx> { self.parse_clippy_lint_name(cursor.get_text(name), name_sp), self.parse_str_lit(cursor.get_text(reason), reason.mk_sp(file)), ) - && let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp) + && let Some(e) = data.lints.get_vacant_lint(&mut self.dcx, name, name_sp) { e.insert(Lint { name_sp, - data: LintData::Deprecated(DeprecatedLint { reason, version }), + version, + data: LintData::Deprecated(DeprecatedLintData { reason }), }); } Ok(parsed) @@ -490,11 +546,12 @@ impl<'cx> ParseCxImpl<'cx> { self.parse_clippy_lint_name(cursor.get_text(name), name_sp), self.parse_lint_name(cursor.get_text(new_name), new_name.mk_sp(file)), ) - && let Some(e) = data.get_vacant_lint(&mut self.dcx, name, name_sp) + && let Some(e) = data.lints.get_vacant_lint(&mut self.dcx, name, name_sp) { e.insert(Lint { name_sp, - data: LintData::Renamed(RenamedLint { new_name, version }), + version, + data: LintData::Renamed(RenamedLintData { new_name }), }); } Ok(parsed) diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 5b27ef97da02..6deffe80b026 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -17,6 +17,7 @@ pub enum Pat { CaptureDocLines, CaptureIdent, CaptureLifetime, + CaptureLineComments, CaptureLitStr, AnyIdent, At, @@ -32,7 +33,6 @@ pub enum Pat { Ident(IdentPat), Lifetime, Lit, - LitStr, Lt, OpenBrace, OpenBracket, @@ -45,6 +45,8 @@ impl Pat { match self { Self::AnyComments => "comments", Self::CaptureDocLines => "doc line comments", + Self::CaptureLineComments => "line comments", + Self::CaptureLitStr => "a string literal", Self::AnyIdent | Self::CaptureIdent => "an identifier", Self::At => "`@`", Self::Bang => "`!`", @@ -59,7 +61,6 @@ impl Pat { Self::Ident(x) => x.desc(), Self::Lifetime | Self::CaptureLifetime => "a lifetime", Self::Lit => "a literal", - Self::LitStr | Self::CaptureLitStr => "a string literal", Self::Lt => "`<`", Self::OpenBrace => "`{`", Self::OpenBracket => "`[`", @@ -225,14 +226,7 @@ impl<'txt> Cursor<'txt> { | (Pat::OpenBracket, TokenKind::OpenBracket) | (Pat::OpenParen, TokenKind::OpenParen) | (Pat::Pound, TokenKind::Pound) - | (Pat::Semi, TokenKind::Semi) - | ( - Pat::LitStr, - TokenKind::Literal { - kind: LiteralKind::Str { terminated: true } | LiteralKind::RawStr { .. }, - .. - }, - ) => break, + | (Pat::Semi, TokenKind::Semi) => break, (Pat::DoubleColon, TokenKind::Colon) if self.inner.as_str().starts_with(':') => { self.step(); @@ -260,6 +254,15 @@ impl<'txt> Cursor<'txt> { return true; }, + (Pat::CaptureLineComments, TokenKind::LineComment { doc_style: None }) => { + let pos = self.pos; + self.eat_line_comments(); + *captures.next().unwrap() = Capture { + pos, + len: self.pos - pos, + }; + return true; + }, ( Pat::CaptureDocLines, TokenKind::LineComment { @@ -274,7 +277,7 @@ impl<'txt> Cursor<'txt> { }; return true; }, - (Pat::CaptureDocLines, _) => { + (Pat::CaptureDocLines | Pat::CaptureLineComments, _) => { *captures.next().unwrap() = Capture::EMPTY; return true; }, @@ -453,6 +456,16 @@ impl<'txt> Cursor<'txt> { } } + /// Consumes all line comments until another non-whitespace token is found. + pub fn eat_line_comments(&mut self) { + while matches!( + self.next_token.kind, + TokenKind::Whitespace | TokenKind::LineComment { doc_style: None } + ) { + self.step(); + } + } + /// Consumes and captures the next outer attribute. Returns `Err` is the attribute /// could not be parsed and `None` if the next token does not start an attribute. pub fn capture_opt_attr_start(&mut self) -> Result, &'static str> { diff --git a/clippy_lints/src/functions/mod.rs b/clippy_lints/src/functions/mod.rs index 7c5fd760a7da..6eb9e03712fa 100644 --- a/clippy_lints/src/functions/mod.rs +++ b/clippy_lints/src/functions/mod.rs @@ -105,7 +105,6 @@ declare_clippy_lint! { /// It is most likely that such a method is a bug caused by a typo or by copy-pasting. /// /// ### Example - /// ```no_run /// struct A { /// a: String, @@ -117,7 +116,6 @@ declare_clippy_lint! { /// &self.b /// } /// } - /// ``` /// Use instead: /// ```no_run diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs index 38a3b1705ad1..23e175472a06 100644 --- a/clippy_lints/src/methods/mod.rs +++ b/clippy_lints/src/methods/mod.rs @@ -1855,7 +1855,7 @@ declare_clippy_lint! { /// Redundant code in the `filter` and `map` operations is poor style and /// less performant. /// - /// ### Example + /// ### Example /// ```no_run /// (0_i32..10) /// .filter(|n| n.checked_add(1).is_some()) @@ -1881,7 +1881,7 @@ declare_clippy_lint! { /// Redundant code in the `find` and `map` operations is poor style and /// less performant. /// - /// ### Example + /// ### Example /// ```no_run /// (0_i32..10) /// .find(|n| n.checked_add(1).is_some()) @@ -2577,28 +2577,28 @@ declare_clippy_lint! { } declare_clippy_lint! { - /// ### What it does - /// It detects useless calls to `str::as_bytes()` before calling `len()` or `is_empty()`. - /// - /// ### Why is this bad? - /// The `len()` and `is_empty()` methods are also directly available on strings, and they - /// return identical results. In particular, `len()` on a string returns the number of - /// bytes. - /// - /// ### Example - /// ``` - /// let len = "some string".as_bytes().len(); - /// let b = "some string".as_bytes().is_empty(); - /// ``` - /// Use instead: - /// ``` - /// let len = "some string".len(); - /// let b = "some string".is_empty(); - /// ``` - #[clippy::version = "1.84.0"] - pub NEEDLESS_AS_BYTES, - complexity, - "detect useless calls to `as_bytes()`" + /// ### What it does + /// It detects useless calls to `str::as_bytes()` before calling `len()` or `is_empty()`. + /// + /// ### Why is this bad? + /// The `len()` and `is_empty()` methods are also directly available on strings, and they + /// return identical results. In particular, `len()` on a string returns the number of + /// bytes. + /// + /// ### Example + /// ``` + /// let len = "some string".as_bytes().len(); + /// let b = "some string".as_bytes().is_empty(); + /// ``` + /// Use instead: + /// ``` + /// let len = "some string".len(); + /// let b = "some string".is_empty(); + /// ``` + #[clippy::version = "1.84.0"] + pub NEEDLESS_AS_BYTES, + complexity, + "detect useless calls to `as_bytes()`" } declare_clippy_lint! { @@ -3658,10 +3658,10 @@ declare_clippy_lint! { /// let s = "Lorem ipsum"; /// &s.as_bytes()[1..5]; /// ``` - #[clippy::version = "1.86.0"] - pub SLICED_STRING_AS_BYTES, - perf, - "slicing a string and immediately calling as_bytes is less efficient and can lead to panics" + #[clippy::version = "1.86.0"] + pub SLICED_STRING_AS_BYTES, + perf, + "slicing a string and immediately calling as_bytes is less efficient and can lead to panics" } declare_clippy_lint! { diff --git a/clippy_lints/src/needless_parens_on_range_literals.rs b/clippy_lints/src/needless_parens_on_range_literals.rs index 0c23bd6b1f96..d07ab7ea0845 100644 --- a/clippy_lints/src/needless_parens_on_range_literals.rs +++ b/clippy_lints/src/needless_parens_on_range_literals.rs @@ -9,33 +9,33 @@ use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; declare_clippy_lint! { - /// ### What it does - /// The lint checks for parenthesis on literals in range statements that are - /// superfluous. - /// - /// ### Why is this bad? - /// Having superfluous parenthesis makes the code less readable - /// overhead when reading. - /// - /// ### Example - /// - /// ```no_run - /// for i in (0)..10 { - /// println!("{i}"); - /// } - /// ``` - /// - /// Use instead: - /// - /// ```no_run - /// for i in 0..10 { - /// println!("{i}"); - /// } - /// ``` - #[clippy::version = "1.63.0"] - pub NEEDLESS_PARENS_ON_RANGE_LITERALS, - style, - "needless parenthesis on range literals can be removed" + /// ### What it does + /// The lint checks for parenthesis on literals in range statements that are + /// superfluous. + /// + /// ### Why is this bad? + /// Having superfluous parenthesis makes the code less readable + /// overhead when reading. + /// + /// ### Example + /// + /// ```no_run + /// for i in (0)..10 { + /// println!("{i}"); + /// } + /// ``` + /// + /// Use instead: + /// + /// ```no_run + /// for i in 0..10 { + /// println!("{i}"); + /// } + /// ``` + #[clippy::version = "1.63.0"] + pub NEEDLESS_PARENS_ON_RANGE_LITERALS, + style, + "needless parenthesis on range literals can be removed" } declare_lint_pass!(NeedlessParensOnRangeLiterals => [ diff --git a/clippy_lints/src/option_if_let_else.rs b/clippy_lints/src/option_if_let_else.rs index 85cf483fce90..9606599e5510 100644 --- a/clippy_lints/src/option_if_let_else.rs +++ b/clippy_lints/src/option_if_let_else.rs @@ -72,10 +72,10 @@ declare_clippy_lint! { /// y*y /// }, |foo| foo); /// ``` - // FIXME: Before moving this lint out of nursery, the lint name needs to be updated. It now also - // covers matches and `Result`. #[clippy::version = "1.47.0"] pub OPTION_IF_LET_ELSE, + // FIXME: Before moving this lint out of nursery, the lint name needs to be updated. It now also + // covers matches and `Result`. nursery, "reimplementation of Option::map_or" } From b8fe775ba6979aad25cc31fff5bc7269213f3f53 Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Wed, 29 Oct 2025 22:27:01 -0400 Subject: [PATCH 11/11] `clippy_dev`: Use the shared parsing logic in `new_lint`. --- clippy_dev/Cargo.toml | 1 - clippy_dev/src/edit_lints.rs | 3 +- clippy_dev/src/fmt.rs | 9 +- clippy_dev/src/generate.rs | 39 +- clippy_dev/src/main.rs | 17 +- clippy_dev/src/new_lint.rs | 850 +++++++++++++++------------------ clippy_dev/src/parse.rs | 106 +++- clippy_dev/src/parse/cursor.rs | 1 + clippy_dev/src/utils.rs | 110 ++++- 9 files changed, 598 insertions(+), 538 deletions(-) diff --git a/clippy_dev/Cargo.toml b/clippy_dev/Cargo.toml index 469fe6b8a2bb..b0310e261848 100644 --- a/clippy_dev/Cargo.toml +++ b/clippy_dev/Cargo.toml @@ -9,7 +9,6 @@ annotate-snippets = { version = "0.12.10", features = ["simd"] } anstream = "0.6.20" 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.6" opener = "0.8" diff --git a/clippy_dev/src/edit_lints.rs b/clippy_dev/src/edit_lints.rs index 5a0256ea8c92..01d35df95ca4 100644 --- a/clippy_dev/src/edit_lints.rs +++ b/clippy_dev/src/edit_lints.rs @@ -180,7 +180,7 @@ fn remove_lint_declaration( let delete_mod = if data.lints.iter().all(|(_, l)| l.name_sp.file != lint_file) { delete_file_if_exists(lint_file.path.get()) } else { - updater.update_file(lint_file.path.get(), &mut |_, src, dst| -> UpdateStatus { + updater.change_file(lint_file.path.get(), |src, dst| { let mut start = &src[..lint_data.decl_range.start as usize]; if start.ends_with("\n\n") { start = &start[..start.len() - 1]; @@ -191,7 +191,6 @@ fn remove_lint_declaration( } dst.push_str(start); dst.push_str(end); - UpdateStatus::Changed }); false }; diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs index 376549114776..331480407d0a 100644 --- a/clippy_dev/src/fmt.rs +++ b/clippy_dev/src/fmt.rs @@ -113,6 +113,7 @@ pub fn run(update_mode: UpdateMode) { cx.dcx.exit_on_err(); let mut updater = FileUpdater::default(); + let copy: &mut dyn FnMut(&str, &mut String) = &mut |src, dst| dst.push_str(src); #[expect(clippy::mutable_key_type)] let mut lints = lint_data.lints.mk_by_file_map(); @@ -122,13 +123,13 @@ pub fn run(update_mode: UpdateMode) { let mut lints = lints.remove(file); let lints = lints.as_deref_mut().unwrap_or_default(); updater.update_loaded_file_checked("cargo dev fmt", update_mode, file, &mut |_, src, dst| { - gen_sorted_lints_file(src, dst, lints, passes, &mut ranges); + gen_sorted_lints_file(src, dst, lints, passes, &mut ranges, copy); UpdateStatus::from_changed(src != dst) }); } for (&file, lints) in &mut lints { updater.update_loaded_file_checked("cargo dev fmt", update_mode, file, &mut |_, src, dst| { - gen_sorted_lints_file(src, dst, lints, &mut [], &mut ranges); + gen_sorted_lints_file(src, dst, lints, &mut [], &mut ranges, copy); UpdateStatus::from_changed(src != dst) }); } @@ -138,9 +139,7 @@ pub fn run(update_mode: UpdateMode) { update_mode, conf_data.decl_sp.file, &mut |_, src, dst| { - dst.push_str(&src[..conf_data.decl_sp.range.start as usize]); - conf_data.gen_mac(src, dst); - dst.push_str(&src[conf_data.decl_sp.range.end as usize..]); + conf_data.gen_file(src, dst); UpdateStatus::from_changed(src != dst) }, ); diff --git a/clippy_dev/src/generate.rs b/clippy_dev/src/generate.rs index 12cf34a5d4d0..47fa87af6be8 100644 --- a/clippy_dev/src/generate.rs +++ b/clippy_dev/src/generate.rs @@ -238,6 +238,12 @@ impl LintPass<'_> { } impl ConfDef<'_> { + pub fn gen_file(&mut self, src: &str, dst: &mut String) { + dst.push_str(&src[..self.decl_sp.range.start as usize]); + self.gen_mac(src, dst); + dst.push_str(&src[self.decl_sp.range.end as usize..]); + } + pub fn gen_mac(&mut self, src: &str, dst: &mut String) { self.opts.sort_unstable_by_key(|o| o.name); dst.push_str("define_Conf! {"); @@ -310,6 +316,7 @@ pub fn gen_sorted_lints_file( lints: &mut [ActiveLint<'_, '_>], passes: &mut [LintPass<'_>], ranges: &mut VecBuf>, + copy_src: &mut dyn FnMut(&str, &mut String), ) { ranges.with(|ranges| { ranges.extend(lints.iter().map(|x| x.data.decl_range)); @@ -321,7 +328,7 @@ pub fn gen_sorted_lints_file( let mut ranges = ranges.iter(); let pos = if let Some(range) = ranges.next() { - dst.push_str(&src[..range.start as usize]); + copy_src(&src[..range.start as usize], dst); for lint in &*lints { lint.gen_mac(dst); dst.push_str("\n\n"); @@ -333,29 +340,31 @@ pub fn gen_sorted_lints_file( } range.end } else { - dst.push_str(src); + copy_src(src, dst); return; }; let pos = ranges.fold(pos, |start, range| { let s = &src[start as usize..range.start as usize]; - dst.push_str(if s.trim_start().is_empty() { - // Only whitespace between this and the previous item. No need to keep that. - "" - } else if src[..pos as usize].ends_with("\n\n") - && let Some(s) = s.strip_prefix("\n\n") - { - // Empty line before and after. Remove one of them. - s - } else { - // Remove only full lines unless something is in the way. - s.strip_prefix('\n').unwrap_or(s) - }); + // Don't keep whitespace between declarations. + if !s.trim_start().is_empty() { + let s = if src[..pos as usize].ends_with("\n\n") + && let Some(s) = s.strip_prefix("\n\n") + { + // Empty line before and after. Remove one of them. + s + } else { + // Remove the line end immediately proceeding a declaration. + s.strip_prefix('\n').unwrap_or(s) + }; + copy_src(s, dst); + } range.end }); // Since we always generate an empty line at the end, make sure to always skip it. let s = &src[pos as usize..]; - dst.push_str(s.strip_prefix('\n').map_or(s, |s| s.strip_prefix('\n').unwrap_or(s))); + let s = s.strip_prefix('\n').map_or(s, |s| s.strip_prefix('\n').unwrap_or(s)); + copy_src(s, dst); }); } diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs index 7f8543e9f2df..d96483763919 100644 --- a/clippy_dev/src/main.rs +++ b/clippy_dev/src/main.rs @@ -35,16 +35,8 @@ fn main() { pass, name, category, - r#type, msrv, - } => { - new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv); - new_parse_cx(|cx| { - let data = cx.parse_lint_decls(); - cx.dcx.exit_on_err(); - data.gen_decls(UpdateMode::Change); - }); - }, + } => new_lint::create(clippy.version, &pass, &name, &category, msrv), DevCommand::Setup(SetupCommand { subcommand }) => match subcommand { SetupSubcommand::Intellij { remove, repo_path } => { if remove { @@ -162,9 +154,9 @@ enum DevCommand { #[command(name = "new_lint")] /// Create a new lint and run `cargo dev update_lints` NewLint { - #[arg(short, long, conflicts_with = "type", default_value = "late")] + #[arg(short, long, default_value = "late")] /// Specify whether the lint runs during the early or late pass - pass: new_lint::Pass, + pass: String, #[arg( short, long, @@ -191,9 +183,6 @@ enum DevCommand { /// What category the lint belongs to category: String, #[arg(long)] - /// What directory the lint belongs in - r#type: Option, - #[arg(long)] /// Add MSRV config code to the lint msrv: bool, }, diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs index f2d57e42d451..f861e3d1aba8 100644 --- a/clippy_dev/src/new_lint.rs +++ b/clippy_dev/src/new_lint.rs @@ -1,524 +1,430 @@ -use crate::parse::cursor::{self, Capture, Cursor}; -use crate::utils::{File, Version, create_new_dir}; -use clap::ValueEnum; -use indoc::{formatdoc, writedoc}; -use std::fmt::{self, Write as _}; -use std::fs::OpenOptions; -use std::path::{Path, PathBuf}; - -#[derive(Clone, Copy, PartialEq, ValueEnum)] -pub enum Pass { +use crate::generate::gen_sorted_lints_file; +use crate::parse::cursor::Cursor; +use crate::parse::{ActiveLint, ActiveLintData, Lint, LintData, LintPass, LintPassMac}; +use crate::utils::{FileUpdater, VecBuf, Version, create_new_dir}; +use crate::{SourceFile, Span, UpdateMode, new_parse_cx}; +use std::collections::hash_map::Entry; +use std::fmt::Write as _; +use std::path::{self, MAIN_SEPARATOR_STR as PATH_SEP, PathBuf}; + +#[derive(Clone, Copy)] +enum LintPassKind { Early, Late, } -impl fmt::Display for Pass { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Pass::Early => "early", - Pass::Late => "late", - }) - } -} - -struct LintData<'a> { - clippy_version: Version, - pass: Pass, - name: &'a str, - category: &'a str, - ty: Option<&'a str>, -} - /// Creates the files required to implement and test a new lint and runs `update_lints`. /// /// # Errors /// /// This function errors out if the files couldn't be created or written to. -pub fn create(clippy_version: Version, pass: Pass, name: &str, category: &str, mut ty: Option<&str>, msrv: bool) { - if category == "cargo" && ty.is_none() { - // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo` - ty = Some("cargo"); - } +#[expect(clippy::too_many_lines)] +pub fn create(clippy_version: Version, pass: &str, name: &str, group: &str, has_msrv: bool) { + new_parse_cx(|cx| { + let cx = &mut **cx; + let mut data = cx.parse_lint_decls(); + let conf_data = has_msrv.then(|| cx.parse_conf_mac()); + match (pass, group) { + ("cargo", "cargo") => {}, + ("cargo", _) => cx + .dcx + .emit_spanless_err("a lint added to the `cargo` pass must be part of the `cargo` group"), + (_, "cargo") => cx + .dcx + .emit_spanless_err("a lint added to the `cargo` group must be part of the `cargo` pass"), + _ => {}, + } + let (pass_idx, new_pass) = match pass { + "early" => (None, LintPassKind::Early), + "late" => (None, LintPassKind::Late), + _ => { + let pass_name = cx.str_buf.alloc_kebab_to_pascal(cx.arena, pass); + let pass_idx = data.lint_passes.iter().position(|p| p.name == pass_name); + if pass_idx.is_none() { + cx.dcx.emit_spanless_err(format!("unknown lint pass `{pass}`")); + } + (pass_idx, LintPassKind::Early) + }, + }; + let name_snake = cx.str_buf.alloc_kebab_to_snake(cx.arena, name); + let Entry::Vacant(vacant_lint) = data.lints.entry(name_snake) else { + cx.dcx.emit_unknown_lint(name); + cx.dcx.exit_assume_err(); + }; + cx.dcx.exit_on_err(); + + let name_pascal = cx.str_buf.alloc_kebab_to_pascal(cx.arena, name); + let name_upper = cx.str_buf.alloc_ascii_upper(cx.arena, name_snake); + let version = cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()); + let mut lint_data = ActiveLintData { + decl_range: 0..0, + docs: if group == "restriction" { + RESTRICTION_DESC + } else { + DEFAULT_DESC + }, + group_comments: "", + group, + desc: r#""default lint description""#, + opts: "", + }; - let lint = LintData { - clippy_version, - pass, - name, - category, - ty, - }; + let mut updater = FileUpdater::default(); + + // Edit clippy source to add the new lint. + if let Some(pass_idx) = pass_idx { + let lint_pass = &mut data.lint_passes[pass_idx]; + let file = lint_pass.decl_sp.file; + let is_late_pass = lint_pass.is_late; + + lint_pass.lints = cx.str_list_buf.with(|buf| { + buf.extend(lint_pass.lints.iter().copied()); + buf.push(name_upper); + cx.arena.alloc_slice(buf) + }); + lint_data.decl_range = lint_pass.decl_sp.range.end..lint_pass.decl_sp.range.end; + vacant_lint.insert(Lint { + name_sp: Span::new(file, lint_data.decl_range), + version, + data: LintData::Active(lint_data), + }); + + let add_mod = if let Some((path, "mod.rs")) = file.path.get().rsplit_once(path::MAIN_SEPARATOR) { + updater.write_new_file(String::from_iter([path, PATH_SEP, name_snake, ".rs"]), |dst| { + write_lint_check_file(dst, name_upper, is_late_pass, has_msrv); + }); + true + } else { + false + }; + updater.change_loaded_file(file, |src, dst| { + let mut lints: Vec<_> = data.lints.lints_in_file(file).collect(); + let passes = data.lint_passes.in_same_file_as_mut(pass_idx); + let mut ranges = VecBuf::with_capacity(lints.len() + passes.len()); + let mut copy = mk_sorted_lints_copy_fn(add_mod, name_snake); + gen_sorted_lints_file(src, dst, &mut lints, passes, &mut ranges, &mut copy); + if add_mod { + // No existing module list was found; just put it at the start. + dst.insert_str(0, &format!("mod {name_snake};\n\n")); + } + }); + } else { + // Create a new lint pass. + let path = cx + .str_buf + .alloc_collect(cx.arena, ["clippy_lints", PATH_SEP, "src", PATH_SEP, name, ".rs"]); + let file = cx.source_files.alloc(SourceFile::new_empty(path)); + vacant_lint.insert(Lint { + name_sp: Span::new(file, 0..0), + version, + data: LintData::Active(lint_data), + }); + + updater.write_new_file(path, |dst| { + write_lint_file( + dst, + &ActiveLint { + name: name_snake, + version, + data: &lint_data, + }, + &LintPass { + docs: "", + name: name_pascal, + lt: None, + mac: if has_msrv { + LintPassMac::Impl + } else { + LintPassMac::Declare + }, + decl_sp: Span::new(file, 0..0), + lints: cx.arena.alloc_slice(&[name_upper]), + is_early: matches!(new_pass, LintPassKind::Early), + is_late: matches!(new_pass, LintPassKind::Late), + }, + has_msrv, + ); + }); + updater.change_file("clippy_lints/src/lib.rs", |src, dst| { + add_lint_pass(src, dst, name_snake, name_pascal, new_pass, has_msrv); + }); + } - create_lint(&lint, msrv); - create_test(&lint, msrv); + // Register the lint with the MSRV option. + if let Some(mut data) = conf_data + && let Some(opt) = data.opts.iter_mut().find(|x| x.name == "msrv") + { + opt.lints = cx.str_list_buf.with(|buf| { + buf.extend(opt.lints.iter().copied()); + buf.push(name_snake); + cx.arena.alloc_slice(buf) + }); + updater.change_loaded_file(data.decl_sp.file, |src, dst| data.gen_file(src, dst)); + } - if lint.ty.is_none() { - add_lint(&lint, msrv); - } + // Create test files. + if group == "cargo" { + let mut path = PathBuf::from_iter(["tests", "ui-cargo", name_snake]); + create_new_dir(&path); + + let mut mk_project = |name: &str, todo: &str| { + path.push(name); + create_new_dir(&path); + path.push("Cargo.toml"); + updater.write_new_file(&path, |dst| write_cargo_manifest(dst, name_snake, todo)); + path.pop(); + path.push("src"); + create_new_dir(&path); + path.push("main.rs"); + updater.write_new_file(&path, |dst| write_test_file(dst, name_snake, has_msrv)); + path.pop(); + path.pop(); + path.pop(); + }; + mk_project("pass", "Add contents that should pass"); + mk_project("fail", "Add contents the should fail"); + } else { + updater.write_new_file( + String::from_iter(["tests", PATH_SEP, "ui", PATH_SEP, name_snake, ".rs"]), + |dst| write_test_file(dst, name_snake, has_msrv), + ); + } - if pass == Pass::Early { - println!( - "\n\ - NOTE: Use a late pass unless you need something specific from\n\ - an early pass, as they lack many features and utilities" - ); - } + data.gen_decls(UpdateMode::Change); + }); } -fn create_lint(lint: &LintData<'_>, enable_msrv: bool) { - if let Some(ty) = lint.ty { - create_lint_for_ty(lint, enable_msrv, ty); +static DEFAULT_DESC: &str = "\ +/// ### What it does +/// +/// ### Why is this bad? +/// +/// ### Example +/// ```no_run +/// // example code where clippy issues a warning +/// ``` +/// Use instead: +/// ```no_run +/// // example code which does not raise clippy warning +/// ```"; + +static RESTRICTION_DESC: &str = "\ +/// ### What it does +/// +/// ### Why restrict this? +/// +/// ### Example +/// ```no_run +/// // example code where clippy issues a warning +/// ``` +/// Use instead: +/// ```no_run +/// // example code which does not raise clippy warning +/// ```"; + +fn write_lint_check_file(dst: &mut String, name_upper: &str, is_late_pass: bool, has_msrv: bool) { + let (cx_ty, cx_lt, msrv_arg, msrv_import) = if is_late_pass { + ( + "LateContext", + "<'_>", + ", msrv: Msrv", + "use clippy_utils::msrvs::{self, Msrv};\n", + ) } else { - let path = format!("clippy_lints/src/{}.rs", lint.name); - File::create_new(&path).write(get_lint_file_contents(lint, enable_msrv)); - println!("Generated lint file: `{path}`"); - } -} - -fn create_test(lint: &LintData<'_>, msrv: bool) { - fn create_project_layout>(lint_name: &str, location: P, case: &str, hint: &str, msrv: bool) { - let mut path = location.into().join(case); - create_new_dir(&path); - - path.push("Cargo.toml"); - File::create_new(&path).write(get_manifest_contents(lint_name, hint)); - path.pop(); - - path.push("src"); - create_new_dir(&path); - path.push("main.rs"); - File::create_new(&path).write(get_test_file_contents(lint_name, msrv)); - } - - if lint.category == "cargo" { - let test_dir = format!("tests/ui-cargo/{}", lint.name); - create_new_dir(&test_dir); + ( + "EarlyContext", + "", + ", msrv: &MsrvStack", + "use clippy_utils::msrvs::{self, MsrvStack};\n", + ) + }; + let (msrv_arg, msrv_import) = if has_msrv { (msrv_arg, msrv_import) } else { ("", "") }; + let _ = write!( + dst, + "\ +{msrv_import}use rustc_lint::{cx_ty}; - create_project_layout( - lint.name, - &test_dir, - "fail", - "Content that triggers the lint goes here", - msrv, - ); - create_project_layout( - lint.name, - &test_dir, - "pass", - "This file should not trigger the lint", - false, - ); +use super::{name_upper}; - println!("Generated test directories: `{test_dir}/pass`, `{test_dir}/fail`"); - } else { - let test_path = format!("tests/ui/{}.rs", lint.name); - File::create_new(&test_path).write(get_test_file_contents(lint.name, msrv)); - println!("Generated test file: `{test_path}`"); - } +pub(super) fn check(cx: &{cx_ty}{cx_lt}{msrv_arg}) {{ + todo!(\"implement lint logic\"); +}}\n" + ); } -fn add_lint(lint: &LintData<'_>, enable_msrv: bool) { - let path = "clippy_lints/src/lib.rs"; - let mut file = File::open(&path, OpenOptions::new().read(true).write(true)); - let mut lib_rs = String::new(); - file.read_append_to_string(&mut lib_rs); - - let module_name = lint.name; - let camel_name = to_camel_case(lint.name); - - let (comment, new_lint) = if lint.pass == Pass::Late { - // Late passes are folded into the statically-combined struct, so a new - // entry is just `Field: Type = constructor` (see `combined_late_pass`). - let new_lint = if enable_msrv { - format!("{camel_name}: {module_name}::{camel_name} = {module_name}::{camel_name}::new(conf),\n ") - } else { - format!("{camel_name}: {module_name}::{camel_name} = {module_name}::{camel_name},\n ") - }; - ("// add late passes here", new_lint) +fn write_test_file(dst: &mut String, name: &str, has_msrv: bool) { + let msrv_contents = if has_msrv { + "\n + // TODO: set `xx` to on below the required MSRV and `yy` to the required MSRV. + #[clippy::msrv = \"1.xx\"] + { + // TODO: test which requires the msrv to be set + }; + #[clippy::msrv =\"1.yy\"] + { + // TODO: same test as above + }\n" } else { - // Early passes are folded into the statically-combined struct, so a new - // entry is just `Field: Type = constructor` (see `combined_early_pass`). - let new_lint = if enable_msrv { - format!("{camel_name}: {module_name}::{camel_name} = {module_name}::{camel_name}::new(conf),\n ") - } else { - format!("{camel_name}: {module_name}::{camel_name} = {module_name}::{camel_name},\n ") - }; - ("// add early passes here", new_lint) + "" }; - let comment_start = lib_rs.find(comment).expect("Couldn't find comment"); - - lib_rs.insert_str(comment_start, &new_lint); - - file.replace_contents(lib_rs); -} - -fn to_camel_case(name: &str) -> String { - name.split('_') - .map(|s| { - if s.is_empty() { - String::new() - } else { - [&s[0..1].to_uppercase(), &s[1..]].concat() - } - }) - .collect() -} - -fn get_test_file_contents(lint_name: &str, msrv: bool) -> String { - let mut test = formatdoc!( - r" - #![warn(clippy::{lint_name})] - - fn main() {{ - // test code goes here - }} - " + let _ = write!( + dst, + "\ +#![warn(clippy::{name})] + +fn main() {{ + // TODO: fill in tests{msrv_contents} +}}\n", ); - - if msrv { - let _ = writedoc!( - test, - r#" - - // TODO: set xx to the version one below the MSRV used by the lint, and yy to - // the version used by the lint - #[clippy::msrv = "1.xx"] - fn msrv_1_xx() {{ - // a simple example that would trigger the lint if the MSRV were met - }} - - #[clippy::msrv = "1.yy"] - fn msrv_1_yy() {{ - // the same example as above - }} - "# - ); - } - - test } -fn get_manifest_contents(lint_name: &str, hint: &str) -> String { - formatdoc!( - r#" - # {hint} +fn write_cargo_manifest(dst: &mut String, name: &str, todo: &str) { + let _ = write!( + dst, + "\ +[package] +name = \"{name}\" +version = \"0.1.0\" +publish = false - [package] - name = "{lint_name}" - version = "0.1.0" - publish = false +[workspace] - [workspace] - "# - ) +# TODO: {todo}\n", + ); } -fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String { - let mut result = String::new(); - - let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass { - Pass::Early => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"), - Pass::Late => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"), - }; - let (msrv_ty, msrv_ctor, extract_msrv) = match lint.pass { - Pass::Early => ( +fn write_lint_file(dst: &mut String, lint: &ActiveLint<'_, '_>, pass: &LintPass<'_>, has_msrv: bool) { + let (pass_ty, pass_lt, cx_ty, msrv_ty, msrv_ctor, extract_msrv) = if pass.is_late { + ("LateLintPass", "<'_>", "LateContext", "Msrv", "conf.msrv", "") + } else { + ( + "EarlyLintPass", + "", + "EarlyContext", "MsrvStack", "MsrvStack::new(conf.msrv)", - "\n extract_msrv_attr!();\n", - ), - Pass::Late => ("Msrv", "conf.msrv", ""), + "\n extract_msrv_attr!();", + ) }; - - let lint_name = lint.name; - let category = lint.category; - let name_camel = to_camel_case(lint.name); - let name_upper = lint_name.to_uppercase(); - - if enable_msrv { - let _: fmt::Result = writedoc!( - result, - r" - use clippy_config::Conf; - use clippy_utils::msrvs::{{self, {msrv_ty}}}; - {pass_import} - use rustc_lint::{{{context_import}, {pass_type}}}; - use rustc_session::impl_lint_pass; - - " + let extract_msrv = if has_msrv { + let _ = write!( + dst, + "use clippy_config::Conf;\n\ + use clippy_utils::msrvs::{{self, {msrv_ty}}};\n", ); + extract_msrv } else { - let _: fmt::Result = writedoc!( - result, - r" - {pass_import} - use rustc_lint::{{{context_import}, {pass_type}}}; - use rustc_session::declare_lint_pass; - - " - ); - } + "" + }; + let pass_mac = pass.mac.name(); + let pass_name = pass.name; - let _: fmt::Result = writeln!( - result, - "{}", - get_lint_declaration(lint.clippy_version, &name_upper, category) + let _ = write!( + dst, + "use rustc_lint::{{{cx_ty}, {pass_ty}}};\n\ + use rustc_session::{pass_mac};\n\n", ); - - if enable_msrv { - let _: fmt::Result = writedoc!( - result, - r" - pub struct {name_camel} {{ - msrv: {msrv_ty}, - }} - - impl {name_camel} {{ - pub fn new(conf: &'static Conf) -> Self {{ - Self {{ msrv: {msrv_ctor} }} - }} - }} - - impl_lint_pass!({name_camel} => [{name_upper}]); - - impl {pass_type}{pass_lifetimes} for {name_camel} {{{extract_msrv}}} - - // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed. - // TODO: Update msrv config comment in `clippy_config/src/conf.rs` - " - ); - } else { - let _: fmt::Result = writedoc!( - result, - r" - declare_lint_pass!({name_camel} => [{name_upper}]); - - impl {pass_type}{pass_lifetimes} for {name_camel} {{}} - " + lint.gen_mac(dst); + dst.push_str("\n\n"); + pass.gen_mac(dst); + dst.push_str("\n\n"); + if has_msrv { + let _ = write!( + dst, + "\ +pub struct {pass_name} {{ + msrv: {msrv_ty}, +}} + +impl {pass_name} {{ + pub fn new(conf: &'static Conf) -> Self {{ + Self {{ msrv: {msrv_ctor} }} + }} +}}\n\n" ); } - - result -} - -fn get_lint_declaration(version: Version, name_upper: &str, category: &str) -> String { - let justification_heading = if category == "restriction" { - "Why restrict this?" - } else { - "Why is this bad?" - }; - formatdoc!( - r#" - declare_clippy_lint! {{ - /// ### What it does - /// - /// ### {justification_heading} - /// - /// ### Example - /// ```no_run - /// // example code where clippy issues a warning - /// ``` - /// Use instead: - /// ```no_run - /// // example code which does not raise clippy warning - /// ``` - #[clippy::version = "{}"] - pub {name_upper}, - {category}, - "default lint description" - }}"#, - version.rust_display(), - ) -} - -fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) { - match ty { - "cargo" => assert_eq!( - lint.category, "cargo", - "Lints of type `cargo` must have the `cargo` category" - ), - _ if lint.category == "cargo" => panic!("Lints of category `cargo` must have the `cargo` type"), - _ => {}, - } - - let ty_dir = PathBuf::from(format!("clippy_lints/src/{ty}")); - assert!( - ty_dir.exists() && ty_dir.is_dir(), - "Directory `{}` does not exist!", - ty_dir.display() - ); - - let lint_file_path = ty_dir.join(format!("{}.rs", lint.name)); - assert!( - !lint_file_path.exists(), - "File `{}` already exists", - lint_file_path.display() + let _ = writeln!( + dst, + "\ +impl {pass_ty}{pass_lt} for {pass_name} {{ + // TODO: implement lint logic{extract_msrv} +}}\n", ); +} - let mod_file_path = ty_dir.join("mod.rs"); - let context_import = setup_mod_file(&mod_file_path, lint); - let (pass_lifetimes, msrv_ty, msrv_ref, msrv_cx) = match context_import { - "LateContext" => ("<'_>", "Msrv", "", "cx, "), - _ => ("", "MsrvStack", "&", ""), +fn add_lint_pass( + src: &str, + dst: &mut String, + name_snake: &str, + name_pascal: &str, + new_pass: LintPassKind, + has_msrv: bool, +) { + let comment = match new_pass { + LintPassKind::Early => "// add early passes here, used by `cargo dev new_lint`", + LintPassKind::Late => "// add late passes here, used by `cargo dev new_lint`", }; - - let name_upper = lint.name.to_uppercase(); - let mut lint_file_contents = String::new(); - - if enable_msrv { - let _: fmt::Result = writedoc!( - lint_file_contents, - r#" - use clippy_utils::msrvs::{{self, {msrv_ty}}}; - use rustc_lint::{{{context_import}, LintContext}}; - - use super::{name_upper}; - - // TODO: Adjust the parameters as necessary - pub(super) fn check(cx: &{context_import}{pass_lifetimes}, msrv: {msrv_ref}{msrv_ty}) {{ - if !msrv.meets({msrv_cx}todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{ - return; - }} - todo!(); - }} - "# + let ctor_call = if has_msrv { "::new(conf)" } else { "" }; + let pos = if let Some(pos) = src.find(comment) { + dst.push_str(&src[..pos]); + let _ = write!( + dst, + "{name_pascal}: {name_snake}::{name_pascal} = {name_snake}::{name_pascal}{ctor_call},\ + \n ", ); + pos } else { - let _: fmt::Result = writedoc!( - lint_file_contents, - r" - use rustc_lint::{{{context_import}, LintContext}}; - - use super::{name_upper}; - - // TODO: Adjust the parameters as necessary - pub(super) fn check(cx: &{context_import}{pass_lifetimes}) {{ - todo!(); - }} - " - ); - } - - File::create_new(&lint_file_path).write(lint_file_contents); - println!("Generated lint file: `clippy_lints/src/{ty}/{}.rs`", lint.name); - println!( - "Be sure to add a call to `{}::check` in `clippy_lints/src/{ty}/mod.rs`!", - lint.name - ); + 0 + }; + dst.push_str(&src[pos..]); } -fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> &'static str { - let lint_name_upper = lint.name.to_uppercase(); - - let mut file = File::open(path, OpenOptions::new().read(true).write(true)); - let mut file_contents = String::new(); - file.read_append_to_string(&mut file_contents); - - assert!( - !file_contents.contains(&format!("pub {lint_name_upper},")), - "Lint `{}` already defined in `{}`", - lint.name, - path.display() - ); - - let (lint_context, lint_decl_end) = parse_mod_file(path, &file_contents); - - // Add the lint declaration to `mod.rs` - file_contents.insert_str( - lint_decl_end, - &format!( - "\n\n{}", - get_lint_declaration(lint.clippy_version, &lint_name_upper, lint.category) - ), - ); - - // Add the lint to `impl_lint_pass`/`declare_lint_pass` - let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| { - file_contents - .find("declare_lint_pass!") - .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`")) - }); - - let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| { - panic!("malformed `impl_lint_pass`/`declare_lint_pass`"); - }); - - arr_start += impl_lint_pass_start; - - let mut arr_end = file_contents[arr_start..] - .find(']') - .expect("failed to find `impl_lint_pass` terminator"); - - arr_end += arr_start; - - let mut arr_content = file_contents[arr_start + 1..arr_end].to_string(); - arr_content.retain(|c| !c.is_whitespace()); - - let mut new_arr_content = String::new(); - for ident in arr_content - .split(',') - .chain(std::iter::once(&*lint_name_upper)) - .filter(|s| !s.is_empty()) - { - let _: fmt::Result = write!(new_arr_content, "\n {ident},"); - } - new_arr_content.push('\n'); - - file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content); - - // Just add the mod declaration at the top, it'll be fixed by rustfmt - file_contents.insert_str(0, &format!("mod {};\n", lint.name)); - - file.replace_contents(file_contents); - lint_context +enum ModPos { + /// The position of the name of the module to insert before. + Name(u32), + /// The position of the end of the module list after the final semicolon. + End(u32), } -// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl -fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) { - #[allow(clippy::enum_glob_use)] - use cursor::Pat::*; - - let mut context = None; - let mut decl_end = None; - let mut cursor = Cursor::new(contents); - let mut captures = [Capture::EMPTY]; - while let Some(name) = cursor.find_capture_ident() { - match cursor.get_text(name) { - "declare_clippy_lint" - if cursor.match_all(&[Bang, OpenBrace], &mut []).is_ok() && cursor.find_close_brace() => - { - decl_end = Some(cursor.pos()); - }, - "impl" - if cursor - .match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) - .is_ok() => - { - match cursor.get_text(captures[0]) { - "LateLintPass" => context = Some("LateContext"), - "EarlyLintPass" => context = Some("EarlyContext"), - _ => {}, - } - }, - _ => {}, +/// Copies the source text to the destination adding a module declaration if `add_mod` is true. +fn mk_sorted_lints_copy_fn(mut add_mod: bool, mod_name: &str) -> impl FnMut(&str, &mut String) { + move |src, dst| { + if add_mod && let Some(pos) = find_mod_decl_after(&mut Cursor::new(src), mod_name) { + match pos { + ModPos::Name(pos) => { + let (pre, post) = src.split_at(pos as usize); + dst.extend([pre, mod_name, ";\nmod ", post]); + }, + ModPos::End(pos) => { + let (pre, post) = src.split_at(pos as usize); + dst.extend([pre, "mod ", mod_name, ";\n", post]); + }, + } + add_mod = false; + } else { + dst.push_str(src); } } - - ( - context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display())), - decl_end.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display())) as usize, - ) } -#[test] -fn test_camel_case() { - let s = "a_lint"; - let s2 = to_camel_case(s); - assert_eq!(s2, "ALint"); - - let name = "a_really_long_new_lint"; - let name2 = to_camel_case(name); - assert_eq!(name2, "AReallyLongNewLint"); - - let name3 = "lint__name"; - let name4 = to_camel_case(name3); - assert_eq!(name4, "LintName"); +/// Gets the position to insert a pub module with the specified name. Returns +/// `None` if a module list could not be found. +fn find_mod_decl_after(cursor: &mut Cursor<'_>, mod_name: &str) -> Option { + if !cursor.find_ident("mod") { + return None; + } + let mut end = None; + while let Some(name) = cursor.capture_ident() { + if !cursor.eat_semi() { + break; + } + if cursor.get_text(name) > mod_name { + return Some(ModPos::Name(name.pos)); + } + end = Some(cursor.pos()); + if !cursor.eat_ident("mod") { + break; + } + } + end.map(ModPos::End) } diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs index 8286ff542101..50f05b48a481 100644 --- a/clippy_dev/src/parse.rs +++ b/clippy_dev/src/parse.rs @@ -79,6 +79,7 @@ impl Display for LintName<'_> { } } +#[derive(Clone, Copy)] pub struct ActiveLintData<'cx> { pub decl_range: Range, /// The raw text of the documentation comments. May include leading/trailing @@ -95,26 +96,31 @@ pub struct ActiveLintData<'cx> { pub opts: &'cx str, } +#[derive(Clone, Copy)] pub struct DeprecatedLintData<'cx> { pub reason: &'cx str, } +#[derive(Clone, Copy)] pub struct RenamedLintData<'cx> { pub new_name: LintName<'cx>, } +#[derive(Clone, Copy)] pub enum LintData<'cx> { Active(ActiveLintData<'cx>), Deprecated(DeprecatedLintData<'cx>), Renamed(RenamedLintData<'cx>), } +#[derive(Clone, Copy)] pub struct ActiveLint<'a, 'cx> { pub name: &'cx str, pub version: &'cx str, pub data: &'a ActiveLintData<'cx>, } +#[derive(Clone, Copy)] pub struct Lint<'cx> { pub name_sp: Span<'cx>, pub version: &'cx str, @@ -135,6 +141,21 @@ impl LintPassMac { } } +#[derive(Clone, Copy)] +enum ImplTrait { + EarlyLintPass, + LateLintPass, +} +impl ImplTrait { + fn from_str(s: &str) -> Option { + match s { + "EarlyLintPass" => Some(Self::EarlyLintPass), + "LateLintPass" => Some(Self::LateLintPass), + _ => None, + } + } +} + pub struct LintPass<'cx> { /// The raw text of the documentation comments. May include leading/trailing /// whitespace and empty lines. @@ -144,6 +165,16 @@ pub struct LintPass<'cx> { pub mac: LintPassMac, pub decl_sp: Span<'cx>, pub lints: &'cx mut [&'cx str], + pub is_early: bool, + pub is_late: bool, +} +impl LintPass<'_> { + fn add_trait_impl(&mut self, kind: ImplTrait) { + match kind { + ImplTrait::EarlyLintPass => self.is_early = true, + ImplTrait::LateLintPass => self.is_late = true, + } + } } pub struct LintMap<'cx>(FxHashMap<&'cx str, Lint<'cx>>); @@ -167,6 +198,22 @@ impl<'cx> LintMap<'cx> { lints } + pub fn lints_in_file<'s>(&'s self, file: &SourceFile<'_>) -> impl Iterator> { + self.iter().filter_map(move |(&name, lint)| { + if let LintData::Active(data) = &lint.data + && lint.name_sp.file == file + { + Some(ActiveLint { + name, + version: lint.version, + data, + }) + } else { + None + } + }) + } + #[track_caller] fn get_vacant_lint<'s>( &'s mut self, @@ -202,11 +249,23 @@ impl<'cx> LintPasses<'cx> { tail.iter().take_while(|&x| x.decl_sp.file == head.decl_sp.file).count() }) } + + pub fn in_same_file_as_mut<'s>(&'s mut self, i: usize) -> &'s mut [LintPass<'cx>] { + let file = self[i].decl_sp.file; + let pre = self[..i].iter().rev().take_while(|&x| x.decl_sp.file == file).count(); + let post = self[i + 1..].iter().take_while(|&x| x.decl_sp.file == file).count(); + &mut self[i - pre..i + 1 + post] + } } impl<'cx> Deref for LintPasses<'cx> { - type Target = [LintPass<'cx>]; + type Target = Vec>; fn deref(&self) -> &Self::Target { - self.0.deref() + &self.0 + } +} +impl DerefMut for LintPasses<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } @@ -363,18 +422,19 @@ impl<'cx> ParseCxImpl<'cx> { } /// Parse a source file looking for `declare_clippy_lint` macro invocations. + #[expect(clippy::too_many_lines)] fn parse_lint_src_file(&mut self, data: &mut ParsedLints<'cx>, file: &'cx SourceFile<'cx>) { #[allow(clippy::enum_glob_use)] use cursor::Pat::*; let mut cursor = Cursor::new(&file.contents); let mut captures = [Capture::EMPTY; 6]; + let mut trait_impls = Vec::new(); + let first_lint_pass = data.lint_passes.len(); + while let Some(mac_name) = cursor.find_capture_ident() { - if !cursor.eat_bang() { - continue; - } match cursor.get_text(mac_name) { - "declare_clippy_lint" => { + "declare_clippy_lint" if cursor.eat_bang() => { #[rustfmt::skip] static DECL_START: &[cursor::Pat] = &[ // { /// docs @@ -428,7 +488,7 @@ impl<'cx> ParseCxImpl<'cx> { }); } }, - mac @ ("declare_lint_pass" | "impl_lint_pass") => { + mac @ ("declare_lint_pass" | "impl_lint_pass") if cursor.eat_bang() => { let mut has_lt = false; let mut lints: &mut [_] = &mut []; if let Err(expected) = cursor @@ -450,7 +510,7 @@ impl<'cx> ParseCxImpl<'cx> { { cursor.emit_unexpected(&mut self.dcx, file, expected); } else { - data.lint_passes.0.push(LintPass { + data.lint_passes.push(LintPass { docs: cursor.get_text(captures[0]), name: cursor.get_text(captures[1]), lt: has_lt.then(|| cursor.get_text(captures[2])), @@ -461,12 +521,42 @@ impl<'cx> ParseCxImpl<'cx> { }, decl_sp: Span::new(file, mac_name.pos..cursor.pos()), lints, + is_early: false, + is_late: false, }); } }, + "impl" + if cursor.opt_match_all(&[Lt, Lifetime, Gt], &mut []).is_ok() + && let Some(trait_) = cursor.capture_ident() + && let Some(trait_) = ImplTrait::from_str(cursor.get_text(trait_)) + && cursor.opt_match_all(&[Lt, Lifetime, Gt], &mut []).is_ok() + && cursor + .match_all(&[Ident(IdentPat::r#for), CaptureIdent], &mut captures) + .is_ok() => + { + let impl_ty = cursor.get_text(captures[0]); + if let Some(pass) = data.lint_passes[first_lint_pass..] + .iter_mut() + .find(|pass| pass.name == impl_ty) + { + pass.add_trait_impl(trait_); + } else { + trait_impls.push((impl_ty, trait_)); + } + }, _ => {}, } } + + for &(impl_ty, trait_) in &trait_impls { + if let Some(pass) = data.lint_passes[first_lint_pass..] + .iter_mut() + .find(|pass| pass.name == impl_ty) + { + pass.add_trait_impl(trait_); + } + } } fn parse_deprecated_lints(&mut self, data: &mut ParsedLints<'cx>) { diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs index 6deffe80b026..2e03318879c2 100644 --- a/clippy_dev/src/parse/cursor.rs +++ b/clippy_dev/src/parse/cursor.rs @@ -101,6 +101,7 @@ decl_ident_pats! { RENAMED, RENAMED_VERSION, clippy, + r#for = "for", r#pub = "pub", version, } diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index a8281a99839c..3a3fa50576a2 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -107,9 +107,21 @@ impl<'a> File<'a> { Self::open(path, OpenOptions::new().read(true)) } + /// Opens a file for reading and writing, panicking of failure. + #[track_caller] + pub fn open_rw(path: &'a (impl AsRef + ?Sized)) -> Self { + Self::open(path, OpenOptions::new().read(true).write(true)) + } + + /// Truncates and opens a file for writing, panicking of failure. + #[track_caller] + pub fn open_truncated(path: &'a (impl AsRef + ?Sized)) -> Self { + Self::open(path, OpenOptions::new().truncate(true).write(true)) + } + /// Creates a new file with the specified contents, panicking on failure. #[track_caller] - pub fn create_new(path: &'a impl AsRef) -> Self { + pub fn create_new(path: &'a (impl AsRef + ?Sized)) -> Self { let path = path.as_ref(); Self { inner: expect_action( @@ -335,11 +347,6 @@ impl UpdateStatus { pub fn from_changed(value: bool) -> Self { if value { Self::Changed } else { Self::Unchanged } } - - #[must_use] - pub fn is_changed(self) -> bool { - matches!(self, Self::Changed) - } } #[derive(Clone, Copy)] @@ -373,7 +380,7 @@ impl FileUpdater { path: &Path, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, ) { - let mut file = File::open(path, OpenOptions::new().read(true).write(true)); + let mut file = File::open_rw(path); file.read_to_cleared_string(&mut self.src_buf); self.dst_buf.clear(); match (mode, update(path, &self.src_buf, &mut self.dst_buf)) { @@ -410,23 +417,12 @@ impl FileUpdater { process::exit(1); }, (UpdateMode::Change, UpdateStatus::Changed) => { - File::open(file.path.get(), OpenOptions::new().truncate(true).write(true)) - .write(self.dst_buf.as_bytes()); + File::open_truncated(file.path.get()).write(self.dst_buf.as_bytes()); }, (UpdateMode::Check | UpdateMode::Change, UpdateStatus::Unchanged) => {}, } } - #[track_caller] - fn update_file_inner(&mut self, path: &Path, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus) { - let mut file = File::open(path, OpenOptions::new().read(true).write(true)); - file.read_to_cleared_string(&mut self.src_buf); - self.dst_buf.clear(); - if update(path, &self.src_buf, &mut self.dst_buf).is_changed() { - file.replace_contents(self.dst_buf.as_bytes()); - } - } - #[track_caller] pub fn update_file_checked( &mut self, @@ -444,7 +440,30 @@ impl FileUpdater { path: impl AsRef, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, ) { - self.update_file_inner(path.as_ref(), update); + self.update_file_checked_inner("", UpdateMode::Change, path.as_ref(), update); + } + + #[track_caller] + pub fn write_new_file(&mut self, path: impl AsRef, f: impl FnOnce(&mut String)) { + self.dst_buf.clear(); + f(&mut self.dst_buf); + File::create_new(path.as_ref()).write(&self.dst_buf); + } + + #[track_caller] + pub fn change_file(&mut self, path: impl AsRef, f: impl FnOnce(&str, &mut String)) { + let mut file = File::open_rw(path.as_ref()); + file.read_to_cleared_string(&mut self.src_buf); + self.dst_buf.clear(); + f(&self.src_buf, &mut self.dst_buf); + file.replace_contents(&self.dst_buf); + } + + #[track_caller] + pub fn change_loaded_file(&mut self, file: &SourceFile<'_>, f: impl FnOnce(&str, &mut String)) { + self.dst_buf.clear(); + f(&file.contents, &mut self.dst_buf); + File::open_truncated(file.path.get()).write(&self.dst_buf); } } @@ -759,6 +778,46 @@ impl StrBuf { } } + /// Allocates the string onto the arena with all ascii characters converted to + /// uppercase. + pub fn alloc_ascii_upper<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str { + self.0.clear(); + self.0.push_str(s); + self.0.make_ascii_uppercase(); + if self.0.is_empty() { + "" + } else { + arena.alloc_str(&self.0) + } + } + + /// Allocates the string onto the arena after converting the kebab-cased identifier + /// to pascal casing. + pub fn alloc_kebab_to_pascal<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str { + self.0.clear(); + let mut first = true; + for c in s.chars() { + if c == '-' { + first = true; + } else if first { + self.0.push(c.to_ascii_uppercase()); + first = false; + } else { + self.0.push(c); + } + } + if self.0.is_empty() { + "" + } else { + arena.alloc_str(&self.0) + } + } + + /// Allocates the string onto the arena after converting the kebab-cased identifier + /// to snake casing. + pub fn alloc_kebab_to_snake<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str { + self.alloc_replaced(arena, s, '-', "_") + } /// Collects all elements into the buffer and allocates that onto the arena. pub fn alloc_collect<'cx, I>(&mut self, arena: &'cx DroplessArena, iter: I) -> &'cx str where @@ -829,11 +888,20 @@ pub struct SourceFile<'cx> { pub contents: String, } impl<'cx> SourceFile<'cx> { + #[must_use] + pub fn new_empty(path: &'cx str) -> Self { + Self { + path: Cell::new(path), + line_starts: OnceCell::new(), + contents: String::new(), + } + } + #[must_use] pub fn load(path: &'cx str) -> Self { let mut contents = String::new(); File::open_read(path).read_append_to_string(&mut contents); - SourceFile { + Self { path: Cell::new(path), line_starts: OnceCell::new(), contents,