use std::fs; use std::fs::File; use std::io::prelude::*; use std::path::{Path, PathBuf}; // This module takes an absolute path to a rustc repo and alters the dependencies to point towards // the respective rustc subcrates instead of using extern crate xyz. // This allows IntelliJ to analyze rustc internals and show proper information inside Clippy // code. See https://github.com/rust-lang/rust-clippy/issues/5514 for details const RUSTC_PATH_SECTION: &str = "[target.'cfg(NOT_A_PLATFORM)'.dependencies]"; const DEPENDENCIES_SECTION: &str = "[dependencies]"; const CLIPPY_PROJECTS: &[ClippyProjectInfo] = &[ ClippyProjectInfo::new("root", "Cargo.toml", "src/driver.rs"), ClippyProjectInfo::new("clippy_lints", "clippy_lints/Cargo.toml", "clippy_lints/src/lib.rs"), ClippyProjectInfo::new("clippy_utils", "clippy_utils/Cargo.toml", "clippy_utils/src/lib.rs"), ]; /// Used to store clippy project information to later inject the dependency into. struct ClippyProjectInfo { /// Only used to display information to the user name: &'static str, cargo_file: &'static str, lib_rs_file: &'static str, } impl ClippyProjectInfo { const fn new(name: &'static str, cargo_file: &'static str, lib_rs_file: &'static str) -> Self { Self { name, cargo_file, lib_rs_file, } } } pub fn setup_rustc_src(rustc_path: &str) { let Ok(rustc_source_dir) = check_and_get_rustc_dir(rustc_path) else { return; }; for project in CLIPPY_PROJECTS { if inject_deps_into_project(&rustc_source_dir, project).is_err() { return; } } println!("info: the source paths can be removed again with `cargo dev remove intellij`"); } fn check_and_get_rustc_dir(rustc_path: &str) -> Result { let mut path = PathBuf::from(rustc_path); if path.is_relative() { match path.canonicalize() { Ok(absolute_path) => { println!("info: the rustc path was resolved to: `{}`", absolute_path.display()); path = absolute_path; }, Err(err) => { eprintln!("error: unable to get the absolute path of rustc ({err})"); return Err(()); }, }; } let path = path.join("compiler"); println!("info: looking for compiler sources at: {}", path.display()); if !path.exists() { eprintln!("error: the given path does not exist"); return Err(()); } if !path.is_dir() { eprintln!("error: the given path is not a directory"); return Err(()); } Ok(path) } fn inject_deps_into_project(rustc_source_dir: &Path, project: &ClippyProjectInfo) -> Result<(), ()> { let cargo_content = read_project_file(project.cargo_file)?; let lib_content = read_project_file(project.lib_rs_file)?; if inject_deps_into_manifest(rustc_source_dir, project.cargo_file, &cargo_content, &lib_content).is_err() { eprintln!( "error: unable to inject dependencies into {} with the Cargo file {}", project.name, project.cargo_file ); Err(()) } else { Ok(()) } } /// `clippy_dev` expects to be executed in the root directory of Clippy. This function /// loads the given file or returns an error. Having it in this extra function ensures /// that the error message looks nice. fn read_project_file(file_path: &str) -> Result { let path = Path::new(file_path); if !path.exists() { eprintln!("error: unable to find the file `{file_path}`"); return Err(()); } match fs::read_to_string(path) { Ok(content) => Ok(content), Err(err) => { eprintln!("error: the file `{file_path}` could not be read ({err})"); Err(()) }, } } fn inject_deps_into_manifest( rustc_source_dir: &Path, manifest_path: &str, cargo_toml: &str, lib_rs: &str, ) -> std::io::Result<()> { // do not inject deps if we have already done so if cargo_toml.contains(RUSTC_PATH_SECTION) { eprintln!("warn: dependencies are already setup inside {manifest_path}, skipping file"); return Ok(()); } let extern_crates = lib_rs .lines() // only take dependencies starting with `rustc_` .filter(|line| line.starts_with("extern crate rustc_")) // we have something like "extern crate foo;", we only care about the "foo" // extern crate rustc_middle; // ^^^^^^^^^^^^ .map(|s| &s[13..(s.len() - 1)]); let new_deps = extern_crates.map(|dep| { // format the dependencies that are going to be put inside the Cargo.toml format!("{dep} = {{ path = \"{}/{dep}\" }}\n", rustc_source_dir.display()) }); // format a new [dependencies]-block with the new deps we need to inject let mut all_deps = String::from("[target.'cfg(NOT_A_PLATFORM)'.dependencies]\n"); new_deps.for_each(|dep_line| { all_deps.push_str(&dep_line); }); all_deps.push_str("\n[dependencies]\n"); // replace "[dependencies]" with // [dependencies] // dep1 = { path = ... } // dep2 = { path = ... } // etc let new_manifest = cargo_toml.replacen("[dependencies]\n", &all_deps, 1); // println!("{new_manifest}"); let mut file = File::create(manifest_path)?; file.write_all(new_manifest.as_bytes())?; println!("info: successfully setup dependencies inside {manifest_path}"); Ok(()) } pub fn remove_rustc_src() { for project in CLIPPY_PROJECTS { remove_rustc_src_from_project(project); } } fn remove_rustc_src_from_project(project: &ClippyProjectInfo) -> bool { let Ok(mut cargo_content) = read_project_file(project.cargo_file) else { return false; }; let Some(section_start) = cargo_content.find(RUSTC_PATH_SECTION) else { println!( "info: dependencies could not be found in `{}` for {}, skipping file", project.cargo_file, project.name ); return true; }; let Some(end_point) = cargo_content.find(DEPENDENCIES_SECTION) else { eprintln!( "error: the end of the rustc dependencies section could not be found in `{}`", project.cargo_file ); return false; }; cargo_content.replace_range(section_start..end_point, ""); match File::create(project.cargo_file) { Ok(mut file) => { file.write_all(cargo_content.as_bytes()).unwrap(); println!("info: successfully removed dependencies inside {}", project.cargo_file); true }, Err(err) => { eprintln!( "error: unable to open file `{}` to remove rustc dependencies for {} ({err})", project.cargo_file, project.name ); false }, } }