use anyhow::{anyhow, bail};
use anyhow::{Context, Result};
use cargo_check_external_types::cargo::CargoRustDocJson;
use cargo_check_external_types::error::{ErrorPrinter, ValidationError};
use cargo_check_external_types::here;
use cargo_check_external_types::visitor::Visitor;
use cargo_metadata::{CargoOpt, Metadata};
use clap::Parser;
use std::borrow::Cow;
use std::fmt;
use std::fs;
use std::path::PathBuf;
use std::process;
use std::str::FromStr;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
#[derive(Debug, Eq, PartialEq)]
enum OutputFormat {
Errors,
MarkdownTable,
}
impl fmt::Display for OutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Errors => "errors",
Self::MarkdownTable => "markdown-table",
})
}
}
impl FromStr for OutputFormat {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"errors" => Ok(OutputFormat::Errors),
"markdown-table" => Ok(OutputFormat::MarkdownTable),
_ => Err(anyhow!(
"invalid output format: {}. Expected `errors` or `markdown-table`.",
s
)),
}
}
}
#[derive(clap::Args, Debug, Eq, PartialEq)]
struct CheckExternalTypesArgs {
#[clap(long, conflicts_with = "no-default-features")]
all_features: bool,
#[clap(long, conflicts_with = "all-features")]
no_default_features: bool,
#[clap(long, use_value_delimiter = true)]
features: Option<Vec<String>>,
#[clap(long)]
manifest_path: Option<PathBuf>,
#[clap(long)]
config: Option<PathBuf>,
#[clap(short, long)]
verbose: bool,
#[clap(long, default_value_t = OutputFormat::Errors)]
output_format: OutputFormat,
}
#[derive(Parser, Debug, Eq, PartialEq)]
#[clap(author, version, about, bin_name = "cargo")]
enum Args {
CheckExternalTypes(CheckExternalTypesArgs),
}
enum Error {
ValidationErrors,
Failure(anyhow::Error),
}
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Self {
Error::Failure(err)
}
}
fn main() {
process::exit(match run_main() {
Ok(_) => 0,
Err(Error::ValidationErrors) => 1,
Err(Error::Failure(err)) => {
println!("{:#}", dbg!(err));
2
}
})
}
fn run_main() -> Result<(), Error> {
let Args::CheckExternalTypes(args) = Args::parse();
if args.verbose {
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("debug"))
.unwrap();
let fmt_layer = tracing_subscriber::fmt::layer()
.without_time()
.with_ansi(true)
.with_level(true)
.with_target(false)
.pretty();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init();
}
let config = if let Some(config_path) = &args.config {
let contents = fs::read_to_string(config_path).context("failed to read config file")?;
toml::from_str(&contents).context("failed to parse config file")?
} else {
Default::default()
};
let mut cargo_metadata_cmd = cargo_metadata::MetadataCommand::new();
if args.all_features {
cargo_metadata_cmd.features(CargoOpt::AllFeatures);
}
if args.no_default_features {
cargo_metadata_cmd.features(CargoOpt::NoDefaultFeatures);
}
if let Some(features) = args.features {
cargo_metadata_cmd.features(CargoOpt::SomeFeatures(features));
}
let crate_path = if let Some(manifest_path) = args.manifest_path {
cargo_metadata_cmd.manifest_path(&manifest_path);
manifest_path
.canonicalize()
.context(here!())?
.parent()
.expect("parent path")
.to_path_buf()
} else {
std::env::current_dir()
.context(here!())?
.canonicalize()
.context(here!())?
};
let cargo_metadata = cargo_metadata_cmd.exec().context(here!())?;
let cargo_features = resolve_features(&cargo_metadata)?;
eprintln!("Running rustdoc to produce json doc output...");
let package = CargoRustDocJson::new(
&*cargo_metadata
.root_package()
.as_ref()
.map(|package| Cow::Borrowed(package.name.as_str()))
.unwrap_or_else(|| crate_path.file_name().expect("file name").to_string_lossy()),
&crate_path,
&cargo_metadata.target_directory,
cargo_features,
)
.run()
.context(here!())?;
eprintln!("Examining all public types...");
let errors = Visitor::new(config, package)?.visit_all()?;
match args.output_format {
OutputFormat::Errors => {
ErrorPrinter::new(&cargo_metadata.workspace_root).pretty_print_errors(&errors);
if errors.error_count() > 0 {
return Err(Error::ValidationErrors);
}
}
OutputFormat::MarkdownTable => {
println!("| Crate | Type | Used In |");
println!("| --- | --- | --- |");
let mut rows = Vec::new();
for error in errors.iter() {
if let ValidationError::UnapprovedExternalTypeRef { .. } = error {
let type_name = error.type_name();
let crate_name = &type_name[0..type_name.find("::").unwrap_or(type_name.len())];
let location = error.location().unwrap();
rows.push(format!(
"| {} | {} | {}:{}:{} |",
crate_name,
type_name,
location.filename.to_string_lossy(),
location.begin.0,
location.begin.1
));
}
}
rows.sort();
rows.into_iter().for_each(|row| println!("{}", row));
}
}
Ok(())
}
fn resolve_features(metadata: &Metadata) -> Result<Vec<String>> {
let root_package = metadata
.root_package()
.ok_or_else(|| anyhow!("No root package found"))?;
if let Some(resolve) = &metadata.resolve {
let root_node = resolve
.nodes
.iter()
.find(|&n| n.id == root_package.id)
.ok_or_else(|| anyhow!("Failed to find node for root package"))?;
Ok(root_node.features.clone())
} else {
bail!("Cargo metadata didn't have resolved nodes");
}
}
#[cfg(test)]
mod arg_parse_tests {
use super::*;
use clap::Parser;
#[test]
fn defaults() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: false,
no_default_features: false,
features: None,
manifest_path: None,
config: None,
verbose: false,
output_format: OutputFormat::Errors,
}),
Args::try_parse_from(["cargo", "check-external-types"]).unwrap()
);
}
#[test]
fn all_features() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: true,
no_default_features: false,
features: None,
manifest_path: None,
config: None,
verbose: false,
output_format: OutputFormat::Errors,
}),
Args::try_parse_from(["cargo", "check-external-types", "--all-features"]).unwrap()
);
}
#[test]
fn no_default_features() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: false,
no_default_features: true,
features: None,
manifest_path: None,
config: None,
verbose: false,
output_format: OutputFormat::Errors,
}),
Args::try_parse_from(["cargo", "check-external-types", "--no-default-features"])
.unwrap()
);
}
#[test]
fn feature_list() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: false,
no_default_features: false,
features: Some(vec!["foo".into(), "bar".into()]),
manifest_path: None,
config: None,
verbose: false,
output_format: OutputFormat::Errors,
}),
Args::try_parse_from(["cargo", "check-external-types", "--features", "foo,bar"])
.unwrap()
);
}
#[test]
fn manifest_path() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: false,
no_default_features: false,
features: None,
manifest_path: Some("test-path".into()),
config: None,
verbose: false,
output_format: OutputFormat::Errors,
}),
Args::try_parse_from([
"cargo",
"check-external-types",
"--manifest-path",
"test-path"
])
.unwrap()
);
}
#[test]
fn verbose() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: false,
no_default_features: false,
features: None,
manifest_path: None,
config: None,
verbose: true,
output_format: OutputFormat::Errors,
}),
Args::try_parse_from(["cargo", "check-external-types", "--verbose"]).unwrap()
);
}
#[test]
fn output_format_markdown_table() {
assert_eq!(
Args::CheckExternalTypes(CheckExternalTypesArgs {
all_features: false,
no_default_features: false,
features: None,
manifest_path: None,
config: None,
verbose: false,
output_format: OutputFormat::MarkdownTable,
}),
Args::try_parse_from([
"cargo",
"check-external-types",
"--output-format",
"markdown-table"
])
.unwrap()
);
}
#[test]
fn conflict_all_features_no_default_features() {
assert!(Args::try_parse_from([
"cargo",
"check-external-types",
"--all-features",
"--no-default-features"
])
.is_err());
}
}