mod cli;
mod config;
mod diagnostics;
mod explain;
mod lints;
mod output;
mod parser;
mod report;
use anyhow::Result;
use cli::{Args, Command, OutputFormat};
use clap::Parser;
fn print_deprecation_warning() {
eprintln!("\x1b[33m"); eprintln!("╔════════════════════════════════════════════════════════════════════╗");
eprintln!("║ ⚠️ DEPRECATION NOTICE ║");
eprintln!("╠════════════════════════════════════════════════════════════════════╣");
eprintln!("║ cargo-fa is deprecated and will not receive new features. ║");
eprintln!("║ ║");
eprintln!("║ Please migrate to `cargo-memlense` (available with memkit 0.12+) ║");
eprintln!("║ which provides improved diagnostics and analysis capabilities. ║");
eprintln!("║ ║");
eprintln!("║ This tool remains functional but is no longer maintained. ║");
eprintln!("╚════════════════════════════════════════════════════════════════════╝");
eprintln!("\x1b[0m"); eprintln!();
}
fn main() -> Result<()> {
print_deprecation_warning();
let args = Args::parse();
let args = if args.subcommand.is_some() {
args
} else {
Args::parse_from(std::env::args())
};
if let Some(ref cmd) = args.command {
return handle_subcommand(cmd, &args);
}
let analyzer = Analyzer::new(args)?;
let report = analyzer.run()?;
report.print_with_format(&analyzer.args.format);
if report.has_errors() {
std::process::exit(1);
} else if report.has_warnings() && analyzer.args.deny_warnings {
std::process::exit(1);
} else if analyzer.has_denied_issues(&report) {
std::process::exit(1);
}
Ok(())
}
fn handle_subcommand(cmd: &Command, args: &Args) -> Result<()> {
match cmd {
Command::Explain { code } => {
if let Some(explanation) = explain::get_explanation(code) {
explain::print_explanation(&explanation);
} else {
eprintln!("Unknown diagnostic code: {}", code);
eprintln!("Run `cargo fa list` to see all available codes.");
std::process::exit(1);
}
}
Command::Show { file, fixes } => {
let config = config::Config::load(args)?;
if !file.exists() {
eprintln!("File not found: {}", file.display());
std::process::exit(1);
}
println!("{}", output::header(&format!("Analyzing {}", file.display())));
match parser::parse_file(file) {
Ok(ast) => {
let mut all_diags = Vec::new();
all_diags.extend(lints::dirtymem::check(&ast, file, &config));
all_diags.extend(lints::threading::check(&ast, file, &config));
all_diags.extend(lints::budgets::check(&ast, file, &config));
all_diags.extend(lints::async_safety::check(&ast, file, &config));
all_diags.extend(lints::gpu::check(&ast, file, &config));
all_diags.extend(lints::architecture::check(&ast, file, &config));
all_diags.extend(lints::rapier::check(&ast, file, &config));
if all_diags.is_empty() {
println!("No issues found in {}.", file.display());
} else {
for diag in &all_diags {
output::print_diagnostic(diag, &OutputFormat::Terminal);
}
output::print_summary(
all_diags.iter().filter(|d| d.severity == cli::Severity::Error).count(),
all_diags.iter().filter(|d| d.severity == cli::Severity::Warning).count(),
all_diags.iter().filter(|d| d.severity == cli::Severity::Hint).count(),
);
}
}
Err(e) => {
eprintln!("Failed to parse {}: {}", file.display(), e);
std::process::exit(1);
}
}
}
Command::Init { force } => {
let config_path = args.path.join(".fa.toml");
if config_path.exists() && !force {
eprintln!(".fa.toml already exists. Use --force to overwrite.");
std::process::exit(1);
}
std::fs::write(&config_path, config::generate_default_config())?;
println!("Created {}", config_path.display());
}
Command::List { category } => {
explain::list_all_codes(category.as_deref());
}
}
Ok(())
}
struct Analyzer {
args: Args,
config: config::Config,
}
impl Analyzer {
fn new(args: Args) -> Result<Self> {
let config = config::Config::load(&args)?;
Ok(Self { args, config })
}
fn run(&self) -> Result<report::Report> {
let mut report = report::Report::new();
let mut source_files = parser::find_rust_files(&self.args.path)?;
if !self.args.exclude.is_empty() {
source_files.retain(|path| {
let path_str = path.to_string_lossy();
!self.args.exclude.iter().any(|pattern| {
glob::Pattern::new(pattern)
.map(|p| p.matches(&path_str))
.unwrap_or(false)
})
});
}
let quiet = !matches!(self.args.format, OutputFormat::Terminal);
if !quiet {
println!(
"{}",
output::header(&format!("Analyzing {} files", source_files.len()))
);
}
for file_path in &source_files {
if self.args.verbose {
println!(" Checking: {}", file_path.display());
}
match parser::parse_file(file_path) {
Ok(ast) => {
let mut file_diagnostics = self.analyze_file(&ast, file_path);
file_diagnostics.retain(|d| {
!self.args.allow.contains(&d.code.code)
});
for diag in &mut file_diagnostics {
if self.args.deny.contains(&diag.code.code) {
diag.severity = cli::Severity::Error;
}
}
if let Some(ref only) = self.args.only {
file_diagnostics.retain(|d| only.contains(&d.code.code));
}
if let Some(ref skip) = self.args.skip {
file_diagnostics.retain(|d| !skip.contains(&d.code.code));
}
file_diagnostics.retain(|d| d.severity >= self.args.min_severity);
report.add_diagnostics(file_diagnostics);
if self.args.fail_fast && report.has_errors() {
break;
}
}
Err(e) => {
eprintln!("ERROR: Failed to parse {}: {}", file_path.display(), e);
std::process::exit(1);
}
}
}
Ok(report)
}
fn analyze_file(
&self,
ast: &syn::File,
path: &std::path::Path,
) -> Vec<diagnostics::Diagnostic> {
let mut diagnostics = Vec::new();
if self.args.architecture || self.args.all {
diagnostics.extend(lints::architecture::check(ast, path, &self.config));
}
if self.args.dirtymem || self.args.all {
diagnostics.extend(lints::dirtymem::check(ast, path, &self.config));
}
if self.args.budgets || self.args.all {
diagnostics.extend(lints::budgets::check(ast, path, &self.config));
}
if self.args.async_safety || self.args.all {
diagnostics.extend(lints::async_safety::check(ast, path, &self.config));
}
if self.args.gpu || self.args.all {
diagnostics.extend(lints::gpu::check(ast, path, &self.config));
}
if self.args.threading || self.args.all {
diagnostics.extend(lints::threading::check(ast, path, &self.config));
}
if self.args.all {
diagnostics.extend(lints::rapier::check(ast, path, &self.config));
}
diagnostics
}
fn has_denied_issues(&self, report: &report::Report) -> bool {
if self.args.deny.is_empty() {
return false;
}
report.diagnostics().iter().any(|d| {
self.args.deny.contains(&d.code.code)
})
}
}