src/tools/clippy/clippy_dev/src/new_lint.rs RUST 563 lines View on github.com → Search inside
1use crate::parse::cursor::{self, Capture, Cursor};2use crate::utils::Version;3use clap::ValueEnum;4use indoc::{formatdoc, writedoc};5use std::fmt::{self, Write as _};6use std::fs::{self, OpenOptions};7use std::io::{self, Write as _};8use std::path::{Path, PathBuf};910#[derive(Clone, Copy, PartialEq, ValueEnum)]11pub enum Pass {12    Early,13    Late,14}1516impl fmt::Display for Pass {17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {18        f.write_str(match self {19            Pass::Early => "early",20            Pass::Late => "late",21        })22    }23}2425struct LintData<'a> {26    clippy_version: Version,27    pass: Pass,28    name: &'a str,29    category: &'a str,30    ty: Option<&'a str>,31}3233trait Context {34    fn context<C: AsRef<str>>(self, text: C) -> Self;35}3637impl<T> Context for io::Result<T> {38    fn context<C: AsRef<str>>(self, text: C) -> Self {39        match self {40            Ok(t) => Ok(t),41            Err(e) => {42                let message = format!("{}: {e}", text.as_ref());43                Err(io::Error::other(message))44            },45        }46    }47}4849/// Creates the files required to implement and test a new lint and runs `update_lints`.50///51/// # Errors52///53/// This function errors out if the files couldn't be created or written to.54pub fn create(55    clippy_version: Version,56    pass: Pass,57    name: &str,58    category: &str,59    mut ty: Option<&str>,60    msrv: bool,61) -> io::Result<()> {62    if category == "cargo" && ty.is_none() {63        // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`64        ty = Some("cargo");65    }6667    let lint = LintData {68        clippy_version,69        pass,70        name,71        category,72        ty,73    };7475    create_lint(&lint, msrv).context("Unable to create lint implementation")?;76    create_test(&lint, msrv).context("Unable to create a test for the new lint")?;7778    if lint.ty.is_none() {79        add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;80    }8182    if pass == Pass::Early {83        println!(84            "\n\85            NOTE: Use a late pass unless you need something specific from\n\86            an early pass, as they lack many features and utilities"87        );88    }8990    Ok(())91}9293fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {94    if let Some(ty) = lint.ty {95        create_lint_for_ty(lint, enable_msrv, ty)96    } else {97        let lint_contents = get_lint_file_contents(lint, enable_msrv);98        let lint_path = format!("clippy_lints/src/{}.rs", lint.name);99        write_file(&lint_path, lint_contents.as_bytes())?;100        println!("Generated lint file: `{lint_path}`");101102        Ok(())103    }104}105106fn create_test(lint: &LintData<'_>, msrv: bool) -> io::Result<()> {107    fn create_project_layout<P: Into<PathBuf>>(108        lint_name: &str,109        location: P,110        case: &str,111        hint: &str,112        msrv: bool,113    ) -> io::Result<()> {114        let mut path = location.into().join(case);115        fs::create_dir(&path)?;116        write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;117118        path.push("src");119        fs::create_dir(&path)?;120        write_file(path.join("main.rs"), get_test_file_contents(lint_name, msrv))?;121122        Ok(())123    }124125    if lint.category == "cargo" {126        let test_dir = format!("tests/ui-cargo/{}", lint.name);127        fs::create_dir(&test_dir)?;128129        create_project_layout(130            lint.name,131            &test_dir,132            "fail",133            "Content that triggers the lint goes here",134            msrv,135        )?;136        create_project_layout(137            lint.name,138            &test_dir,139            "pass",140            "This file should not trigger the lint",141            false,142        )?;143144        println!("Generated test directories: `{test_dir}/pass`, `{test_dir}/fail`");145    } else {146        let test_path = format!("tests/ui/{}.rs", lint.name);147        let test_contents = get_test_file_contents(lint.name, msrv);148        write_file(&test_path, test_contents)?;149150        println!("Generated test file: `{test_path}`");151    }152153    Ok(())154}155156fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {157    let path = "clippy_lints/src/lib.rs";158    let mut lib_rs = fs::read_to_string(path).context("reading")?;159160    let (comment, ctor_arg) = if lint.pass == Pass::Late {161        ("// add late passes here", "_")162    } else {163        ("// add early passes here", "")164    };165    let comment_start = lib_rs.find(comment).expect("Couldn't find comment");166    let module_name = lint.name;167    let camel_name = to_camel_case(lint.name);168169    let new_lint = if enable_msrv {170        format!("Box::new(move |{ctor_arg}| Box::new({module_name}::{camel_name}::new(conf))),\n        ")171    } else {172        format!("Box::new(|{ctor_arg}| Box::new({module_name}::{camel_name})),\n        ")173    };174175    lib_rs.insert_str(comment_start, &new_lint);176177    fs::write(path, lib_rs).context("writing")178}179180fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {181    fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {182        OpenOptions::new()183            .write(true)184            .create_new(true)185            .open(path)?186            .write_all(contents)187    }188189    inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))190}191192fn to_camel_case(name: &str) -> String {193    name.split('_')194        .map(|s| {195            if s.is_empty() {196                String::new()197            } else {198                [&s[0..1].to_uppercase(), &s[1..]].concat()199            }200        })201        .collect()202}203204fn get_test_file_contents(lint_name: &str, msrv: bool) -> String {205    let mut test = formatdoc!(206        r"207        #![warn(clippy::{lint_name})]208209        fn main() {{210            // test code goes here211        }}212    "213    );214215    if msrv {216        let _ = writedoc!(217            test,218            r#"219220                // TODO: set xx to the version one below the MSRV used by the lint, and yy to221                // the version used by the lint222                #[clippy::msrv = "1.xx"]223                fn msrv_1_xx() {{224                    // a simple example that would trigger the lint if the MSRV were met225                }}226227                #[clippy::msrv = "1.yy"]228                fn msrv_1_yy() {{229                    // the same example as above230                }}231            "#232        );233    }234235    test236}237238fn get_manifest_contents(lint_name: &str, hint: &str) -> String {239    formatdoc!(240        r#"241        # {hint}242243        [package]244        name = "{lint_name}"245        version = "0.1.0"246        publish = false247248        [workspace]249    "#250    )251}252253fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {254    let mut result = String::new();255256    let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {257        Pass::Early => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),258        Pass::Late => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),259    };260    let (msrv_ty, msrv_ctor, extract_msrv) = match lint.pass {261        Pass::Early => (262            "MsrvStack",263            "MsrvStack::new(conf.msrv)",264            "\n    extract_msrv_attr!();\n",265        ),266        Pass::Late => ("Msrv", "conf.msrv", ""),267    };268269    let lint_name = lint.name;270    let category = lint.category;271    let name_camel = to_camel_case(lint.name);272    let name_upper = lint_name.to_uppercase();273274    if enable_msrv {275        let _: fmt::Result = writedoc!(276            result,277            r"278            use clippy_config::Conf;279            use clippy_utils::msrvs::{{self, {msrv_ty}}};280            {pass_import}281            use rustc_lint::{{{context_import}, {pass_type}}};282            use rustc_session::impl_lint_pass;283284        "285        );286    } else {287        let _: fmt::Result = writedoc!(288            result,289            r"290            {pass_import}291            use rustc_lint::{{{context_import}, {pass_type}}};292            use rustc_session::declare_lint_pass;293294        "295        );296    }297298    let _: fmt::Result = writeln!(299        result,300        "{}",301        get_lint_declaration(lint.clippy_version, &name_upper, category)302    );303304    if enable_msrv {305        let _: fmt::Result = writedoc!(306            result,307            r"308            pub struct {name_camel} {{309                msrv: {msrv_ty},310            }}311312            impl {name_camel} {{313                pub fn new(conf: &'static Conf) -> Self {{314                    Self {{ msrv: {msrv_ctor} }}315                }}316            }}317318            impl_lint_pass!({name_camel} => [{name_upper}]);319320            impl {pass_type}{pass_lifetimes} for {name_camel} {{{extract_msrv}}}321322            // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.323            // TODO: Update msrv config comment in `clippy_config/src/conf.rs`324        "325        );326    } else {327        let _: fmt::Result = writedoc!(328            result,329            r"330            declare_lint_pass!({name_camel} => [{name_upper}]);331332            impl {pass_type}{pass_lifetimes} for {name_camel} {{}}333        "334        );335    }336337    result338}339340fn get_lint_declaration(version: Version, name_upper: &str, category: &str) -> String {341    let justification_heading = if category == "restriction" {342        "Why restrict this?"343    } else {344        "Why is this bad?"345    };346    formatdoc!(347        r#"348            declare_clippy_lint! {{349                /// ### What it does350                ///351                /// ### {justification_heading}352                ///353                /// ### Example354                /// ```no_run355                /// // example code where clippy issues a warning356                /// ```357                /// Use instead:358                /// ```no_run359                /// // example code which does not raise clippy warning360                /// ```361                #[clippy::version = "{}"]362                pub {name_upper},363                {category},364                "default lint description"365            }}"#,366        version.rust_display(),367    )368}369370fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {371    match ty {372        "cargo" => assert_eq!(373            lint.category, "cargo",374            "Lints of type `cargo` must have the `cargo` category"375        ),376        _ if lint.category == "cargo" => panic!("Lints of category `cargo` must have the `cargo` type"),377        _ => {},378    }379380    let ty_dir = PathBuf::from(format!("clippy_lints/src/{ty}"));381    assert!(382        ty_dir.exists() && ty_dir.is_dir(),383        "Directory `{}` does not exist!",384        ty_dir.display()385    );386387    let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));388    assert!(389        !lint_file_path.exists(),390        "File `{}` already exists",391        lint_file_path.display()392    );393394    let mod_file_path = ty_dir.join("mod.rs");395    let context_import = setup_mod_file(&mod_file_path, lint)?;396    let (pass_lifetimes, msrv_ty, msrv_ref, msrv_cx) = match context_import {397        "LateContext" => ("<'_>", "Msrv", "", "cx, "),398        _ => ("", "MsrvStack", "&", ""),399    };400401    let name_upper = lint.name.to_uppercase();402    let mut lint_file_contents = String::new();403404    if enable_msrv {405        let _: fmt::Result = writedoc!(406            lint_file_contents,407            r#"408                use clippy_utils::msrvs::{{self, {msrv_ty}}};409                use rustc_lint::{{{context_import}, LintContext}};410411                use super::{name_upper};412413                // TODO: Adjust the parameters as necessary414                pub(super) fn check(cx: &{context_import}{pass_lifetimes}, msrv: {msrv_ref}{msrv_ty}) {{415                    if !msrv.meets({msrv_cx}todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{416                        return;417                    }}418                    todo!();419                }}420           "#421        );422    } else {423        let _: fmt::Result = writedoc!(424            lint_file_contents,425            r"426                use rustc_lint::{{{context_import}, LintContext}};427428                use super::{name_upper};429430                // TODO: Adjust the parameters as necessary431                pub(super) fn check(cx: &{context_import}{pass_lifetimes}) {{432                    todo!();433                }}434           "435        );436    }437438    write_file(lint_file_path.as_path(), lint_file_contents)?;439    println!("Generated lint file: `clippy_lints/src/{ty}/{}.rs`", lint.name);440    println!(441        "Be sure to add a call to `{}::check` in `clippy_lints/src/{ty}/mod.rs`!",442        lint.name443    );444445    Ok(())446}447448fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {449    let lint_name_upper = lint.name.to_uppercase();450451    let mut file_contents = fs::read_to_string(path)?;452    assert!(453        !file_contents.contains(&format!("pub {lint_name_upper},")),454        "Lint `{}` already defined in `{}`",455        lint.name,456        path.display()457    );458459    let (lint_context, lint_decl_end) = parse_mod_file(path, &file_contents);460461    // Add the lint declaration to `mod.rs`462    file_contents.insert_str(463        lint_decl_end,464        &format!(465            "\n\n{}",466            get_lint_declaration(lint.clippy_version, &lint_name_upper, lint.category)467        ),468    );469470    // Add the lint to `impl_lint_pass`/`declare_lint_pass`471    let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| {472        file_contents473            .find("declare_lint_pass!")474            .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))475    });476477    let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {478        panic!("malformed `impl_lint_pass`/`declare_lint_pass`");479    });480481    arr_start += impl_lint_pass_start;482483    let mut arr_end = file_contents[arr_start..]484        .find(']')485        .expect("failed to find `impl_lint_pass` terminator");486487    arr_end += arr_start;488489    let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();490    arr_content.retain(|c| !c.is_whitespace());491492    let mut new_arr_content = String::new();493    for ident in arr_content494        .split(',')495        .chain(std::iter::once(&*lint_name_upper))496        .filter(|s| !s.is_empty())497    {498        let _: fmt::Result = write!(new_arr_content, "\n    {ident},");499    }500    new_arr_content.push('\n');501502    file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);503504    // Just add the mod declaration at the top, it'll be fixed by rustfmt505    file_contents.insert_str(0, &format!("mod {};\n", lint.name));506507    let mut file = OpenOptions::new()508        .write(true)509        .truncate(true)510        .open(path)511        .context(format!("trying to open: `{}`", path.display()))?;512    file.write_all(file_contents.as_bytes())513        .context(format!("writing to file: `{}`", path.display()))?;514515    Ok(lint_context)516}517518// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl519fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) {520    #[allow(clippy::enum_glob_use)]521    use cursor::Pat::*;522523    let mut context = None;524    let mut decl_end = None;525    let mut cursor = Cursor::new(contents);526    let mut captures = [Capture::EMPTY];527    while let Some(name) = cursor.find_any_ident() {528        match cursor.get_text(name) {529            "declare_clippy_lint" if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_pat(CloseBrace) => {530                decl_end = Some(cursor.pos());531            },532            "impl" if cursor.match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) => {533                match cursor.get_text(captures[0]) {534                    "LateLintPass" => context = Some("LateContext"),535                    "EarlyLintPass" => context = Some("EarlyContext"),536                    _ => {},537                }538            },539            _ => {},540        }541    }542543    (544        context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display())),545        decl_end.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display())) as usize,546    )547}548549#[test]550fn test_camel_case() {551    let s = "a_lint";552    let s2 = to_camel_case(s);553    assert_eq!(s2, "ALint");554555    let name = "a_really_long_new_lint";556    let name2 = to_camel_case(name);557    assert_eq!(name2, "AReallyLongNewLint");558559    let name3 = "lint__name";560    let name4 = to_camel_case(name3);561    assert_eq!(name4, "LintName");562}

