about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJason Newcomb <jsnewcomb@pm.me>2025-04-15 00:58:31 -0400
committerJason Newcomb <jsnewcomb@pm.me>2025-05-12 17:07:53 -0400
commita9beb8b68d5bc7e66636fdab0ecf21af793e4b7f (patch)
treebfdf547f0df5793a4179cd21b57820a73c1d477d
parent97abf33fa0f8eec35d162d8eab1673e29ecbba29 (diff)
downloadrust-a9beb8b68d5bc7e66636fdab0ecf21af793e4b7f.tar.gz
rust-a9beb8b68d5bc7e66636fdab0ecf21af793e4b7f.zip
clippy_dev: Refactor token parsing to avoid macros.
-rw-r--r--clippy_dev/src/new_lint.rs119
-rw-r--r--clippy_dev/src/update_lints.rs286
-rw-r--r--clippy_dev/src/utils.rs195
-rw-r--r--clippy_lints/src/deprecated_lints.rs8
4 files changed, 345 insertions, 263 deletions
diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs
index a6e8c3ac324..4121daa85e6 100644
--- a/clippy_dev/src/new_lint.rs
+++ b/clippy_dev/src/new_lint.rs
@@ -1,4 +1,4 @@
-use crate::utils::Version;
+use crate::utils::{RustSearcher, Token, Version};
 use clap::ValueEnum;
 use indoc::{formatdoc, writedoc};
 use std::fmt::{self, Write as _};
@@ -360,8 +360,7 @@ fn get_lint_declaration(version: Version, name_upper: &str, category: &str) -> S
                 pub {name_upper},
                 {category},
                 "default lint description"
-            }}
-        "#,
+            }}"#,
         version.rust_display(),
     )
 }
@@ -446,9 +445,6 @@ fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::R
 
 #[allow(clippy::too_many_lines)]
 fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
-    use super::update_lints::{LintDeclSearchResult, match_tokens};
-    use rustc_lexer::TokenKind;
-
     let lint_name_upper = lint.name.to_uppercase();
 
     let mut file_contents = fs::read_to_string(path)?;
@@ -459,81 +455,11 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str>
         path.display()
     );
 
-    let mut offset = 0usize;
-    let mut last_decl_curly_offset = None;
-    let mut lint_context = None;
-
-    let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
-        let range = offset..offset + t.len as usize;
-        offset = range.end;
-
-        LintDeclSearchResult {
-            token_kind: t.kind,
-            content: &file_contents[range.clone()],
-            range,
-        }
-    });
-
-    // Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
-    while let Some(LintDeclSearchResult { content, .. }) = iter.find(|result| result.token_kind == TokenKind::Ident) {
-        let mut iter = iter
-            .by_ref()
-            .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
-
-        match content {
-            "declare_clippy_lint" => {
-                // matches `!{`
-                match_tokens!(iter, Bang OpenBrace);
-                if let Some(LintDeclSearchResult { range, .. }) =
-                    iter.find(|result| result.token_kind == TokenKind::CloseBrace)
-                {
-                    last_decl_curly_offset = Some(range.end);
-                }
-            },
-            "impl" => {
-                let mut token = iter.next();
-                match token {
-                    // matches <'foo>
-                    Some(LintDeclSearchResult {
-                        token_kind: TokenKind::Lt,
-                        ..
-                    }) => {
-                        match_tokens!(iter, Lifetime { .. } Gt);
-                        token = iter.next();
-                    },
-                    None => break,
-                    _ => {},
-                }
-
-                if let Some(LintDeclSearchResult {
-                    token_kind: TokenKind::Ident,
-                    content,
-                    ..
-                }) = token
-                {
-                    // Get the appropriate lint context struct
-                    lint_context = match content {
-                        "LateLintPass" => Some("LateContext"),
-                        "EarlyLintPass" => Some("EarlyContext"),
-                        _ => continue,
-                    };
-                }
-            },
-            _ => {},
-        }
-    }
-
-    drop(iter);
-
-    let last_decl_curly_offset =
-        last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
-    let lint_context =
-        lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
+    let (lint_context, lint_decl_end) = parse_mod_file(path, &file_contents);
 
     // Add the lint declaration to `mod.rs`
