mod argv;
mod check;
mod checks;
mod cli;
mod error;
mod principles;
mod project;
mod runner;
mod scorecard;
mod source;
mod types;
use clap::Parser as _;
use clap_complete::generate;
use argv::inject_default_subcommand;
use check::Check;
use checks::behavioral::all_behavioral_checks;
use checks::project::all_project_checks;
use checks::source::all_source_checks;
use cli::{Cli, Commands, GenerateKind, OutputFormat};
use error::AppError;
use principles::matrix;
use project::Project;
use scorecard::{exit_code, format_json, format_text};
use types::{CheckGroup, CheckResult, CheckStatus, Confidence};
fn main() {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
let code = match run() {
Ok(code) => code,
Err(e) => {
eprintln!("error: {e}");
2
}
};
std::process::exit(code);
}
fn run() -> Result<i32, AppError> {
let cli = Cli::parse_from(inject_default_subcommand(std::env::args_os()));
let quiet = cli.quiet;
let (path, command, binary_only, source_only, principle, output, include_tests) =
match cli.command {
Some(Commands::Check {
path,
command,
binary,
source,
principle,
output,
include_tests,
}) => (
path,
command,
binary,
source,
principle,
output,
include_tests,
),
Some(Commands::Completions { shell }) => {
let mut cmd = <Cli as clap::CommandFactory>::command();
generate(shell, &mut cmd, "anc", &mut std::io::stdout());
return Ok(0);
}
Some(Commands::Generate { artifact }) => {
return run_generate(artifact);
}
None => {
let mut cmd = <Cli as clap::CommandFactory>::command();
eprintln!("{}", cmd.render_help());
return Ok(2);
}
};
let resolved_path = match command {
Some(name) => resolve_command_on_path(&name)?,
None => path,
};
let mut project = Project::discover(&resolved_path)?;
project.include_tests = include_tests;
let mut all_checks: Vec<Box<dyn Check>> = Vec::new();
let has_binary = project.runner.is_some();
let has_language = project.language.is_some();
if !source_only {
if has_binary {
all_checks.extend(all_behavioral_checks());
} else if binary_only {
eprintln!("warning: --binary specified but no binary found");
} else if has_language {
eprintln!("warning: no binary found, running source checks only");
}
}
if !binary_only {
if let Some(lang) = project.language {
all_checks.extend(all_source_checks(lang));
} else if source_only {
eprintln!("warning: --source specified but no language detected");
}
}
if !binary_only && project.path.is_dir() {
all_checks.extend(all_project_checks());
}
let mut results: Vec<CheckResult> = Vec::new();
for check in &all_checks {
if !check.applicable(&project) {
continue;
}
let result = match check.run(&project) {
Ok(r) => r,
Err(e) => CheckResult {
id: check.id().to_string(),
label: check.id().to_string(),
group: check.group(),
layer: check.layer(),
status: CheckStatus::Error(e.to_string()),
confidence: Confidence::High,
},
};
results.push(result);
}
if let Some(p) = principle {
results.retain(|r| matches_principle(&r.group, p));
}
let output_str = match output {
OutputFormat::Text => format_text(&results, quiet),
OutputFormat::Json => format_json(&results, &all_checks),
};
print!("{output_str}");
Ok(exit_code(&results))
}
fn resolve_command_on_path(name: &str) -> Result<std::path::PathBuf, AppError> {
let locator = if cfg!(windows) { "where" } else { "which" };
let output = std::process::Command::new(locator)
.arg(name)
.output()
.map_err(|e| {
AppError::ProjectDetection(anyhow::anyhow!("failed to invoke `{locator}`: {e}"))
})?;
if !output.status.success() {
return Err(AppError::ProjectDetection(anyhow::anyhow!(
"command '{name}' not found on PATH"
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let first = stdout.lines().next().map(str::trim).unwrap_or("");
if first.is_empty() {
return Err(AppError::ProjectDetection(anyhow::anyhow!(
"command '{name}' not found on PATH"
)));
}
Ok(std::path::PathBuf::from(first))
}
fn run_generate(artifact: GenerateKind) -> Result<i32, AppError> {
match artifact {
GenerateKind::CoverageMatrix {
out,
json_out,
check,
} => {
let catalog = checks::all_checks_catalog();
let dangling = matrix::dangling_cover_ids(&catalog);
if !dangling.is_empty() {
for (check_id, req_id) in &dangling {
eprintln!("error: check `{check_id}` covers unknown requirement `{req_id}`");
}
return Err(AppError::ProjectDetection(anyhow::anyhow!(
"registry drift: {} dangling requirement reference(s)",
dangling.len()
)));
}
let m = matrix::build(&catalog);
let rendered_md = matrix::render_markdown(&m);
let rendered_json = matrix::render_json(&m);
if check {
let existing_md = std::fs::read_to_string(&out).unwrap_or_default();
let existing_json = std::fs::read_to_string(&json_out).unwrap_or_default();
let md_matches = normalize_trailing_newline(&existing_md)
== normalize_trailing_newline(&rendered_md);
let json_matches = normalize_trailing_newline(&existing_json)
== normalize_trailing_newline(&rendered_json);
if !md_matches {
eprintln!(
"error: {} is out of date — run `anc generate coverage-matrix`",
out.display()
);
}
if !json_matches {
eprintln!(
"error: {} is out of date — run `anc generate coverage-matrix`",
json_out.display()
);
}
return Ok(if md_matches && json_matches { 0 } else { 2 });
}
if let Some(parent) = out.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(|e| {
AppError::ProjectDetection(anyhow::anyhow!(
"creating parent dir for {}: {e}",
out.display()
))
})?;
}
}
if let Some(parent) = json_out.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(|e| {
AppError::ProjectDetection(anyhow::anyhow!(
"creating parent dir for {}: {e}",
json_out.display()
))
})?;
}
}
std::fs::write(&out, &rendered_md).map_err(|e| {
AppError::ProjectDetection(anyhow::anyhow!("writing {}: {e}", out.display()))
})?;
std::fs::write(&json_out, &rendered_json).map_err(|e| {
AppError::ProjectDetection(anyhow::anyhow!("writing {}: {e}", json_out.display()))
})?;
eprintln!(
"wrote {} ({} rows) and {}",
out.display(),
m.rows.len(),
json_out.display()
);
Ok(0)
}
}
}
fn normalize_trailing_newline(s: &str) -> &str {
s.trim_end_matches('\n')
}
fn matches_principle(group: &CheckGroup, principle: u8) -> bool {
matches!(
group,
CheckGroup::CodeQuality | CheckGroup::ProjectStructure
) || matches!(
(group, principle),
(CheckGroup::P1, 1)
| (CheckGroup::P2, 2)
| (CheckGroup::P3, 3)
| (CheckGroup::P4, 4)
| (CheckGroup::P5, 5)
| (CheckGroup::P6, 6)
| (CheckGroup::P7, 7)
)
}