]> git.lizzy.rs Git - rust.git/blobdiff - clippy_dev/src/new_lint.rs
rustc: Parameterize `ty::Visibility` over used ID
[rust.git] / clippy_dev / src / new_lint.rs
index a3ccb2758e258a79fc166902b496b502bcc1e212..be05e67d724dfc120acfec0d1385dda0cd3df741 100644 (file)
-use clippy_dev::clippy_project_root;
-use std::fs::{File, OpenOptions};
-use std::io;
+use crate::clippy_project_root;
+use indoc::{indoc, writedoc};
+use std::fmt::Write as _;
+use std::fs::{self, OpenOptions};
 use std::io::prelude::*;
-use std::io::ErrorKind;
-use std::path::Path;
-
-pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> Result<(), io::Error> {
-    let pass = pass.expect("`pass` argument is validated by clap");
-    let lint_name = lint_name.expect("`name` argument is validated by clap");
-    let category = category.expect("`category` argument is validated by clap");
-
-    match open_files(lint_name) {
-        Ok((mut test_file, mut lint_file)) => {
-            let (pass_type, pass_lifetimes, pass_import, context_import) = match pass {
-                "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
-                "late" => ("LateLintPass", "<'_, '_>", "use rustc_hir::*;", "LateContext"),
-                _ => {
-                    unreachable!("`pass_type` should only ever be `early` or `late`!");
-                },
-            };
-
-            let camel_case_name = to_camel_case(lint_name);
-
-            if let Err(e) = test_file.write_all(get_test_file_contents(lint_name).as_bytes()) {
-                return Err(io::Error::new(
-                    ErrorKind::Other,
-                    format!("Could not write to test file: {}", e),
-                ));
-            };
-
-            if let Err(e) = lint_file.write_all(
-                get_lint_file_contents(
-                    pass_type,
-                    pass_lifetimes,
-                    lint_name,
-                    &camel_case_name,
-                    category,
-                    pass_import,
-                    context_import,
-                )
-                .as_bytes(),
-            ) {
-                return Err(io::Error::new(
-                    ErrorKind::Other,
-                    format!("Could not write to lint file: {}", e),
-                ));
-            }
-            Ok(())
-        },
-        Err(e) => Err(io::Error::new(
-            ErrorKind::Other,
-            format!("Unable to create lint: {}", e),
-        )),
+use std::io::{self, ErrorKind};
+use std::path::{Path, PathBuf};
+
+struct LintData<'a> {
+    pass: &'a str,
+    name: &'a str,
+    category: &'a str,
+    ty: Option<&'a str>,
+    project_root: PathBuf,
+}
+
+trait Context {
+    fn context<C: AsRef<str>>(self, text: C) -> Self;
+}
+
+impl<T> Context for io::Result<T> {
+    fn context<C: AsRef<str>>(self, text: C) -> Self {
+        match self {
+            Ok(t) => Ok(t),
+            Err(e) => {
+                let message = format!("{}: {}", text.as_ref(), e);
+                Err(io::Error::new(ErrorKind::Other, message))
+            },
+        }
     }
 }
 
-fn open_files(lint_name: &str) -> Result<(File, File), io::Error> {
-    let project_root = clippy_project_root();
+/// 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(
+    pass: Option<&String>,
+    lint_name: Option<&String>,
+    category: Option<&str>,
+    mut ty: Option<&str>,
+    msrv: bool,
+) -> io::Result<()> {
+    if category == Some("cargo") && ty.is_none() {
+        // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`
+        ty = Some("cargo");
+    }
+
+    let lint = LintData {
+        pass: pass.map_or("", String::as_str),
+        name: lint_name.expect("`name` argument is validated by clap"),
+        category: category.expect("`category` argument is validated by clap"),
+        ty,
+        project_root: clippy_project_root(),
+    };
+
+    create_lint(&lint, msrv).context("Unable to create lint implementation")?;
+    create_test(&lint).context("Unable to create a test for the new lint")?;
+
+    if lint.ty.is_none() {
+        add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;
+    }
+
+    Ok(())
+}
 
-    let test_file_path = project_root.join("tests").join("ui").join(format!("{}.rs", lint_name));
-    let lint_file_path = project_root
-        .join("clippy_lints")
-        .join("src")
-        .join(format!("{}.rs", lint_name));
+fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
+    if let Some(ty) = lint.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.project_root.join(&lint_path), lint_contents.as_bytes())?;
+        println!("Generated lint file: `{}`", lint_path);
 