-    file_contents.replace_range(
-        // Remove the trailing newline, which should always be present
-        last_decl_curly_offset..=last_decl_curly_offset,
+    file_contents.insert_str(
+        lint_decl_end,
         &format!(
             "\n\n{}",
             get_lint_declaration(lint.clippy_version, &lint_name_upper, lint.category)
@@ -588,6 +514,41 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str>
     Ok(lint_context)
 }
 
+// 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 Token::*;
+
+    let mut context = None;
+    let mut decl_end = None;
+    let mut searcher = RustSearcher::new(contents);
+    while let Some(name) = searcher.find_capture_token(CaptureIdent) {
+        match name {
+            "declare_clippy_lint" => {
+                if searcher.match_tokens(&[Bang, OpenBrace], &mut []) && searcher.find_token(CloseBrace) {
+                    decl_end = Some(searcher.pos());
+                }
+            },
+            "impl" => {
+                let mut capture = "";
+                if searcher.match_tokens(&[Lt, Lifetime, Gt, CaptureIdent], &mut [&mut capture]) {
+                    match capture {
+                        "LateLintPass" => context = Some("LateContext"),
+                        "EarlyLintPass" => context = Some("EarlyContext"),
+                        _ => {},
+                    }
+                }
+            },
+            _ => {},
+        }
+    }
+
+    (
+        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";
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index 28c988bc19f..09e97491c51 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -1,8 +1,7 @@
-use crate::utils::{File, FileAction, FileUpdater, UpdateMode, UpdateStatus, panic_file, update_text_region_fn};
-use core::str;
+use crate::utils::{
+    File, FileAction, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, panic_file, update_text_region_fn,
+};
 use itertools::Itertools;
-use rustc_lexer::{LiteralKind, TokenKind, tokenize};
-use rustc_literal_escaper::{Mode, unescape_unicode};
 use std::collections::{HashMap, HashSet};
 use std::fmt::Write;
 use std::fs::OpenOptions;
@@ -140,17 +139,6 @@ pub struct Lint {
 }
 
 impl Lint {
-    #[must_use]
-    fn new(name: &str, group: &str, desc: &str, module: &str, declaration_range: Range<usize>) -> Self {
-        Self {
-            name: name.to_lowercase(),
-            group: group.into(),
-            desc: remove_line_splices(desc),
-            module: module.into(),
-            declaration_range,
-        }
-    }
-
     /// Returns the lints in a `HashMap`, grouped by the different lint groups
     #[must_use]
     fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
@@ -163,27 +151,11 @@ pub struct DeprecatedLint {
     pub name: String,
     pub reason: String,
 }
-impl DeprecatedLint {
-    fn new(name: &str, reason: &str) -> Self {
-        Self {
-            name: remove_line_splices(name),
-            reason: remove_line_splices(reason),
-        }
-    }
-}
 
 pub struct RenamedLint {
     pub old_name: String,
     pub new_name: String,
 }
-impl RenamedLint {
-    fn new(old_name: &str, new_name: &str) -> Self {
-        Self {
-            old_name: remove_line_splices(old_name),
-            new_name: remove_line_splices(new_name),
-        }
-    }
-}
 
 pub fn gen_renamed_lints_test_fn(lints: &[RenamedLint]) -> impl Fn(&Path, &str, &mut String) -> UpdateStatus {
     move |_, src, dst| {
@@ -213,6 +185,7 @@ pub fn find_lint_decls() -> Vec<Lint> {
     let mut contents = String::new();
     for (file, module) in read_src_with_module("clippy_lints/src".as_ref()) {
         parse_clippy_lint_decls(
+            file.path(),
             File::open_read_to_cleared_string(file.path(), &mut contents),
             &module,
             &mut lints,
@@ -256,94 +229,34 @@ fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator<Item = (DirE
     })
 }
 
-macro_rules! match_tokens {
-    ($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
-         {
-            $(#[allow(clippy::redundant_pattern)] let Some(LintDeclSearchResult {
-                    token_kind: TokenKind::$token $({$($fields)*})?,
-                    content: $($capture @)? _,
-                    ..
-            }) = $iter.next() else {
-                continue;
-            };)*
-            #[allow(clippy::unused_unit)]
-            { ($($($capture,)?)*) }
-        }
-    }
-}
-
-pub(crate) use match_tokens;
-
-pub(crate) struct LintDeclSearchResult<'a> {
-    pub token_kind: TokenKind,
-    pub content: &'a str,
-    pub range: Range<usize>,
-}
-
 /// Parse a source file looking for `declare_clippy_lint` macro invocations.
-fn parse_clippy_lint_decls(contents: &str, module: &str, lints: &mut Vec<Lint>) {
-    let mut offset = 0usize;
-    let mut iter = tokenize(contents).map(|t| {
-        let range = offset..offset + t.len as usize;
-        offset = range.end;
-
-        LintDeclSearchResult {
-            token_kind: t.kind,
-            content: &contents[range.clone()],
-            range,
-        }
-    });
-
-    while let Some(LintDeclSearchResult { range, .. }) = iter.find(
-        |LintDeclSearchResult {
-             token_kind, content, ..
-         }| token_kind == &TokenKind::Ident && *content == "declare_clippy_lint",
-    ) {
-        let start = range.start;
-        let mut iter = iter
-            .by_ref()
-            .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
-        // matches `!{`
-        match_tokens!(iter, Bang OpenBrace);
-        match iter.next() {
-            // #[clippy::version = "version"] pub
-            Some(LintDeclSearchResult {
-                token_kind: TokenKind::Pound,
-                ..
-            }) => {
-                match_tokens!(iter, OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket Ident);
-            },
-            // pub
-            Some(LintDeclSearchResult {
-                token_kind: TokenKind::Ident,
-                ..
-            }) => (),
-            _ => continue,
-        }
-
-        let (name, group, desc) = match_tokens!(
-            iter,
-            // LINT_NAME
-            Ident(name) Comma
-            // group,
-            Ident(group) Comma
-            // "description"
-            Literal{..}(desc)
-        );
-
-        if let Some(end) = iter.find_map(|t| {
-            if let LintDeclSearchResult {
-                token_kind: TokenKind::CloseBrace,
-                range,
-                ..
-            } = t
-            {
-                Some(range.end)
-            } else {
-                None
-            }
-        }) {
-            lints.push(Lint::new(name, group, desc, module, start..end));
+fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec<Lint>) {
+    #[allow(clippy::enum_glob_use)]
+    use Token::*;
+    #[rustfmt::skip]
+    static DECL_TOKENS: &[Token] = &[
+        // !{ /// docs
+        Bang, OpenBrace, AnyDoc,
+        // #[clippy::version = "version"]
+        Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
+        // pub NAME, GROUP, "description"
+        Ident("pub"), CaptureIdent, Comma, CaptureIdent, Comma, CaptureLitStr,
+    ];
+
+    let mut searcher = RustSearcher::new(contents);
+    while searcher.find_token(Ident("declare_clippy_lint")) {
+        let start = searcher.pos() as usize - "declare_clippy_lint".len();
+        let (mut name, mut group, mut desc) = ("", "", "");
+        if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group, &mut desc])
+            && searcher.find_token(CloseBrace)
+        {
+            lints.push(Lint {
+                name: name.to_lowercase(),
+                group: group.into(),
+                desc: parse_str_single_line(path, desc),
+                module: module.into(),
+                declaration_range: start..searcher.pos() as usize,
+            });
         }
     }
 }
@@ -358,13 +271,30 @@ pub struct DeprecatedLints {
 }
 
 #[must_use]
-#[expect(clippy::cast_possible_truncation)]
 pub fn read_deprecated_lints() -> DeprecatedLints {
+    #[allow(clippy::enum_glob_use)]
+    use Token::*;
+    #[rustfmt::skip]
+    static DECL_TOKENS: &[Token] = &[
+        // #[clippy::version = "version"]
+        Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
+        // ("first", "second"),
+        OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
+    ];
+    #[rustfmt::skip]
+    static DEPRECATED_TOKENS: &[Token] = &[
+        // !{ DEPRECATED(DEPRECATED_VERSION) = [
+        Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
+    ];
+    #[rustfmt::skip]
+    static RENAMED_TOKENS: &[Token] = &[
+        // !{ RENAMED(RENAMED_VERSION) = [
+        Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
+    ];
+
+    let path = "clippy_lints/src/deprecated_lints.rs";
     let mut res = DeprecatedLints {
-        file: File::open(
-            "clippy_lints/src/deprecated_lints.rs",
-            OpenOptions::new().read(true).write(true),
-        ),
+        file: File::open(path, OpenOptions::new().read(true).write(true)),
         contents: String::new(),
         deprecated: Vec::with_capacity(30),
         renamed: Vec::with_capacity(80),
@@ -373,81 +303,77 @@ pub fn read_deprecated_lints() -> DeprecatedLints {
     };
 
     res.file.read_append_to_string(&mut res.contents);
+    let mut searcher = RustSearcher::new(&res.contents);
 
-    let (_, contents) = res.contents.split_once("\ndeclare_with_version! { DEPRECATED").unwrap();
-    let (deprecated_src, contents) = contents.split_once("\n]}").unwrap();
-    res.deprecated_end = (res.contents.len() - contents.len() - 2) as u32;
-
-    let (_, contents) = contents.split_once("\ndeclare_with_version! { RENAMED").unwrap();
-    let (renamed_src, contents) = contents.split_once("\n]}").unwrap();
-    res.renamed_end = (res.contents.len() - contents.len() - 2) as u32;
-
-    for line in deprecated_src.lines() {
-        let mut offset = 0usize;
-        let mut iter = tokenize(line).map(|t| {
-            let range = offset..offset + t.len as usize;
-            offset = range.end;
+    // First instance is the macro definition.
+    assert!(
+        searcher.find_token(Ident("declare_with_version")),
+        "error reading deprecated lints"
+    );
 
-            LintDeclSearchResult {
-                token_kind: t.kind,
-                content: &line[range.clone()],
-                range,
-            }
-        });
-
-        let (name, reason) = match_tokens!(
-            iter,
-            // ("old_name",
-            Whitespace OpenParen Literal{kind: LiteralKind::Str{..},..}(name) Comma
-            // "new_name"),
-            Whitespace Literal{kind: LiteralKind::Str{..},..}(reason) CloseParen Comma
-        );
-        res.deprecated.push(DeprecatedLint::new(name, reason));
+    if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) {
+        let mut name = "";
+        let mut reason = "";
+        while searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut reason]) {
+            res.deprecated.push(DeprecatedLint {
+                name: parse_str_single_line(path.as_ref(), name),
+                reason: parse_str_single_line(path.as_ref(), reason),
+            });
+        }
+    } else {
+        panic!("error reading deprecated lints");
     }
-    for line in renamed_src.lines() {
-        let mut offset = 0usize;
-        let mut iter = tokenize(line).map(|t| {
-            let range = offset..offset + t.len as usize;
-            offset = range.end;
-
-            LintDeclSearchResult {
-                token_kind: t.kind,
-                content: &line[range.clone()],
-                range,
-            }
-        });
-
-        let (old_name, new_name) = match_tokens!(
-            iter,
-            // ("old_name",
-            Whitespace OpenParen Literal{kind: LiteralKind::Str{..},..}(old_name) Comma
-            // "new_name"),
-            Whitespace Literal{kind: LiteralKind::Str{..},..}(new_name) CloseParen Comma
-        );
-        res.renamed.push(RenamedLint::new(old_name, new_name));
+    // position of the closing `]}` of `declare_with_version`
+    res.deprecated_end = searcher.pos();
+
+    if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) {
+        let mut old_name = "";
+        let mut new_name = "";
+        while searcher.match_tokens(DECL_TOKENS, &mut [&mut old_name, &mut new_name]) {
+            res.renamed.push(RenamedLint {
+                old_name: parse_str_single_line(path.as_ref(), old_name),
+                new_name: parse_str_single_line(path.as_ref(), new_name),
+            });
+        }
+    } else {
+        panic!("error reading renamed lints");
     }
+    // position of the closing `]}` of `declare_with_version`
+    res.renamed_end = searcher.pos();
 
     res
 }
 
 /// Removes the line splices and surrounding quotes from a string literal
-fn remove_line_splices(s: &str) -> String {
+fn parse_str_lit(s: &str) -> String {
+    let (s, mode) = if let Some(s) = s.strip_prefix("r") {
+        (s.trim_matches('#'), rustc_literal_escaper::Mode::RawStr)
+    } else {
+        (s, rustc_literal_escaper::Mode::Str)
+    };
     let s = s
-        .strip_prefix('r')
-        .unwrap_or(s)
-        .trim_matches('#')
         .strip_prefix('"')
         .and_then(|s| s.strip_suffix('"'))
         .unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
     let mut res = String::with_capacity(s.len());
-    unescape_unicode(s, Mode::Str, &mut |range, ch| {
-        if ch.is_ok() {
-            res.push_str(&s[range]);
+    rustc_literal_escaper::unescape_unicode(s, mode, &mut |_, ch| {
+        if let Ok(ch) = ch {
+            res.push(ch);
         }
     });
     res
 }
 
+fn parse_str_single_line(path: &Path, s: &str) -> String {
+    let value = parse_str_lit(s);
+    assert!(
+        !value.contains('\n'),
+        "error parsing `{}`: `{s}` should be a single line string",
+        path.display(),
+    );
+    value
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -471,7 +397,7 @@ mod tests {
             }
         "#;
         let mut result = Vec::new();
-        parse_clippy_lint_decls(CONTENTS, "module_name", &mut result);
+        parse_clippy_lint_decls("".as_ref(), CONTENTS, "module_name", &mut result);
         for r in &mut result {
             r.declaration_range = Range::default();
         }
diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs
index 87645aff674..1fb338d39cc 100644
--- a/clippy_dev/src/utils.rs
+++ b/clippy_dev/src/utils.rs
@@ -1,6 +1,8 @@
 use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
 use core::fmt::{self, Display};
+use core::slice;
 use core::str::FromStr;
+use rustc_lexer as lexer;
 use std::env;
 use std::fs::{self, OpenOptions};
 use std::io::{self, Read as _, Seek as _, SeekFrom, Write};
@@ -406,6 +408,199 @@ impl<'a> StringReplacer<'a> {
     }
 }
 
+#[derive(Clone, Copy)]
+pub enum Token {
+    /// Matches any number of doc comments.
+    AnyDoc,
+    Ident(&'static str),
+    CaptureIdent,
+    LitStr,
+    CaptureLitStr,
+    Bang,
+    CloseBrace,
+    CloseBracket,
+    CloseParen,
+    /// This will consume the first colon even if the second doesn't exist.
+    DoubleColon,
+    Comma,
+    Eq,
+    Lifetime,
+    Lt,
+    Gt,
+    OpenBrace,
+    OpenBracket,
+    OpenParen,
+    Pound,
+}
+
+pub struct RustSearcher<'txt> {
+    text: &'txt str,
+    cursor: lexer::Cursor<'txt>,
+    pos: u32,
+
+    // Either the next token or a zero-sized whitespace sentinel.
+    next_token: lexer::Token,
+}
+impl<'txt> RustSearcher<'txt> {
+    #[must_use]
+    pub fn new(text: &'txt str) -> Self {
+        Self {
+            text,
+            cursor: lexer::Cursor::new(text),
+            pos: 0,
+
+            // Sentinel value indicating there is no read token.
+            next_token: lexer::Token {
+                len: 0,
+                kind: lexer::TokenKind::Whitespace,
+            },
+        }
+    }
+
+    #[must_use]
+    pub fn peek_text(&self) -> &'txt str {
+        &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize]
+    }
+
+    #[must_use]
+    pub fn peek(&self) -> lexer::TokenKind {
+        self.next_token.kind
+    }
+
+    #[must_use]
+    pub fn pos(&self) -> u32 {
+        self.pos
+    }
+
+    #[must_use]
+    pub fn at_end(&self) -> bool {
+        self.next_token.kind == lexer::TokenKind::Eof
+    }
+
+    pub fn step(&mut self) {
+        // `next_len` is zero for the sentinel value and the eof marker.
+        self.pos += self.next_token.len;
+        self.next_token = self.cursor.advance_token();
+    }
+
+    /// Consumes the next token if it matches the requested value and captures the value if
+    /// requested. Returns true if a token was matched.
+    fn read_token(&mut self, token: Token, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool {
+        loop {
+            match (token, self.next_token.kind) {
+                // Has to be the first match arm so the empty sentinel token will be handled.
+                // This will also skip all whitespace/comments preceding any tokens.
+                (
+                    _,
+                    lexer::TokenKind::Whitespace
+                    | lexer::TokenKind::LineComment { doc_style: None }
+                    | lexer::TokenKind::BlockComment {
+                        doc_style: None,
+                        terminated: true,
+                    },
+                ) => {
+                    self.step();
+                    if self.at_end() {
+                        // `AnyDoc` always matches.
+                        return matches!(token, Token::AnyDoc);
+                    }
+                },
+                (
+                    Token::AnyDoc,
+                    lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. },
+                ) => {
+                    self.step();
+                    if self.at_end() {
+                        // `AnyDoc` always matches.
+                        return true;
+                    }
+                },
+                (Token::AnyDoc, _) => return true,
+                (Token::Bang, lexer::TokenKind::Bang)
+                | (Token::CloseBrace, lexer::TokenKind::CloseBrace)
+                | (Token::CloseBracket, lexer::TokenKind::CloseBracket)
+                | (Token::CloseParen, lexer::TokenKind::CloseParen)
+                | (Token::Comma, lexer::TokenKind::Comma)
+                | (Token::Eq, lexer::TokenKind::Eq)
+                | (Token::Lifetime, lexer::TokenKind::Lifetime { .. })
+                | (Token::Lt, lexer::TokenKind::Lt)
+                | (Token::Gt, lexer::TokenKind::Gt)
+                | (Token::OpenBrace, lexer::TokenKind::OpenBrace)
+                | (Token::OpenBracket, lexer::TokenKind::OpenBracket)
+                | (Token::OpenParen, lexer::TokenKind::OpenParen)
+                | (Token::Pound, lexer::TokenKind::Pound)
+                | (
+                    Token::LitStr,
+                    lexer::TokenKind::Literal {
+                        kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
+                        ..
+                    },
+                ) => {
+                    self.step();
+                    return true;
+                },
+                (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => {
+                    self.step();
+                    return true;
+                },
+                (Token::DoubleColon, lexer::TokenKind::Colon) => {
+                    self.step();
+                    if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) {
+                        self.step();
+                        return true;
+                    }
+                    return false;
+                },
+                (
+                    Token::CaptureLitStr,
+                    lexer::TokenKind::Literal {
+                        kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
+                        ..
+                    },
+                )
+                | (Token::CaptureIdent, lexer::TokenKind::Ident) => {
+                    **captures.next().unwrap() = self.peek_text();
+                    self.step();
+                    return true;
+                },
+                _ => return false,
+            }
+        }
+    }
+
+    #[must_use]
+    pub fn find_token(&mut self, token: Token) -> bool {
+        let mut capture = [].iter_mut();
+        while !self.read_token(token, &mut capture) {
+            self.step();
+            if self.at_end() {
+                return false;
+            }
+        }
+        true
+    }
+
+    #[must_use]
+    pub fn find_capture_token(&mut self, token: Token) -> Option<&'txt str> {
+        let mut res = "";
+        let mut capture = &mut res;
+        let mut capture = slice::from_mut(&mut capture).iter_mut();
+        while !self.read_token(token, &mut capture) {
+            self.step();
+            if self.at_end() {
+                return None;
+            }
+        }
+        Some(res)
+    }
+
+    #[must_use]
+    pub fn match_tokens(&mut self, tokens: &[Token], captures: &mut [&mut &'txt str]) -> bool {
+        let mut captures = captures.iter_mut();
+        tokens.iter().all(|&t| self.read_token(t, &mut captures))
+    }
+}
+
 #[expect(clippy::must_use_candidate)]
 pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
     match OpenOptions::new().create_new(true).write(true).open(new_name) {
diff --git a/clippy_lints/src/deprecated_lints.rs b/clippy_lints/src/deprecated_lints.rs
index d37b0efd35d..94651538669 100644
--- a/clippy_lints/src/deprecated_lints.rs
+++ b/clippy_lints/src/deprecated_lints.rs
@@ -2,18 +2,18 @@
 // Prefer to use those when possible.
 
 macro_rules! declare_with_version {
-    ($name:ident($name_version:ident): &[$ty:ty] = &[$(
+    ($name:ident($name_version:ident) = [$(
         #[clippy::version = $version:literal]
         $e:expr,
     )*]) => {
-        pub static $name: &[$ty] = &[$($e),*];
+        pub static $name: &[(&str, &str)] = &[$($e),*];
         #[allow(unused)]
         pub static $name_version: &[&str] = &[$($version),*];
     };
 }
 
 #[rustfmt::skip]
-declare_with_version! { DEPRECATED(DEPRECATED_VERSION): &[(&str, &str)] = &[
+declare_with_version! { DEPRECATED(DEPRECATED_VERSION) = [
     #[clippy::version = "pre 1.29.0"]
     ("clippy::should_assert_eq", "`assert!(a == b)` can now print the values the same way `assert_eq!(a, b) can"),
     #[clippy::version = "pre 1.29.0"]
@@ -47,7 +47,7 @@ declare_with_version! { DEPRECATED(DEPRECATED_VERSION): &[(&str, &str)] = &[
 ]}
 
 #[rustfmt::skip]
-declare_with_version! { RENAMED(RENAMED_VERSION): &[(&str, &str)] = &[
+declare_with_version! { RENAMED(RENAMED_VERSION) = [
     #[clippy::version = ""]
     ("clippy::almost_complete_letter_range", "clippy::almost_complete_range"),
     #[clippy::version = ""]