From 8708a261a1e297323060a8c71789e482b2d3725f Mon Sep 17 00:00:00 2001 From: Alex Macleod Date: Sat, 7 May 2022 22:10:56 +0100 Subject: [PATCH] Some lintcheck cleanup --- lintcheck/Cargo.toml | 5 +- lintcheck/lintcheck_crates.toml | 2 +- lintcheck/src/config.rs | 140 ++++++++++ lintcheck/src/main.rs | 458 ++++++++------------------------ 4 files changed, 258 insertions(+), 347 deletions(-) create mode 100644 lintcheck/src/config.rs diff --git a/lintcheck/Cargo.toml b/lintcheck/Cargo.toml index c694037021a..e63f65ce2f7 100644 --- a/lintcheck/Cargo.toml +++ b/lintcheck/Cargo.toml @@ -6,16 +6,15 @@ readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/rust-clippy" categories = ["development-tools"] -edition = "2018" +edition = "2021" publish = false [dependencies] +cargo_metadata = "0.14" clap = "2.33" flate2 = "1.0" -fs_extra = "1.2" rayon = "1.5.1" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" tar = "0.4" toml = "0.5" ureq = "2.2" diff --git a/lintcheck/lintcheck_crates.toml b/lintcheck/lintcheck_crates.toml index dfee28f1a87..4fbae8614ca 100644 --- a/lintcheck/lintcheck_crates.toml +++ b/lintcheck/lintcheck_crates.toml @@ -24,7 +24,7 @@ unicode-xid = {name = "unicode-xid", versions = ['0.2.1']} anyhow = {name = "anyhow", versions = ['1.0.38']} async-trait = {name = "async-trait", versions = ['0.1.42']} cxx = {name = "cxx", versions = ['1.0.32']} -ryu = {name = "ryu", version = ['1.0.5']} +ryu = {name = "ryu", versions = ['1.0.5']} serde_yaml = {name = "serde_yaml", versions = ['0.8.17']} thiserror = {name = "thiserror", versions = ['1.0.24']} # some embark crates, there are other interesting crates but diff --git a/lintcheck/src/config.rs b/lintcheck/src/config.rs new file mode 100644 index 00000000000..de32b484360 --- /dev/null +++ b/lintcheck/src/config.rs @@ -0,0 +1,140 @@ +use clap::{App, Arg, ArgMatches}; +use std::env; +use std::path::PathBuf; + +fn get_clap_config<'a>() -> ArgMatches<'a> { + App::new("lintcheck") + .about("run clippy on a set of crates and check output") + .arg( + Arg::with_name("only") + .takes_value(true) + .value_name("CRATE") + .long("only") + .help("Only process a single crate of the list"), + ) + .arg( + Arg::with_name("crates-toml") + .takes_value(true) + .value_name("CRATES-SOURCES-TOML-PATH") + .long("crates-toml") + .help("Set the path for a crates.toml where lintcheck should read the sources from"), + ) + .arg( + Arg::with_name("threads") + .takes_value(true) + .value_name("N") + .short("j") + .long("jobs") + .help("Number of threads to use, 0 automatic choice"), + ) + .arg( + Arg::with_name("fix") + .long("--fix") + .help("Runs cargo clippy --fix and checks if all suggestions apply"), + ) + .arg( + Arg::with_name("filter") + .long("--filter") + .takes_value(true) + .multiple(true) + .value_name("clippy_lint_name") + .help("Apply a filter to only collect specified lints, this also overrides `allow` attributes"), + ) + .arg( + Arg::with_name("markdown") + .long("--markdown") + .help("Change the reports table to use markdown links"), + ) + .get_matches() +} + +#[derive(Debug)] +pub(crate) struct LintcheckConfig { + /// max number of jobs to spawn (default 1) + pub max_jobs: usize, + /// we read the sources to check from here + pub sources_toml_path: PathBuf, + /// we save the clippy lint results here + pub lintcheck_results_path: PathBuf, + /// Check only a specified package + pub only: Option, + /// whether to just run --fix and not collect all the warnings + pub fix: bool, + /// A list of lints that this lintcheck run should focus on + pub lint_filter: Vec, + /// Indicate if the output should support markdown syntax + pub markdown: bool, +} + +impl LintcheckConfig { + pub fn new() -> Self { + let clap_config = get_clap_config(); + + // first, check if we got anything passed via the LINTCHECK_TOML env var, + // if not, ask clap if we got any value for --crates-toml + // if not, use the default "lintcheck/lintcheck_crates.toml" + let sources_toml = env::var("LINTCHECK_TOML").unwrap_or_else(|_| { + clap_config + .value_of("crates-toml") + .clone() + .unwrap_or("lintcheck/lintcheck_crates.toml") + .to_string() + }); + + let markdown = clap_config.is_present("markdown"); + let sources_toml_path = PathBuf::from(sources_toml); + + // for the path where we save the lint results, get the filename without extension (so for + // wasd.toml, use "wasd"...) + let filename: PathBuf = sources_toml_path.file_stem().unwrap().into(); + let lintcheck_results_path = PathBuf::from(format!( + "lintcheck-logs/{}_logs.{}", + filename.display(), + if markdown { "md" } else { "txt" } + )); + + // look at the --threads arg, if 0 is passed, ask rayon rayon how many threads it would spawn and + // use half of that for the physical core count + // by default use a single thread + let max_jobs = match clap_config.value_of("threads") { + Some(threads) => { + let threads: usize = threads + .parse() + .unwrap_or_else(|_| panic!("Failed to parse '{}' to a digit", threads)); + if threads == 0 { + // automatic choice + // Rayon seems to return thread count so half that for core count + (rayon::current_num_threads() / 2) as usize + } else { + threads + } + }, + // no -j passed, use a single thread + None => 1, + }; + + let lint_filter: Vec = clap_config + .values_of("filter") + .map(|iter| { + iter.map(|lint_name| { + let mut filter = lint_name.replace('_', "-"); + if !filter.starts_with("clippy::") { + filter.insert_str(0, "clippy::"); + } + filter + }) + .collect() + }) + .unwrap_or_default(); + + LintcheckConfig { + max_jobs, + sources_toml_path, + lintcheck_results_path, + only: clap_config.value_of("only").map(String::from), + fix: clap_config.is_present("fix"), + lint_filter, + markdown, + } + } +} diff --git a/lintcheck/src/main.rs b/lintcheck/src/main.rs index 816efbdaedf..dff9d27db0a 100644 --- a/lintcheck/src/main.rs +++ b/lintcheck/src/main.rs @@ -7,23 +7,25 @@ #![allow(clippy::collapsible_else_if)] -use std::ffi::OsStr; +mod config; + +use config::LintcheckConfig; + +use std::collections::HashMap; +use std::env; use std::fmt::Write as _; +use std::fs::write; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::{collections::HashMap, io::ErrorKind}; -use std::{ - env, - fs::write, - path::{Path, PathBuf}, - thread, - time::Duration, -}; +use std::thread; +use std::time::Duration; -use clap::{App, Arg, ArgMatches}; +use cargo_metadata::diagnostic::DiagnosticLevel; +use cargo_metadata::Message; use rayon::prelude::*; use serde::{Deserialize, Serialize}; -use serde_json::Value; use walkdir::{DirEntry, WalkDir}; #[cfg(not(windows))] @@ -93,37 +95,67 @@ struct Crate { #[derive(Debug)] struct ClippyWarning { crate_name: String, - crate_version: String, file: String, - line: String, - column: String, - linttype: String, + line: usize, + column: usize, + lint_type: String, message: String, is_ice: bool, } #[allow(unused)] impl ClippyWarning { + fn new(cargo_message: Message, krate: &Crate) -> Option { + let diag = match cargo_message { + Message::CompilerMessage(message) => message.message, + _ => return None, + }; + + let lint_type = diag.code?.code; + if !(lint_type.contains("clippy") || diag.message.contains("clippy")) + || diag.message.contains("could not read cargo metadata") + { + return None; + } + + let span = diag.spans.into_iter().find(|span| span.is_primary)?; + + let file = match Path::new(&span.file_name).strip_prefix(env!("CARGO_HOME")) { + Ok(stripped) => format!("$CARGO_HOME/{}", stripped.display()), + Err(_) => format!( + "target/lintcheck/sources/{}-{}/{}", + krate.name, krate.version, span.file_name + ), + }; + + Some(Self { + crate_name: krate.name.clone(), + file, + line: span.line_start, + column: span.column_start, + lint_type, + message: diag.message, + is_ice: diag.level == DiagnosticLevel::Ice, + }) + } + fn to_output(&self, markdown: bool) -> String { - let file = format!("{}-{}/{}", &self.crate_name, &self.crate_version, &self.file); - let file_with_pos = format!("{}:{}:{}", &file, &self.line, &self.column); + let file_with_pos = format!("{}:{}:{}", &self.file, &self.line, &self.column); if markdown { - let lint = format!("`{}`", self.linttype); + let lint = format!("`{}`", self.lint_type); + + let mut file = self.file.clone(); + if !file.starts_with('$') { + file.insert_str(0, "../"); + } let mut output = String::from("| "); - let _ = write!( - output, - "[`{}`](../target/lintcheck/sources/{}#L{})", - file_with_pos, file, self.line - ); + let _ = write!(output, "[`{}`]({}#L{})", file_with_pos, file, self.line); let _ = write!(output, r#" | {:<50} | "{}" |"#, lint, self.message); output.push('\n'); output } else { - format!( - "target/lintcheck/sources/{} {} \"{}\"\n", - file_with_pos, self.linttype, self.message - ) + format!("{} {} \"{}\"\n", file_with_pos, self.lint_type, self.message) } } } @@ -278,18 +310,17 @@ impl Crate { &self, cargo_clippy_path: &Path, target_dir_index: &AtomicUsize, - thread_limit: usize, total_crates_to_lint: usize, - fix: bool, + config: &LintcheckConfig, lint_filter: &Vec, ) -> Vec { // advance the atomic index by one let index = target_dir_index.fetch_add(1, Ordering::SeqCst); // "loop" the index within 0..thread_limit - let thread_index = index % thread_limit; + let thread_index = index % config.max_jobs; let perc = (index * 100) / total_crates_to_lint; - if thread_limit == 1 { + if config.max_jobs == 1 { println!( "{}/{} {}% Linting {} {}", index, total_crates_to_lint, perc, &self.name, &self.version @@ -305,7 +336,7 @@ impl Crate { let shared_target_dir = clippy_project_root().join("target/lintcheck/shared_target_dir"); - let mut args = if fix { + let mut args = if config.fix { vec!["--fix", "--"] } else { vec!["--", "--message-format=json", "--"] @@ -356,7 +387,7 @@ impl Crate { ); } - if fix { + if config.fix { if let Some(stderr) = stderr .lines() .find(|line| line.contains("failed to automatically apply fixes suggested by rustc to crate")) @@ -371,127 +402,15 @@ impl Crate { return Vec::new(); } - let output_lines = stdout.lines(); - let warnings: Vec = output_lines - .into_iter() - // get all clippy warnings and ICEs - .filter(|line| filter_clippy_warnings(&line)) - .map(|json_msg| parse_json_message(json_msg, &self)) + // get all clippy warnings and ICEs + let warnings: Vec = Message::parse_stream(stdout.as_bytes()) + .filter_map(|msg| ClippyWarning::new(msg.unwrap(), &self)) .collect(); warnings } } -#[derive(Debug)] -struct LintcheckConfig { - /// max number of jobs to spawn (default 1) - max_jobs: usize, - /// we read the sources to check from here - sources_toml_path: PathBuf, - /// we save the clippy lint results here - lintcheck_results_path: PathBuf, - /// whether to just run --fix and not collect all the warnings - fix: bool, - /// A list of lints that this lintcheck run should focus on - lint_filter: Vec, - /// Indicate if the output should support markdown syntax - markdown: bool, -} - -impl LintcheckConfig { - fn from_clap(clap_config: &ArgMatches) -> Self { - // first, check if we got anything passed via the LINTCHECK_TOML env var, - // if not, ask clap if we got any value for --crates-toml - // if not, use the default "lintcheck/lintcheck_crates.toml" - let sources_toml = env::var("LINTCHECK_TOML").unwrap_or_else(|_| { - clap_config - .value_of("crates-toml") - .clone() - .unwrap_or("lintcheck/lintcheck_crates.toml") - .to_string() - }); - - let markdown = clap_config.is_present("markdown"); - let sources_toml_path = PathBuf::from(sources_toml); - - // for the path where we save the lint results, get the filename without extension (so for - // wasd.toml, use "wasd"...) - let filename: PathBuf = sources_toml_path.file_stem().unwrap().into(); - let lintcheck_results_path = PathBuf::from(format!( - "lintcheck-logs/{}_logs.{}", - filename.display(), - if markdown { "md" } else { "txt" } - )); - - // look at the --threads arg, if 0 is passed, ask rayon rayon how many threads it would spawn and - // use half of that for the physical core count - // by default use a single thread - let max_jobs = match clap_config.value_of("threads") { - Some(threads) => { - let threads: usize = threads - .parse() - .unwrap_or_else(|_| panic!("Failed to parse '{}' to a digit", threads)); - if threads == 0 { - // automatic choice - // Rayon seems to return thread count so half that for core count - (rayon::current_num_threads() / 2) as usize - } else { - threads - } - }, - // no -j passed, use a single thread - None => 1, - }; - let fix: bool = clap_config.is_present("fix"); - let lint_filter: Vec = clap_config - .values_of("filter") - .map(|iter| { - iter.map(|lint_name| { - let mut filter = lint_name.replace('_', "-"); - if !filter.starts_with("clippy::") { - filter.insert_str(0, "clippy::"); - } - filter - }) - .collect() - }) - .unwrap_or_default(); - - LintcheckConfig { - max_jobs, - sources_toml_path, - lintcheck_results_path, - fix, - lint_filter, - markdown, - } - } -} - -/// takes a single json-formatted clippy warnings and returns true (we are interested in that line) -/// or false (we aren't) -fn filter_clippy_warnings(line: &str) -> bool { - // we want to collect ICEs because clippy might have crashed. - // these are summarized later - if line.contains("internal compiler error: ") { - return true; - } - // in general, we want all clippy warnings - // however due to some kind of bug, sometimes there are absolute paths - // to libcore files inside the message - // or we end up with cargo-metadata output (https://github.com/rust-lang/rust-clippy/issues/6508) - - // filter out these message to avoid unnecessary noise in the logs - if line.contains("clippy::") - && !(line.contains("could not read cargo metadata") - || (line.contains(".rustup") && line.contains("toolchains"))) - { - return true; - } - false -} - /// Builds clippy inside the repo to make sure we have a clippy executable we can use. fn build_clippy() { let status = Command::new("cargo") @@ -527,10 +446,8 @@ fn read_crates(toml_path: &Path) -> Vec { path: PathBuf::from(path), options: tk.options.clone(), }); - } - - // if we have multiple versions, save each one - if let Some(ref versions) = tk.versions { + } else if let Some(ref versions) = tk.versions { + // if we have multiple versions, save each one versions.iter().for_each(|ver| { crate_sources.push(CrateSource::CratesIo { name: tk.name.clone(), @@ -538,16 +455,18 @@ fn read_crates(toml_path: &Path) -> Vec { options: tk.options.clone(), }); }) - } - // otherwise, we should have a git source - if tk.git_url.is_some() && tk.git_hash.is_some() { + } else if tk.git_url.is_some() && tk.git_hash.is_some() { + // otherwise, we should have a git source crate_sources.push(CrateSource::Git { name: tk.name.clone(), url: tk.git_url.clone().unwrap(), commit: tk.git_hash.clone().unwrap(), options: tk.options.clone(), }); + } else { + panic!("Invalid crate source: {tk:?}"); } + // if we have a version as well as a git data OR only one git data, something is funky if tk.versions.is_some() && (tk.git_url.is_some() || tk.git_hash.is_some()) || tk.git_hash.is_some() != tk.git_url.is_some() @@ -568,57 +487,13 @@ fn read_crates(toml_path: &Path) -> Vec { crate_sources } -/// Parse the json output of clippy and return a `ClippyWarning` -fn parse_json_message(json_message: &str, krate: &Crate) -> ClippyWarning { - let jmsg: Value = serde_json::from_str(&json_message).unwrap_or_else(|e| panic!("Failed to parse json:\n{:?}", e)); - - let file: String = jmsg["message"]["spans"][0]["file_name"] - .to_string() - .trim_matches('"') - .into(); - - let file = if file.contains(".cargo") { - // if we deal with macros, a filename may show the origin of a macro which can be inside a dep from - // the registry. - // don't show the full path in that case. - - // /home/matthias/.cargo/registry/src/github.com-1ecc6299db9ec823/syn-1.0.63/src/custom_keyword.rs - let path = PathBuf::from(file); - let mut piter = path.iter(); - // consume all elements until we find ".cargo", so that "/home/matthias" is skipped - let _: Option<&OsStr> = piter.find(|x| x == &std::ffi::OsString::from(".cargo")); - // collect the remaining segments - let file = piter.collect::(); - format!("{}", file.display()) - } else { - file - }; - - ClippyWarning { - crate_name: krate.name.to_string(), - crate_version: krate.version.to_string(), - file, - line: jmsg["message"]["spans"][0]["line_start"] - .to_string() - .trim_matches('"') - .into(), - column: jmsg["message"]["spans"][0]["text"][0]["highlight_start"] - .to_string() - .trim_matches('"') - .into(), - linttype: jmsg["message"]["code"]["code"].to_string().trim_matches('"').into(), - message: jmsg["message"]["message"].to_string().trim_matches('"').into(), - is_ice: json_message.contains("internal compiler error: "), - } -} - /// Generate a short list of occurring lints-types and their count fn gather_stats(clippy_warnings: &[ClippyWarning]) -> (String, HashMap<&String, usize>) { // count lint type occurrences let mut counter: HashMap<&String, usize> = HashMap::new(); clippy_warnings .iter() - .for_each(|wrn| *counter.entry(&wrn.linttype).or_insert(0) += 1); + .for_each(|wrn| *counter.entry(&wrn.lint_type).or_insert(0) += 1); // collect into a tupled list for sorting let mut stats: Vec<(&&String, &usize)> = counter.iter().map(|(lint, count)| (lint, count)).collect(); @@ -667,22 +542,14 @@ fn lintcheck_needs_rerun(lintcheck_logs_path: &Path) -> bool { logs_modified < clippy_modified } -/// lintchecks `main()` function -/// -/// # Panics -/// -/// This function panics if the clippy binaries don't exist -/// or if lintcheck is executed from the wrong directory (aka none-repo-root) -pub fn main() { +fn main() { // assert that we launch lintcheck from the repo root (via cargo lintcheck) if std::fs::metadata("lintcheck/Cargo.toml").is_err() { eprintln!("lintcheck needs to be run from clippys repo root!\nUse `cargo lintcheck` alternatively."); std::process::exit(3); } - let clap_config = &get_clap_config(); - - let config = LintcheckConfig::from_clap(clap_config); + let config = LintcheckConfig::new(); println!("Compiling clippy..."); build_clippy(); @@ -736,76 +603,46 @@ pub fn main() { }) .collect(); - let clippy_warnings: Vec = if let Some(only_one_crate) = clap_config.value_of("only") { - // if we don't have the specified crate in the .toml, throw an error - if !crates.iter().any(|krate| { - let name = match krate { - CrateSource::CratesIo { name, .. } | CrateSource::Git { name, .. } | CrateSource::Path { name, .. } => { - name - }, - }; - name == only_one_crate - }) { - eprintln!( - "ERROR: could not find crate '{}' in lintcheck/lintcheck_crates.toml", - only_one_crate - ); - std::process::exit(1); - } + let crates: Vec = crates + .into_iter() + .filter(|krate| { + if let Some(only_one_crate) = &config.only { + let name = match krate { + CrateSource::CratesIo { name, .. } + | CrateSource::Git { name, .. } + | CrateSource::Path { name, .. } => name, + }; - // only check a single crate that was passed via cmdline - crates - .into_iter() - .map(|krate| krate.download_and_extract()) - .filter(|krate| krate.name == only_one_crate) - .flat_map(|krate| { - krate.run_clippy_lints(&cargo_clippy_path, &AtomicUsize::new(0), 1, 1, config.fix, &lint_filter) - }) - .collect() - } else { - if config.max_jobs > 1 { - // run parallel with rayon + name == only_one_crate + } else { + true + } + }) + .map(|krate| krate.download_and_extract()) + .collect(); - // Ask rayon for thread count. Assume that half of that is the number of physical cores - // Use one target dir for each core so that we can run N clippys in parallel. - // We need to use different target dirs because cargo would lock them for a single build otherwise, - // killing the parallelism. However this also means that deps will only be reused half/a - // quarter of the time which might result in a longer wall clock runtime + if crates.is_empty() { + eprintln!( + "ERROR: could not find crate '{}' in lintcheck/lintcheck_crates.toml", + config.only.unwrap(), + ); + std::process::exit(1); + } - // This helps when we check many small crates with dep-trees that don't have a lot of branches in - // order to achieve some kind of parallelism + // run parallel with rayon - // by default, use a single thread - let num_cpus = config.max_jobs; - let num_crates = crates.len(); + // This helps when we check many small crates with dep-trees that don't have a lot of branches in + // order to achieve some kind of parallelism - // check all crates (default) - crates - .into_par_iter() - .map(|krate| krate.download_and_extract()) - .flat_map(|krate| { - krate.run_clippy_lints( - &cargo_clippy_path, - &counter, - num_cpus, - num_crates, - config.fix, - &lint_filter, - ) - }) - .collect() - } else { - // run sequential - let num_crates = crates.len(); - crates - .into_iter() - .map(|krate| krate.download_and_extract()) - .flat_map(|krate| { - krate.run_clippy_lints(&cargo_clippy_path, &counter, 1, num_crates, config.fix, &lint_filter) - }) - .collect() - } - }; + rayon::ThreadPoolBuilder::new() + .num_threads(config.max_jobs) + .build_global() + .unwrap(); + + let clippy_warnings: Vec = crates + .par_iter() + .flat_map(|krate| krate.run_clippy_lints(&cargo_clippy_path, &counter, crates.len(), &config, &lint_filter)) + .collect(); // if we are in --fix mode, don't change the log files, terminate here if config.fix { @@ -837,7 +674,7 @@ pub fn main() { text.push_str("| file | lint | message |\n"); text.push_str("| --- | --- | --- |\n"); } - write!(text, "{}", all_msgs.join("")); + write!(text, "{}", all_msgs.join("")).unwrap(); text.push_str("\n\n### ICEs:\n"); for (cratename, msg) in ices.iter() { let _ = write!(text, "{}: '{}'", cratename, msg); @@ -949,75 +786,10 @@ fn create_dirs(krate_download_dir: &Path, extract_dir: &Path) { }); } -fn get_clap_config<'a>() -> ArgMatches<'a> { - App::new("lintcheck") - .about("run clippy on a set of crates and check output") - .arg( - Arg::with_name("only") - .takes_value(true) - .value_name("CRATE") - .long("only") - .help("only process a single crate of the list"), - ) - .arg( - Arg::with_name("crates-toml") - .takes_value(true) - .value_name("CRATES-SOURCES-TOML-PATH") - .long("crates-toml") - .help("set the path for a crates.toml where lintcheck should read the sources from"), - ) - .arg( - Arg::with_name("threads") - .takes_value(true) - .value_name("N") - .short("j") - .long("jobs") - .help("number of threads to use, 0 automatic choice"), - ) - .arg( - Arg::with_name("fix") - .long("--fix") - .help("runs cargo clippy --fix and checks if all suggestions apply"), - ) - .arg( - Arg::with_name("filter") - .long("--filter") - .takes_value(true) - .multiple(true) - .value_name("clippy_lint_name") - .help("apply a filter to only collect specified lints, this also overrides `allow` attributes"), - ) - .arg( - Arg::with_name("markdown") - .long("--markdown") - .help("change the reports table to use markdown links"), - ) - .get_matches() -} - /// Returns the path to the Clippy project directory -/// -/// # Panics -/// -/// Panics if the current directory could not be retrieved, there was an error reading any of the -/// Cargo.toml files or ancestor directory is the clippy root directory #[must_use] -pub fn clippy_project_root() -> PathBuf { - let current_dir = std::env::current_dir().unwrap(); - for path in current_dir.ancestors() { - let result = std::fs::read_to_string(path.join("Cargo.toml")); - if let Err(err) = &result { - if err.kind() == std::io::ErrorKind::NotFound { - continue; - } - } - - let content = result.unwrap(); - if content.contains("[package]\nname = \"clippy\"") { - return path.to_path_buf(); - } - } - panic!("error: Can't determine root of project. Please run inside a Clippy working dir."); +fn clippy_project_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap() } #[test]