use std::{fs, path::PathBuf};
use bulloak_foundry::{
check::{
context::{fix_order, Context},
rules::{self, Checker},
},
sol::find_contract,
violation::{Violation, ViolationKind},
};
use bulloak_syntax::utils::pluralize;
use clap::Parser;
use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize};
use crate::{cli::Cli, glob::expand_glob};
#[doc(hidden)]
#[derive(Debug, Parser, Clone, Serialize, Deserialize)]
pub struct Check {
pub files: Vec<PathBuf>,
#[arg(long, group = "fix-violations", default_value_t = false)]
pub fix: bool,
#[arg(long, requires = "fix-violations", default_value_t = false)]
pub stdout: bool,
#[arg(short = 'm', long, default_value_t = false)]
pub skip_modifiers: bool,
#[arg(long = "format-descriptions", default_value_t = false)]
pub format_descriptions: bool,
}
impl Default for Check {
fn default() -> Self {
Check::parse_from(Vec::<String>::new())
}
}
impl Check {
pub(crate) fn run(&self, cfg: &Cli) {
let mut specs = Vec::new();
for pattern in &self.files {
match expand_glob(pattern.clone()) {
Ok(iter) => specs.extend(iter),
Err(e) => eprintln!(
"{}: could not expand {}: {}",
"warn".yellow(),
pattern.display(),
e
),
}
}
let mut violations = Vec::new();
let ctxs: Vec<Context> = specs
.iter()
.filter_map(|tree_path| {
Context::new(tree_path.clone(), &cfg.into())
.map_err(|violation| violations.push(violation))
.ok()
})
.collect();
if !self.fix {
for ctx in ctxs {
violations.append(&mut rules::StructuralMatcher::check(&ctx));
}
return exit(&violations);
}
let mut fixed_count = 0;
for mut ctx in ctxs {
let violations = rules::StructuralMatcher::check(&ctx);
let fixable_count =
violations.iter().filter(|v| v.is_fixable()).count();
let violations = violations.iter().filter(|v| {
!matches!(v.kind, ViolationKind::FunctionOrderMismatch(_, _, _))
});
for violation in violations {
ctx = match violation.kind.fix(ctx.clone()) {
Ok(ctx) => ctx,
Err(e) => {
eprintln!(
"unable to fix \"{}\" due to:\n{}",
violation.kind, e
);
continue;
}
};
}
let violations = rules::StructuralMatcher::check(&ctx);
let violations: Vec<Violation> = violations
.into_iter()
.filter(|v| {
matches!(
v.kind,
ViolationKind::FunctionOrderMismatch(_, _, _)
)
})
.collect();
if !violations.is_empty() {
if let Some(contract_sol) = find_contract(&ctx.pt) {
if let Some(contract_hir) = ctx.hir.clone().find_contract()
{
ctx = fix_order(
&violations,
&contract_sol,
contract_hir,
ctx,
);
}
}
}
let sol = ctx.sol.clone();
let formatted =
ctx.fmt().expect("should format the emitted solidity code");
self.write(&formatted, sol);
fixed_count += fixable_count;
}
let issue_literal = pluralize(fixed_count, "issue", "issues");
println!(
"\n{}: {} {} fixed.",
"success".bold().green(),
fixed_count,
issue_literal
);
}
fn write(&self, output: &str, sol: PathBuf) {
if self.stdout {
println!("{} {}", "-->".blue(), sol.to_string_lossy());
println!("{}", output.trim());
println!("{}", "<--".blue());
} else if let Err(e) = fs::write(sol, output) {
eprintln!("{}: {e}", "warn".yellow());
}
}
}
fn exit(violations: &[Violation]) {
if violations.is_empty() {
println!(
"{}",
"All checks completed successfully! No issues found.".green()
);
} else {
for violation in violations {
eprintln!("{violation}");
}
let check_literal = pluralize(violations.len(), "check", "checks");
eprint!(
"{}: {} {} failed",
"warn".bold().yellow(),
violations.len(),
check_literal
);
let fixable_count =
violations.iter().filter(|v| v.is_fixable()).count();
if fixable_count > 0 {
let fix_literal = pluralize(fixable_count, "fix", "fixes");
eprintln!(
" (run `bulloak check --fix <.tree files>` to apply {fixable_count} {fix_literal})"
);
} else {
eprintln!();
}
std::process::exit(1);
}
}