-    if Path::new(&test_file_path).exists() {
-        return Err(io::Error::new(
-            ErrorKind::AlreadyExists,
-            format!("test file {:?} already exists", test_file_path),
-        ));
+        Ok(())
     }
-    if Path::new(&lint_file_path).exists() {
-        return Err(io::Error::new(
-            ErrorKind::AlreadyExists,
-            format!("lint file {:?} already exists", lint_file_path),
-        ));
+}
+
+fn create_test(lint: &LintData<'_>) -> io::Result<()> {
+    fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
+        let mut path = location.into().join(case);
+        fs::create_dir(&path)?;
+        write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
+
+        path.push("src");
+        fs::create_dir(&path)?;
+        let header = format!("// compile-flags: --crate-name={}", lint_name);
+        write_file(path.join("main.rs"), get_test_file_contents(lint_name, Some(&header)))?;
+
+        Ok(())
     }
 
-    let test_file = OpenOptions::new().write(true).create_new(true).open(test_file_path)?;
-    let lint_file = OpenOptions::new().write(true).create_new(true).open(lint_file_path)?;
+    if lint.category == "cargo" {
+        let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
+        let test_dir = lint.project_root.join(&relative_test_dir);
+        fs::create_dir(&test_dir)?;
+
+        create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
+        create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")?;
+
+        println!("Generated test directories: `{relative_test_dir}/pass`, `{relative_test_dir}/fail`");
+    } else {
+        let test_path = format!("tests/ui/{}.rs", lint.name);
+        let test_contents = get_test_file_contents(lint.name, None);
+        write_file(lint.project_root.join(&test_path), test_contents)?;
 
-    Ok((test_file, lint_file))
+        println!("Generated test file: `{}`", test_path);
+    }
+
+    Ok(())
+}
+
+fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
+    let path = "clippy_lints/src/lib.rs";
+    let mut lib_rs = fs::read_to_string(path).context("reading")?;
+
+    let comment_start = lib_rs.find("// add lints here,").expect("Couldn't find comment");
+
+    let new_lint = if enable_msrv {
+        format!(
+            "store.register_{lint_pass}_pass(move || Box::new({module_name}::{camel_name}::new(msrv)));\n    ",
+            lint_pass = lint.pass,
+            module_name = lint.name,
+            camel_name = to_camel_case(lint.name),
+        )
+    } else {
+        format!(
+            "store.register_{lint_pass}_pass(|| Box::new({module_name}::{camel_name}));\n    ",
+            lint_pass = lint.pass,
+            module_name = lint.name,
+            camel_name = to_camel_case(lint.name),
+        )
+    };
+
+    lib_rs.insert_str(comment_start, &new_lint);
+
+    fs::write(path, lib_rs).context("writing")
+}
+
+fn write_file<P: AsRef<Path>, 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()))
 }
 
 fn to_camel_case(name: &str) -> String {
     name.split('_')
         .map(|s| {
             if s.is_empty() {
-                String::from("")
+                String::new()
             } else {
                 [&s[0..1].to_uppercase(), &s[1..]].concat()
             }
@@ -95,63 +163,402 @@ fn to_camel_case(name: &str) -> String {
         .collect()
 }
 
-fn get_test_file_contents(lint_name: &str) -> String {
-    format!(
-        "#![warn(clippy::{})]
+pub(crate) fn get_stabilization_version() -> String {
+    fn parse_manifest(contents: &str) -> Option<String> {
+        let version = contents
+            .lines()
+            .filter_map(|l| l.split_once('='))
+            .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
+        let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
+            return None;
+        };
+        let (minor, patch) = version.split_once('.')?;
+        Some(format!(
+            "{}.{}.0",
+            minor.parse::<u32>().ok()?,
+            patch.parse::<u32>().ok()?
+        ))
+    }
+    let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
+    parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
+}
+
+fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
+    let mut contents = format!(
+        indoc! {"
+            #![warn(clippy::{})]
 
-fn main() {{
-    // test code goes here
-}}
-",
+            fn main() {{
+                // test code goes here
+            }}
+        "},
         lint_name
+    );
+
+    if let Some(header) = header_commands {
+        contents = format!("{}\n{}", header, contents);
+    }
+
+    contents
+}
+
+fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
+    format!(
+        indoc! {r#"
+            # {}
+
+            [package]
+            name = "{}"
+            version = "0.1.0"
+            publish = false
+
+            [workspace]
+        "#},
+        hint, lint_name
     )
 }
 
-fn get_lint_file_contents(
-    pass_type: &str,
-    pass_lifetimes: &str,
-    lint_name: &str,
-    camel_case_name: &str,
-    category: &str,
-    pass_import: &str,
-    context_import: &str,
-) -> String {
+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 {
+        "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
+        "late" => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),
+        _ => {
+            unreachable!("`pass_type` should only ever be `early` or `late`!");
+        },
+    };
+
+    let lint_name = lint.name;
+    let category = lint.category;
+    let name_camel = to_camel_case(lint.name);
+    let name_upper = lint_name.to_uppercase();
+
+    result.push_str(&if enable_msrv {
+        format!(
+            indoc! {"
+                use clippy_utils::msrvs;
+                {pass_import}
+                use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
+                use rustc_semver::RustcVersion;
+                use rustc_session::{{declare_tool_lint, impl_lint_pass}};
+
+            "},
+            pass_type = pass_type,
+            pass_import = pass_import,
+            context_import = context_import,
+        )
+    } else {
+        format!(
+            indoc! {"
+                {pass_import}
+                use rustc_lint::{{{context_import}, {pass_type}}};
+                use rustc_session::{{declare_lint_pass, declare_tool_lint}};
+
+            "},
+            pass_import = pass_import,
+            pass_type = pass_type,
+            context_import = context_import
+        )
+    });
+
+    let _ = write!(result, "{}", get_lint_declaration(&name_upper, category));
+
+    result.push_str(&if enable_msrv {
+        format!(
+            indoc! {"
+                pub struct {name_camel} {{
+                    msrv: Option<RustcVersion>,
+                }}
+
+                impl {name_camel} {{
+                    #[must_use]
+                    pub fn new(msrv: Option<RustcVersion>) -> Self {{
+                        Self {{ msrv }}
+                    }}
+                }}
+
+                impl_lint_pass!({name_camel} => [{name_upper}]);
+
+                impl {pass_type}{pass_lifetimes} for {name_camel} {{
+                    extract_msrv_attr!({context_import});
+                }}
+
+                // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.
+                // TODO: Add MSRV test to `tests/ui/min_rust_version_attr.rs`.
+                // TODO: Update msrv config comment in `clippy_lints/src/utils/conf.rs`
+            "},
+            pass_type = pass_type,
+            pass_lifetimes = pass_lifetimes,
+            name_upper = name_upper,
+            name_camel = name_camel,
+            context_import = context_import,
+        )
+    } else {
+        format!(
+            indoc! {"
+                declare_lint_pass!({name_camel} => [{name_upper}]);
+
+                impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
+            "},
+            pass_type = pass_type,
+            pass_lifetimes = pass_lifetimes,
+            name_upper = name_upper,
+            name_camel = name_camel,
+        )
+    });
+
+    result
+}
+
+fn get_lint_declaration(name_upper: &str, category: &str) -> String {
     format!(
-        "use rustc_lint::{{{type}, {context_import}}};
-use rustc_session::{{declare_lint_pass, declare_tool_lint}};
-{pass_import}
-
-declare_clippy_lint! {{
-    /// **What it does:**
-    ///
-    /// **Why is this bad?**
-    ///
-    /// **Known problems:** None.
-    ///
-    /// **Example:**
-    ///
-    /// ```rust
-    /// // example code
-    /// ```
-    pub {name_upper},
-    {category},
-    \"default lint description\"
-}}
-
-declare_lint_pass!({name_camel} => [{name_upper}]);
-
-impl {type}{lifetimes} for {name_camel} {{}}
-",
-        type=pass_type,
-        lifetimes=pass_lifetimes,
-        name_upper=lint_name.to_uppercase(),
-        name_camel=camel_case_name,
-        category=category,
-        pass_import=pass_import,
-        context_import=context_import
+        indoc! {r#"
+            declare_clippy_lint! {{
+                /// ### What it does
+                ///
+                /// ### Why is this bad?
+                ///
+                /// ### Example
+                /// ```rust
+                /// // example code where clippy issues a warning
+                /// ```
+                /// Use instead:
+                /// ```rust
+                /// // example code which does not raise clippy warning
+                /// ```
+                #[clippy::version = "{version}"]
+                pub {name_upper},
+                {category},
+                "default lint description"
+            }}
+        "#},
+        version = get_stabilization_version(),
+        name_upper = name_upper,
+        category = category,
     )
 }
 
+fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {
+    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 = lint.project_root.join(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 mod_file_path = ty_dir.join("mod.rs");
+    let context_import = setup_mod_file(&mod_file_path, lint)?;
+
+    let name_upper = lint.name.to_uppercase();
+    let mut lint_file_contents = String::new();
+
+    if enable_msrv {
+        let _ = writedoc!(
+            lint_file_contents,
+            r#"
+                use clippy_utils::{{meets_msrv, msrvs}};
+                use rustc_lint::{{{context_import}, LintContext}};
+                use rustc_semver::RustcVersion;
+
+                use super::{name_upper};
+
+                // TODO: Adjust the parameters as necessary
+                pub(super) fn check(cx: &{context_import}, msrv: Option<RustcVersion>) {{
+                    if !meets_msrv(msrv, todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{
+                        return;
+                    }}
+                    todo!();
+                }}
+           "#,
+            context_import = context_import,
+            name_upper = name_upper,
+        );
+    } else {
+        let _ = 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}) {{
+                    todo!();
+                }}
+           "#,
+            context_import = context_import,
+            name_upper = name_upper,
+        );
+    }
+
+    write_file(lint_file_path.as_path(), lint_file_contents)?;
+    println!("Generated lint file: `clippy_lints/src/{}/{}.rs`", ty, lint.name);
+    println!(
+        "Be sure to add a call to `{}::check` in `clippy_lints/src/{}/mod.rs`!",
+        lint.name, ty
+    );
+
+    Ok(())
+}
+
+#[allow(clippy::too_many_lines)]
+fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
+    use super::update_lints::{match_tokens, LintDeclSearchResult};
+    use rustc_lexer::TokenKind;
+
+    let lint_name_upper = lint.name.to_uppercase();
+
+    let mut file_contents = fs::read_to_string(path)?;
+    assert!(
+        !file_contents.contains(&lint_name_upper),
+        "Lint `{}` already defined in `{}`",
+        lint.name,
+        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()));
+
+    // 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,
+        &format!("\n\n{}", get_lint_declaration(&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 _ = 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));
+
+    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)
+}
+
 #[test]
 fn test_camel_case() {
     let s = "a_lint";