Code quality findings 26

Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
let comment_start = lib_rs.find(comment).expect("Couldn't find comment");
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
[&s[0..1].to_uppercase(), &s[1..]].concat()
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let mut arr_end = file_contents[arr_start..]
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
.expect("failed to find `impl_lint_pass` terminator");
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
"declare_clippy_lint" if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_pat(CloseBrace) => {
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
match cursor.get_text(captures[0]) {
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!(
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!("Generated lint file: `{lint_path}`");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!("Generated test directories: `{test_dir}/pass`, `{test_dir}/fail`");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!("Generated test file: `{test_path}`");
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info correctness unhandled-result
fs::write(path, lib_rs).context("writing")
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match ty {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
let (pass_lifetimes, msrv_ty, msrv_ref, msrv_cx) = match context_import {
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
if !msrv.meets({msrv_cx}todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
todo!();
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
todo!();
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!("Generated lint file: `clippy_lints/src/{ty}/{}.rs`", lint.name);
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!(
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info performance to-string-in-loop
let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
new_arr_content.push('\n');
Info: Usage of `#[allow(...)]` suppresses compiler lints. Ensure the allowance is justified, well-scoped, and ideally temporary. Overuse can hide potential issues.
info maintainability allow-lint
#[allow(clippy::enum_glob_use)]
Info: Wildcard imports (`use some::path::*;`) can obscure the origin of names and lead to conflicts. Prefer importing specific items explicitly.
info maintainability wildcard-import
use cursor::Pat::*;
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match cursor.get_text(name) {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match cursor.get_text(captures[0]) {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.