use std::io::Write as _;
use std::process::ExitCode;
use clap::Parser;
const GITHUB_SUMMARY_LIMIT: usize = 1_048_576;
#[derive(clap::Parser)]
#[command(
name = "refquiem",
bin_name = "cargo refquiem",
version,
about = "Find mentions of Rust items that no longer exist",
args_conflicts_with_subcommands = true
)]
struct Args {
#[arg(long, value_name = "REV", conflicts_with = "base")]
since: Option<String>,
#[arg(long, value_name = "REV")]
base: Option<String>,
#[arg(long, value_name = "REV", requires = "base")]
head: Option<String>,
#[arg(long, value_name = "PATH")]
config: Option<std::path::PathBuf>,
#[arg(long, value_name = "NAME")]
package: Option<String>,
#[command(flatten)]
output: Output,
#[arg(long)]
no_fail: bool,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(clap::Subcommand)]
enum Command {
Vocabularies(Vocabularies),
}
#[derive(clap::Args)]
struct Vocabularies {
#[arg(long)]
effective: bool,
#[arg(long, value_name = "PATH", requires = "effective")]
config: Option<std::path::PathBuf>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Copy, clap::Args)]
struct Output {
#[arg(long)]
json: bool,
#[arg(long, conflicts_with_all = ["json", "github"])]
markdown: bool,
#[arg(long)]
github: bool,
}
impl Output {
fn emit(self, report: &refquiem::Report, no_fail: bool) -> Result<(), refquiem::Error> {
if self.json {
println!("{}", report.json());
return Ok(());
}
if self.markdown {
let web_root = std::env::current_dir()
.ok()
.and_then(|root| web_root(&root));
print!("{}", report.job_summary(web_root.as_deref()));
return Ok(());
}
print!("{}", report.human());
if self.github {
print!("{}", report.annotations(no_fail));
if let Some(path) = std::env::var_os("GITHUB_STEP_SUMMARY") {
let path = std::path::PathBuf::from(path);
let io = |source| refquiem::Error::Io {
path: path.clone(),
source,
};
let web_root = std::env::current_dir()
.ok()
.and_then(|root| web_root(&root));
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(io)?;
std::io::Write::write_all(
&mut file,
report
.github_summary(web_root.as_deref(), GITHUB_SUMMARY_LIMIT)
.as_bytes(),
)
.map_err(io)?;
}
}
Ok(())
}
}
fn main() -> ExitCode {
let mut args: Vec<String> = std::env::args().collect();
if args.get(1).is_some_and(|a| a == "refquiem") {
args.remove(1);
}
let args = Args::parse_from(args);
match &args.command {
Some(Command::Vocabularies(vocabularies)) => match inspection(vocabularies) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("refquiem: {}", chain(&error));
ExitCode::from(2)
}
},
None => {
let output = args.output;
let no_fail = args.no_fail;
match run(args).and_then(|report| output.emit(&report, no_fail).map(|()| report)) {
Ok(report) if report.is_clean() || no_fail => ExitCode::SUCCESS,
Ok(_) => ExitCode::FAILURE,
Err(error) => {
eprintln!("refquiem: {}", chain(&error));
ExitCode::from(2)
}
}
}
}
}
#[derive(Debug, thiserror::Error)]
enum InspectionError {
#[error(transparent)]
Scan(#[from] refquiem::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
fn inspection(vocabularies: &Vocabularies) -> Result<(), InspectionError> {
if vocabularies.effective {
let root = std::env::current_dir()?;
let config = vocabularies.config.as_ref().map(|path| root.join(path));
let mut options = refquiem::Options::new(root);
options.config = config;
let selection = refquiem::vocabulary::effective(&options)?;
print_selection(&selection, vocabularies.json)
} else {
print_catalog(vocabularies.json)
}
}
fn print_catalog(json: bool) -> Result<(), InspectionError> {
let builtins = refquiem::vocabulary::available();
let mut stdout = std::io::stdout().lock();
if json {
serde_json::to_writer(&mut stdout, builtins)?;
writeln!(stdout)?;
return Ok(());
}
print_builtins(&mut stdout, builtins)?;
Ok(())
}
fn print_selection(
selection: &refquiem::vocabulary::EffectiveVocabularies,
json: bool,
) -> Result<(), InspectionError> {
let mut stdout = std::io::stdout().lock();
if json {
serde_json::to_writer(&mut stdout, selection)?;
writeln!(stdout)?;
return Ok(());
}
writeln!(stdout, "Configuration {}", selection.config.display())?;
writeln!(
stdout,
"Rust standard-library resolution {}",
selection.rust_stdlib
)?;
if selection.builtins.is_empty() && selection.files.is_empty() {
writeln!(stdout, "No optional vocabularies selected.")?;
}
print_builtins(&mut stdout, selection.builtins.iter().copied())?;
print_files(&mut stdout, &selection.files)?;
Ok(())
}
fn print_builtins<'a>(
output: &mut impl std::io::Write,
builtins: impl IntoIterator<Item = &'a refquiem::vocabulary::BuiltinVocabulary>,
) -> std::io::Result<()> {
for builtin in builtins {
writeln!(
output,
"{} ({}, {})",
builtin.id, builtin.kind, builtin.case
)?;
writeln!(
output,
" source {} @{}",
builtin.source, builtin.source_version
)?;
}
Ok(())
}
fn print_files(
output: &mut impl std::io::Write,
files: &[refquiem::vocabulary::CustomVocabulary],
) -> std::io::Result<()> {
for file in files {
writeln!(
output,
"{} ({}, {})",
file.path.display(),
file.kind,
file.case
)?;
writeln!(output, " format version {}", file.format_version)?;
}
Ok(())
}
fn web_root(root: &std::path::Path) -> Option<String> {
let repo = gix::discover(root).ok()?;
let origin = repo.find_remote("origin").ok().and_then(|remote| {
remote
.url(gix::remote::Direction::Fetch)
.map(std::string::ToString::to_string)
});
refquiem::web::root(
std::env::var("GITHUB_SERVER_URL").ok().as_deref(),
std::env::var("GITHUB_REPOSITORY").ok().as_deref(),
origin.as_deref(),
)
}
fn run(args: Args) -> Result<refquiem::Report, refquiem::Error> {
let root = std::env::current_dir().map_err(|source| refquiem::Error::Io {
path: ".".into(),
source,
})?;
let config = args.config.map(|path| root.join(path));
let mut options = refquiem::Options::new(root);
options.range = match (args.since, args.base) {
(Some(since), _) => refquiem::Range::Since(since),
(None, Some(base)) => refquiem::Range::Base {
base,
head: args.head,
},
(None, None) => refquiem::Range::Full,
};
if let Some(target) = std::env::var_os("CARGO_TARGET_DIR") {
options.cache_dir = Some(std::path::PathBuf::from(target).join("refquiem"));
}
options.config = config;
options.package = args.package;
refquiem::run(&options)
}
fn chain(error: &dyn std::error::Error) -> String {
let mut out = error.to_string();
let mut source = error.source();
while let Some(cause) = source {
out.push_str(": ");
out.push_str(&cause.to_string());
source = cause.source();
}
out
}