#![deny(clippy::expect_used)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use anyhow::{anyhow, bail, ensure, Result};
use cargo_metadata::MetadataCommand;
use clap::{crate_version, lazy_static::lazy_static, Clap};
use dylint_internal::{
env::{self, var},
Command,
};
use std::{
collections::{BTreeMap, BTreeSet},
env::consts,
ffi::OsStr,
fmt::Debug,
fs::read_dir,
path::{Path, PathBuf},
};
pub mod driver_builder;
mod error;
use error::warn;
pub use error::{ColorizedError, ColorizedResult};
lazy_static! {
static ref REQUIRED_FORM: String = format!(
r#""{}" LIBRARY_NAME "@" TOOLCHAIN "{}""#,
consts::DLL_PREFIX,
consts::DLL_SUFFIX
);
}
type ToolchainMap = BTreeMap<String, BTreeSet<PathBuf>>;
pub type NameToolchainMap = BTreeMap<String, ToolchainMap>;
#[derive(Clap, Debug)]
struct Opts {
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap, Debug)]
enum SubCommand {
Dylint(Dylint),
}
#[derive(Clap, Debug, Default)]
#[clap(
version = crate_version!(),
after_help = "ENVIRONMENT VARIABLES:
DYLINT_DRIVER_PATH (default: $HOME/.dylint_drivers) is the directory where Dylint stores rustc
drivers.
DYLINT_LIBRARY_PATH (default: none) is a colon-separated list of directories where Dylint searches
for libraries.
",
)]
pub struct Dylint {
#[clap(long = "all", about = "Load all discovered libraries")]
pub all: bool,
#[clap(
multiple = true,
number_of_values = 1,
long = "lib",
value_name = "name",
about = "Library name to load lints from. A file with a name of the form \"DLL_PREFIX \
<name> '@' TOOLCHAIN DLL_SUFFIX\" is searched for in the directories listed in \
DYLINT_LIBRARY_PATH and in the current package's `debug` and `release` directories."
)]
pub libs: Vec<String>,
#[clap(
long = "list",
about = "If no libaries are named, list the name, toolchain, and location of all \
discovered libraries. If at least one library is named, list the name, level, and \
description of all lints in all named libraries. Combine with `--all` to list all \
lints in all discovered libraries."
)]
pub list: bool,
#[clap(
multiple = true,
number_of_values = 1,
long = "path",
value_name = "path",
about = "Library path to load lints from"
)]
pub paths: Vec<String>,
#[clap(short = 'q', long = "quiet", about = "Suppress warnings")]
pub quiet: bool,
#[clap(
about = "Libraries to load lints from. Each <name> is searched for as described under \
`--lib`. If no library is found, <name> is treated as path. To avoid ambiguity, use \
`--lib` or `--path`."
)]
pub names: Vec<String>,
#[clap(last = true, about = "Arguments for `cargo check`")]
pub args: Vec<String>,
}
pub fn cargo_dylint<T: AsRef<OsStr>>(args: &[T]) -> ColorizedResult<()> {
match Opts::parse_from(args).subcmd {
SubCommand::Dylint(opts) => run(&opts),
}
.map_err(ColorizedError::new)
}
pub fn run(opts: &Dylint) -> Result<()> {
let name_toolchain_map = name_toolchain_map()?;
run_with_name_toolchain_map(opts, &name_toolchain_map)
}
pub fn name_toolchain_map() -> Result<NameToolchainMap> {
let mut name_toolchain_map = NameToolchainMap::new();
let dylint_library_paths = dylint_library_paths()?;
let profile_paths = profile_paths();
for path in dylint_library_paths.iter().chain(&profile_paths) {
for entry in dylint_libraries_in(path)? {
let (name, toolchain, path) = entry?;
name_toolchain_map
.entry(name)
.or_insert_with(Default::default)
.entry(toolchain)
.or_insert_with(Default::default)
.insert(path);
}
}
Ok(name_toolchain_map)
}
fn dylint_library_paths() -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
if let Ok(val) = var(env::DYLINT_LIBRARY_PATH) {
for path in val.split(':') {
let path = PathBuf::from(path);
ensure!(
path.is_absolute(),
"DYLINT_LIBRARY_PATH contains `{}`, which is not absolute",
path.to_string_lossy()
);
ensure!(
path.is_dir(),
"DYLINT_LIBRARY_PATH contains `{}`, which is not a directory",
path.to_string_lossy()
);
paths.push(path);
}
}
Ok(paths)
}
fn profile_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(metadata) = MetadataCommand::new().no_deps().exec() {
let debug = metadata.target_directory.join_os("debug");
let release = metadata.target_directory.join_os("release");
if debug.is_dir() {
paths.push(debug);
}
if release.is_dir() {
paths.push(release);
}
}
paths
}
#[allow(clippy::option_if_let_else)]
fn dylint_libraries_in(
path: &Path,
) -> Result<impl Iterator<Item = Result<(String, String, PathBuf)>>> {
let iter = read_dir(path)?;
Ok(iter
.map(|entry| -> Result<Option<(String, String, PathBuf)>> {
let entry = entry?;
let path = entry.path();
Ok(if let Some(filename) = path.file_name() {
parse_filename(&filename.to_string_lossy())
.map(|(lib_name, toolchain)| (lib_name, toolchain, path))
} else {
None
})
})
.filter_map(Result::transpose))
}
fn run_with_name_toolchain_map(opts: &Dylint, name_toolchain_map: &NameToolchainMap) -> Result<()> {
if opts.list
&& opts.libs.is_empty()
&& opts.paths.is_empty()
&& opts.names.is_empty()
&& !opts.all
{
return list_libs(name_toolchain_map);
}
let resolved = resolve(opts, name_toolchain_map)?;
if resolved.is_empty() {
assert!(opts.libs.is_empty());
assert!(opts.paths.is_empty());
assert!(opts.names.is_empty());
if name_toolchain_map.is_empty() {
warn(opts, "No libraries were found.");
return Ok(());
}
assert!(!opts.all);
warn(opts, "Nothing to do. Did you forget `--all`?");
return Ok(());
}
if opts.list {
list_lints(opts, name_toolchain_map, &resolved)
} else {
check(opts, name_toolchain_map, &resolved)
}
}
fn list_libs(name_toolchain_map: &NameToolchainMap) -> Result<()> {
let name_width = name_toolchain_map
.keys()
.map(String::len)
.max()
.unwrap_or_default();
let toolchain_width = name_toolchain_map
.values()
.flat_map(ToolchainMap::keys)
.map(String::len)
.max()
.unwrap_or_default();
for (name, toolchain_map) in name_toolchain_map {
for (toolchain, paths) in toolchain_map {
for path in paths {
let parent = path
.parent()
.ok_or_else(|| anyhow!("Could not get parent directory"))?;
println!(
"{:<name_width$} {:<toolchain_width$} {}",
name,
toolchain,
parent.to_string_lossy(),
name_width = name_width,
toolchain_width = toolchain_width
);
}
}
}
Ok(())
}
#[allow(unknown_lints)]
#[allow(question_mark_in_expression)]
fn resolve(opts: &Dylint, name_toolchain_map: &NameToolchainMap) -> Result<ToolchainMap> {
let mut toolchain_map = ToolchainMap::new();
if opts.all {
name_toolchain_map.values().cloned().for_each(|other| {
for (toolchain, mut paths) in other {
toolchain_map
.entry(toolchain)
.or_insert_with(Default::default)
.append(&mut paths);
}
});
}
for name in &opts.libs {
ensure!(!opts.all, "`--lib` cannot be used with `--all`");
let (toolchain, path) =
name_as_lib(name_toolchain_map, name, true)?.unwrap_or_else(|| unreachable!());
toolchain_map
.entry(toolchain)
.or_insert_with(Default::default)
.insert(path);
}
for name in &opts.paths {
let (toolchain, path) = name_as_path(name, true)?.unwrap_or_else(|| unreachable!());
toolchain_map
.entry(toolchain)
.or_insert_with(Default::default)
.insert(path);
}
for name in &opts.names {
if let Some((toolchain, path)) = name_as_lib(name_toolchain_map, name, false)? {
ensure!(
!opts.all,
"`{}` is a library name and cannot be used with `--all`; if a path was meant, use `--path {}`",
name,
name
);
toolchain_map
.entry(toolchain)
.or_insert_with(Default::default)
.insert(path);
} else if let Some((toolchain, path)) = name_as_path(name, false)? {
toolchain_map
.entry(toolchain)
.or_insert_with(Default::default)
.insert(path);
} else {
bail!("Could not find `{}`", name);
}
}
Ok(toolchain_map)
}
pub fn name_as_lib(
name_toolchain_map: &NameToolchainMap,
name: &str,
as_lib_only: bool,
) -> Result<Option<(String, PathBuf)>> {
if !is_valid_lib_name(name) {
ensure!(!as_lib_only, "`{}` is not a valid library name", name);
return Ok(None);
}
if let Some(toolchain_map) = name_toolchain_map.get(name) {
let mut toolchain_paths = flatten_toolchain_map(toolchain_map);
return match toolchain_paths.len() {
0 => Ok(None),
1 => Ok(Some(toolchain_paths.remove(0))),
_ => Err(anyhow!(
"Found multiple libraries matching `{}`: {:?}",
name,
toolchain_paths
.iter()
.map(|(_, path)| path)
.collect::<Vec<_>>()
)),
};
}
ensure!(!as_lib_only, "Could not find `--lib {}`", name);
Ok(None)
}
fn is_valid_lib_name(name: &str) -> bool {
Path::new(name).file_name() == Some(OsStr::new(name))
}
fn name_as_path(name: &str, as_path_only: bool) -> Result<Option<(String, PathBuf)>> {
if let Ok(path) = PathBuf::from(name).canonicalize() {
if let Some(filename) = path.file_name() {
if let Some((_, toolchain)) = parse_filename(&filename.to_string_lossy()) {
return Ok(Some((toolchain, path)));
}
ensure!(
!as_path_only,
"`--path {}` was used, but the filename does not have the required form: {}",
name,
*REQUIRED_FORM
);
ensure!(
!name.contains(std::path::MAIN_SEPARATOR),
"`{}` is a valid path, but the filename does not have the required form: {}",
name,
*REQUIRED_FORM
);
}
ensure!(
!as_path_only,
"`--path {}` was used, but it is invalid",
name
);
}
ensure!(!as_path_only, "Could not find `--path {}`", name);
Ok(None)
}
fn parse_filename(filename: &str) -> Option<(String, String)> {
let file_stem = filename.strip_suffix(consts::DLL_SUFFIX)?;
let target_name = file_stem.strip_prefix(consts::DLL_PREFIX)?;
parse_target_name(target_name)
}
fn parse_target_name(target_name: &str) -> Option<(String, String)> {
let mut iter = target_name.splitn(2, '@');
let lib_name = iter.next()?;
let toolchain = iter.next()?;
Some((lib_name.to_owned(), toolchain.to_owned()))
}
fn list_lints(
_opts: &Dylint,
name_toolchain_map: &NameToolchainMap,
resolved: &ToolchainMap,
) -> Result<()> {
for (name, toolchain_map) in name_toolchain_map {
for (toolchain, paths) in toolchain_map {
for path in paths {
if resolved
.get(toolchain)
.map_or(false, |paths| paths.contains(path))
{
let driver = driver_builder::get(&toolchain)?;
let dylint_libs = serde_json::to_string(&[path])?;
print!("{}", name);
if toolchain_map.keys().len() >= 2 {
print!("@{}", toolchain);
}
if paths.len() >= 2 {
let parent = path
.parent()
.ok_or_else(|| anyhow!("Could not get parent directory"))?;
print!(" ({})", parent.to_string_lossy());
}
println!();
Command::new(driver)
.envs(vec![
(env::DYLINT_LIBS.to_owned(), dylint_libs),
(env::DYLINT_LIST.to_owned(), "1".to_owned()),
])
.args(vec!["rustc", "-W", "help"])
.success()?;
println!();
}
}
}
}
Ok(())
}
fn check(
opts: &Dylint,
_name_toolchain_map: &NameToolchainMap,
resolved: &ToolchainMap,
) -> Result<()> {
let clippy_disable_docs_links = clippy_disable_docs_links()?;
for (toolchain, paths) in resolved {
let driver = driver_builder::get(&toolchain)?;
let dylint_libs = serde_json::to_string(&paths)?;
dylint_internal::check()
.envs(vec![
(
env::CLIPPY_DISABLE_DOCS_LINKS.to_owned(),
clippy_disable_docs_links.clone(),
),
(env::DYLINT_LIBS.to_owned(), dylint_libs),
(
env::RUSTC_WORKSPACE_WRAPPER.to_owned(),
driver.to_string_lossy().to_string(),
),
(env::RUSTUP_TOOLCHAIN.to_owned(), toolchain.clone()),
])
.args(opts.args.clone())
.success()?;
}
Ok(())
}
fn flatten_toolchain_map(toolchain_map: &ToolchainMap) -> Vec<(String, PathBuf)> {
toolchain_map
.iter()
.flat_map(|(toolchain, paths)| {
paths
.iter()
.map(|path| (toolchain.clone(), path.clone()))
.collect::<Vec<_>>()
})
.collect()
}
fn clippy_disable_docs_links() -> Result<String> {
let val = var(env::CLIPPY_DISABLE_DOCS_LINKS).ok();
serde_json::to_string(&val).map_err(Into::into)
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
use dylint_examples as examples;
use lazy_static::lazy_static;
use std::env::set_var;
use test_env_log::test;
lazy_static! {
static ref NAME_TOOLCHAIN_MAP: NameToolchainMap = {
examples::build().unwrap();
let dylint_library_path = examples::iter()
.unwrap()
.map(|example| {
example
.unwrap()
.join("target")
.join("debug")
.to_string_lossy()
.to_string()
})
.collect::<Vec<_>>()
.join(":");
set_var(env::DYLINT_LIBRARY_PATH, dylint_library_path);
name_toolchain_map().unwrap()
};
}
#[test]
fn multiple_libraries_multiple_toolchains() {
let allow_clippy = NAME_TOOLCHAIN_MAP.get("allow_clippy").unwrap();
let question_mark_in_expression = NAME_TOOLCHAIN_MAP
.get("question_mark_in_expression")
.unwrap();
assert_ne!(
allow_clippy.keys().collect::<Vec<_>>(),
question_mark_in_expression.keys().collect::<Vec<_>>()
);
let opts = Dylint {
libs: vec![
"allow_clippy".to_owned(),
"question_mark_in_expression".to_owned(),
],
..Dylint::default()
};
run_with_name_toolchain_map(&opts, &NAME_TOOLCHAIN_MAP).unwrap();
}
#[test]
fn multiple_libraries_one_toolchain() {
let clippy = NAME_TOOLCHAIN_MAP.get("clippy").unwrap();
let question_mark_in_expression = NAME_TOOLCHAIN_MAP
.get("question_mark_in_expression")
.unwrap();
assert_eq!(
clippy.keys().collect::<Vec<_>>(),
question_mark_in_expression.keys().collect::<Vec<_>>()
);
let opts = Dylint {
libs: vec![
"clippy".to_owned(),
"question_mark_in_expression".to_owned(),
],
..Dylint::default()
};
run_with_name_toolchain_map(&opts, &NAME_TOOLCHAIN_MAP).unwrap();
}
}