use camino::Utf8PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use mollify_types::{Confidence, Finding, Report, Severity, Summary};
mod osv;
mod update_check;
#[derive(Parser)]
#[command(
name = "mollify",
version,
about = "Deterministic codebase intelligence for Python — evidence, not decisions."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Audit(Scope),
#[command(name = "dead-code", alias = "check")]
DeadCode(Scope),
Deps(Scope),
Arch(Scope),
#[command(name = "complexity", alias = "health")]
Complexity(Scope),
Dupes(Scope),
Types(Scope),
Security(Scope),
Coverage(CoverageArgs),
#[command(name = "supply-chain")]
SupplyChain(SupplyChainArgs),
Fix(FixArgs),
Explain(ExplainArgs),
Trace(TraceArgs),
Watch(WatchArgs),
Inspect(InspectArgs),
List(ListArgs),
Metrics(MetricsArgs),
Graph(GraphArgs),
Init(InitArgs),
Mcp,
Lsp,
}
#[derive(clap::Args)]
struct InitArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, value_name = "AGENT")]
agent: Vec<String>,
#[arg(long)]
all: bool,
#[arg(long)]
force: bool,
}
#[derive(clap::Args)]
struct CoverageArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long)]
coverage_file: Utf8PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
}
#[derive(clap::Args)]
struct ExplainArgs {
rule: Option<String>,
}
#[derive(clap::Args)]
struct TraceArgs {
module: String,
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
}
#[derive(clap::Args)]
struct SupplyChainArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long)]
advisory_db: Option<Utf8PathBuf>,
#[arg(long)]
offline: bool,
#[arg(long)]
refresh: bool,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
}
#[derive(clap::Args)]
struct InspectArgs {
file: String,
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
}
#[derive(clap::Args)]
struct ListArgs {
#[arg(value_enum, default_value_t = ListKind::EntryPoints)]
kind: ListKind,
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum ListKind {
#[value(name = "entry-points")]
EntryPoints,
Files,
Frameworks,
}
#[derive(clap::Args)]
struct MetricsArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
}
#[derive(clap::Args)]
struct GraphArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long)]
mermaid: bool,
}
#[derive(clap::Args)]
struct WatchArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, default_value_t = 1000)]
interval_ms: u64,
}
#[derive(clap::Args)]
struct FixArgs {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long)]
apply: bool,
}
#[derive(clap::Args)]
struct Scope {
#[arg(long, default_value = ".")]
path: Utf8PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
#[arg(long, value_enum, default_value_t = Gate::All)]
gate: Gate,
#[arg(long)]
base: Option<String>,
#[arg(long)]
save_baseline: Option<Utf8PathBuf>,
#[arg(long)]
baseline: Option<Utf8PathBuf>,
#[arg(long)]
fail_on_regression: bool,
#[arg(long)]
brief: bool,
#[arg(long, value_enum)]
min_confidence: Option<ConfidenceArg>,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum ConfidenceArg {
Certain,
Likely,
Uncertain,
}
fn apply_min_confidence(s: &Scope, findings: &mut Vec<Finding>) {
let Some(min) = s.min_confidence else { return };
let threshold = match min {
ConfidenceArg::Certain => Confidence::Certain,
ConfidenceArg::Likely => Confidence::Likely,
ConfidenceArg::Uncertain => Confidence::Uncertain,
};
findings.retain(|f| f.confidence <= threshold);
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum Format {
Human,
Json,
Sarif,
Github,
Junit,
}
fn github_annotations(findings: &[Finding]) -> String {
let mut s = String::new();
for f in findings {
let level = if f.severity == Severity::Error {
"error"
} else {
"warning"
};
s.push_str(&format!(
"::{level} file={},line={}::{}: {}\n",
f.location.path, f.location.line, f.rule, f.reason
));
}
s
}
fn junit_xml(findings: &[Finding], suite: &str) -> String {
fn esc(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
let mut s = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
s.push_str(&format!(
"<testsuites>\n <testsuite name=\"mollify:{}\" tests=\"{}\" failures=\"{}\">\n",
esc(suite),
findings.len(),
findings.len()
));
for f in findings {
let name = format!("{}: {}:{}", f.rule, f.location.path, f.location.line);
s.push_str(&format!(
" <testcase name=\"{}\" classname=\"{}\">\n <failure message=\"{}\"/>\n </testcase>\n",
esc(&name),
esc(f.rule.as_str()),
esc(&f.reason)
));
}
s.push_str(" </testsuite>\n</testsuites>\n");
s
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum Gate {
All,
#[value(name = "new-only")]
NewOnly,
}
fn apply_gate(scope: &Scope, findings: &mut Vec<mollify_types::Finding>) {
use mollify_types::Attribution;
if scope.gate == Gate::All && scope.base.is_none() {
return; }
let Some(changed) = mollify_core::git::changed_files(&scope.path, scope.base.as_deref()) else {
eprintln!("mollify: --gate requested but this isn't a git repo; reporting all findings.");
return;
};
let lines = mollify_core::git::changed_lines(&scope.path, scope.base.as_deref());
for f in findings.iter_mut() {
let file_changed =
mollify_core::git::path_is_changed(&scope.path, &f.location.path, &changed);
let introduced = match lines.as_ref().and_then(|m| {
mollify_core::git::line_is_changed(&scope.path, &f.location.path, f.location.line, m)
}) {
Some(in_hunk) => in_hunk,
None => file_changed,
};
f.attribution = Some(if introduced {
Attribution::Introduced
} else {
Attribution::Inherited
});
}
if scope.gate == Gate::NewOnly {
findings.retain(|f| f.attribution == Some(Attribution::Introduced));
}
}
fn main() {
let cli = Cli::parse();
let code = match cli.command {
Command::Audit(s) => run_audit(&s),
Command::DeadCode(s) => run_findings(
&s,
mollify_core::dead_code_report,
Report::DeadCode,
"dead-code",
),
Command::Deps(s) => run_findings(&s, mollify_core::deps_report, Report::Deps, "deps"),
Command::Arch(s) => run_findings(&s, mollify_core::arch_report, Report::Arch, "arch"),
Command::Complexity(s) => run_findings(
&s,
mollify_core::complexity_report,
Report::Complexity,
"complexity",
),
Command::Dupes(s) => run_findings(&s, mollify_core::dupes_report, Report::Dupes, "dupes"),
Command::Types(s) => run_findings(&s, mollify_core::types_report, Report::Types, "types"),
Command::Security(s) => run_findings(
&s,
mollify_core::security_report,
Report::Security,
"security",
),
Command::Coverage(a) => run_coverage(&a),
Command::SupplyChain(a) => run_supply_chain(&a),
Command::Fix(a) => run_fix(&a),
Command::Explain(a) => run_explain(&a),
Command::Trace(a) => run_trace(&a),
Command::Watch(a) => run_watch(&a),
Command::Inspect(a) => run_inspect(&a),
Command::List(a) => run_list(&a),
Command::Metrics(a) => run_metrics(&a),
Command::Graph(a) => {
print!("{}", mollify_core::graph_export(&a.path, a.mermaid));
0
}
Command::Init(a) => run_init(&a),
Command::Mcp => match mollify_mcp::run() {
Ok(()) => 0,
Err(e) => {
eprintln!("mollify mcp: {e}");
1
}
},
Command::Lsp => match mollify_lsp::run() {
Ok(()) => 0,
Err(e) => {
eprintln!("mollify lsp: {e}");
1
}
},
};
std::process::exit(code);
}
enum BaselineOutcome {
Saved(Utf8PathBuf),
Filtered(usize),
None,
}
fn handle_baseline(s: &Scope, findings: &mut Vec<mollify_types::Finding>) -> BaselineOutcome {
use mollify_core::baseline::{split_new, Baseline};
if let Some(path) = &s.save_baseline {
let b = Baseline::from_findings(findings);
if let Err(e) = b.save(path) {
eprintln!("error: could not write baseline {path}: {e}");
}
return BaselineOutcome::Saved(path.clone());
}
if let Some(path) = &s.baseline {
let Some(b) = Baseline::load(path) else {
eprintln!("mollify: baseline {path} missing or invalid; reporting all findings.");
return BaselineOutcome::None;
};
let new_count = {
let (new, _known) = split_new(findings, &b);
let keep: std::collections::HashSet<String> =
new.iter().map(|f| f.fingerprint.clone()).collect();
findings.retain(|f| keep.contains(&f.fingerprint));
findings.len()
};
return BaselineOutcome::Filtered(new_count);
}
BaselineOutcome::None
}
fn gated_exit(s: &Scope, errors: usize, outcome: &BaselineOutcome) -> i32 {
if s.brief {
return 0;
}
if s.fail_on_regression {
if let BaselineOutcome::Filtered(n) = outcome {
if *n > 0 {
return 1;
}
}
}
exit_code(errors)
}
fn run_audit(s: &Scope) -> i32 {
let mut report = mollify_core::audit_report(&s.path);
apply_gate(s, &mut report.findings);
apply_min_confidence(s, &mut report.findings);
let outcome = handle_baseline(s, &mut report.findings);
if let BaselineOutcome::Saved(p) = &outcome {
println!(
"Wrote baseline with {} fingerprint(s) to {p}",
report.findings.len()
);
return 0;
}
report.summary =
mollify_types::Summary::from_findings(&report.findings, report.summary.files_analyzed);
let errors = report.summary.errors;
match s.format {
Format::Json => println!(
"{}",
serde_json::to_string_pretty(&Report::Audit(report)).unwrap()
),
Format::Sarif => println!(
"{}",
serde_json::to_string_pretty(&mollify_core::sarif::to_sarif(
&report.findings,
env!("CARGO_PKG_VERSION")
))
.unwrap()
),
Format::Github => print!("{}", github_annotations(&report.findings)),
Format::Junit => print!("{}", junit_xml(&report.findings, "audit")),
Format::Human => {
println!("Mollify audit — {}", s.path);
println!("Quality score: {}/100", report.quality_score);
print_summary(&report.summary);
print_findings(&report.findings);
update_check::maybe_nudge();
}
}
gated_exit(s, errors, &outcome)
}
fn run_findings(
s: &Scope,
f: fn(&camino::Utf8Path) -> mollify_types::FindingsReport,
wrap: fn(mollify_types::FindingsReport) -> Report,
label: &str,
) -> i32 {
let mut report = f(&s.path);
apply_gate(s, &mut report.findings);
apply_min_confidence(s, &mut report.findings);
let outcome = handle_baseline(s, &mut report.findings);
if let BaselineOutcome::Saved(p) = &outcome {
println!(
"Wrote baseline with {} fingerprint(s) to {p}",
report.findings.len()
);
return 0;
}
report.summary =
mollify_types::Summary::from_findings(&report.findings, report.summary.files_analyzed);
let errors = report.summary.errors;
match s.format {
Format::Json => println!("{}", serde_json::to_string_pretty(&wrap(report)).unwrap()),
Format::Sarif => println!(
"{}",
serde_json::to_string_pretty(&mollify_core::sarif::to_sarif(
&report.findings,
env!("CARGO_PKG_VERSION")
))
.unwrap()
),
Format::Github => print!("{}", github_annotations(&report.findings)),
Format::Junit => print!("{}", junit_xml(&report.findings, label)),
Format::Human => {
println!("Mollify {label} — {}", s.path);
print_summary(&report.summary);
print_findings(&report.findings);
update_check::maybe_nudge();
}
}
gated_exit(s, errors, &outcome)
}
fn run_coverage(a: &CoverageArgs) -> i32 {
let report = mollify_core::coverage_report(&a.path, &a.coverage_file);
let errors = report.summary.errors;
match a.format {
Format::Json => println!(
"{}",
serde_json::to_string_pretty(&Report::Coverage(report)).unwrap()
),
Format::Sarif => println!(
"{}",
serde_json::to_string_pretty(&mollify_core::sarif::to_sarif(
&report.findings,
env!("CARGO_PKG_VERSION")
))
.unwrap()
),
Format::Github => print!("{}", github_annotations(&report.findings)),
Format::Junit => print!("{}", junit_xml(&report.findings, "coverage")),
Format::Human => {
println!("Mollify coverage — {}", a.path);
print_summary(&report.summary);
print_findings(&report.findings);
}
}
exit_code(errors)
}
fn run_supply_chain(a: &SupplyChainArgs) -> i32 {
let db = a
.advisory_db
.clone()
.unwrap_or_else(|| a.path.join(mollify_core::DEFAULT_ADVISORY_DB));
let report = if a.offline {
if !db.exists() {
eprintln!(
"No advisory DB at {db}. Drop `--offline` to fetch live from OSV, run \
`python3 scripts/fetch-advisories.py {db}`, or pass --advisory-db <path>."
);
return 1;
}
eprintln!("supply-chain: offline mode, using advisory DB {db}.");
mollify_core::supply_chain_report(&a.path, &db)
} else {
let pins = mollify_core::supplychain::collect_pins(&a.path);
match osv::fetch_for_pins(&pins) {
Ok(advisories) => {
eprintln!(
"supply-chain: live OSV data for {} pinned package(s).",
pins.len()
);
if a.refresh {
match osv::write_db(&db, &advisories) {
Ok(()) => eprintln!(
"supply-chain: cached {} advisory(ies) to {db}.",
advisories.len()
),
Err(e) => eprintln!("supply-chain: could not write cache {db}: {e}"),
}
}
mollify_core::supply_chain_report_with(&a.path, &advisories)
}
Err(e) => {
if db.exists() {
eprintln!("supply-chain: live fetch failed ({e}); falling back to DB {db}.");
mollify_core::supply_chain_report(&a.path, &db)
} else {
eprintln!(
"supply-chain: live fetch failed ({e}) and no local DB at {db}. \
Pass --advisory-db, run scripts/fetch-advisories.py, or check connectivity."
);
mollify_core::supply_chain_report_with(&a.path, &[])
}
}
}
};
let errors = report.summary.errors;
match a.format {
Format::Json => println!(
"{}",
serde_json::to_string_pretty(&Report::Security(report)).unwrap()
),
Format::Sarif => println!(
"{}",
serde_json::to_string_pretty(&mollify_core::sarif::to_sarif(
&report.findings,
env!("CARGO_PKG_VERSION")
))
.unwrap()
),
Format::Github => print!("{}", github_annotations(&report.findings)),
Format::Junit => print!("{}", junit_xml(&report.findings, "supply-chain")),
Format::Human => {
println!("Mollify supply-chain — {} (db: {db})", a.path);
print_summary(&report.summary);
print_findings(&report.findings);
}
}
exit_code(errors)
}
fn run_fix(a: &FixArgs) -> i32 {
let edits = mollify_core::fix::plan(&a.path);
if edits.is_empty() {
println!("No auto-fixable findings (only `certain` unused symbols are auto-fixed). ✓");
return 0;
}
println!(
"{} safe fix(es){}:",
edits.len(),
if a.apply {
""
} else {
" (dry-run — pass --apply to write)"
}
);
for e in &edits {
println!(
" {}:{}-{} {}",
e.path, e.start_line, e.end_line, e.description
);
}
if !a.apply {
return 0;
}
match mollify_core::fix::apply(&edits) {
Ok(n) => {
println!("Applied {n} fix(es). Re-run `mollify audit` to confirm.");
0
}
Err(e) => {
eprintln!("error: applying fixes: {e}");
1
}
}
}
fn run_explain(a: &ExplainArgs) -> i32 {
match &a.rule {
Some(rule) => match mollify_core::explain::text(rule) {
Some(t) => {
println!("{rule}\n {t}");
0
}
None => {
eprintln!("Unknown rule `{rule}`. Run `mollify explain` to list all rules.");
1
}
},
None => {
println!("Mollify rules (run `mollify explain <rule>` for details):");
for r in mollify_core::explain::RULES {
println!(" {r}");
}
0
}
}
}
fn run_trace(a: &TraceArgs) -> i32 {
let graph = mollify_core::build_graph(&a.path);
let Some(t) = mollify_core::trace::module(&graph, &a.module) else {
eprintln!("No module matching `{}` found under {}.", a.module, a.path);
return 1;
};
match a.format {
Format::Json => println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"kind": "trace",
"target": t.target,
"imports": t.imports,
"imported_by": t.imported_by,
}))
.unwrap()
),
_ => {
println!("Trace — {}", t.target);
println!(" imports ({}):", t.imports.len());
for m in &t.imports {
println!(" → {m}");
}
println!(" imported by ({}):", t.imported_by.len());
for m in &t.imported_by {
println!(" ← {m}");
}
}
}
0
}
fn watch_signature(root: &camino::Utf8Path) -> Vec<(String, u64, u64)> {
let mut sig: Vec<(String, u64, u64)> = mollify_core::build_graph(root)
.modules
.iter()
.map(|m| {
let meta = std::fs::metadata(&m.path).ok();
let mtime = meta
.as_ref()
.and_then(|x| x.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
let len = meta.as_ref().map(|x| x.len()).unwrap_or(0);
(m.path.to_string(), mtime, len)
})
.collect();
sig.sort();
sig
}
fn run_watch(a: &WatchArgs) -> i32 {
let scope = Scope {
path: a.path.clone(),
format: Format::Human,
gate: Gate::All,
base: None,
save_baseline: None,
baseline: None,
fail_on_regression: false,
brief: false,
min_confidence: None,
};
println!(
"Watching {} (every {}ms) — Ctrl-C to stop.\n",
a.path, a.interval_ms
);
let mut last: Option<Vec<(String, u64, u64)>> = None;
loop {
let sig = watch_signature(&a.path);
if last.as_ref() != Some(&sig) {
println!("── re-running audit ──");
run_audit(&scope);
println!();
last = Some(sig);
}
std::thread::sleep(std::time::Duration::from_millis(a.interval_ms));
}
}
fn run_inspect(a: &InspectArgs) -> i32 {
let ins = mollify_core::inspect(&a.path, &a.file);
match a.format {
Format::Json => {
let body = serde_json::json!({
"kind": "inspect",
"file": ins.file,
"module": ins.module,
"findings": ins.findings,
"imports": ins.imports,
"imported_by": ins.imported_by,
});
println!("{}", serde_json::to_string_pretty(&body).unwrap());
}
_ => {
println!("Mollify inspect — {}", ins.file);
if let Some(m) = &ins.module {
println!("module: {m}");
}
println!(
"imports {} module(s); imported by {} module(s)",
ins.imports.len(),
ins.imported_by.len()
);
println!("{} finding(s):", ins.findings.len());
let refs: Vec<&Finding> = ins.findings.iter().collect();
print_findings_refs(&refs);
}
}
0
}
fn run_list(a: &ListArgs) -> i32 {
let label = match a.kind {
ListKind::EntryPoints => "entry-points",
ListKind::Files => "files",
ListKind::Frameworks => "frameworks",
};
let rows = mollify_core::list_topology(&a.path, label);
match a.format {
Format::Json => {
println!(
"{}",
serde_json::to_string_pretty(
&serde_json::json!({ "kind": "list", "of": label, "items": rows })
)
.unwrap()
);
}
_ => {
println!("Mollify list:{label} — {} item(s)", rows.len());
for r in &rows {
println!(" {}", r.replace('\t', " "));
}
}
}
0
}
fn run_metrics(a: &MetricsArgs) -> i32 {
let report = mollify_core::metrics::report(&a.path);
match a.format {
Format::Json => println!(
"{}",
serde_json::to_string_pretty(&Report::Metrics(report)).unwrap()
),
_ => {
println!("Mollify metrics — {}", a.path);
println!(
"{} file(s), {} LOC ({} SLOC), {} function(s); mean MI {:.1}",
report.totals.files,
report.totals.loc,
report.totals.sloc,
report.totals.functions,
report.totals.mean_maintainability_index
);
for f in &report.files {
println!(
" [{}] MI {:>5.1} cc(max {:>2}, sum {:>3}) {} sloc {}",
f.mi_rank,
f.maintainability_index,
f.max_cyclomatic,
f.total_cyclomatic,
f.sloc,
f.path
);
}
}
}
0
}
fn run_init(a: &InitArgs) -> i32 {
if a.all || !a.agent.is_empty() {
return run_init_agents(a);
}
let cfg = a.path.join(".mollifyrc.json");
if cfg.exists() {
println!("{cfg} already exists; leaving it untouched.");
return 0;
}
let default = "{\n \"source_roots\": [\".\", \"src\"],\n \"severity\": { \"dead-code\": \"warn\", \"dependency-hygiene\": \"warn\" }\n}\n";
match std::fs::write(&cfg, default) {
Ok(()) => {
println!("Wrote {cfg}");
0
}
Err(e) => {
eprintln!("error: could not write {cfg}: {e}");
1
}
}
}
fn run_init_agents(a: &InitArgs) -> i32 {
use mollify_core::agents::{self, Agent, FileOutcome};
let agents: Vec<Agent> = if a.all {
Agent::ALL.to_vec()
} else {
let mut resolved = Vec::new();
for name in &a.agent {
match Agent::parse(name) {
Some(ag) => resolved.push(ag),
None => {
eprintln!(
"Unknown agent `{name}`. Valid: claude, cursor, gemini, codex, cascade (or --all)."
);
return 1;
}
}
}
resolved
};
let mut created = 0usize;
let mut overwritten = 0usize;
let mut skipped = 0usize;
for ag in agents {
match agents::install(&a.path, ag, a.force) {
Ok(files) => {
println!("{} — {} file(s):", ag.name(), files.len());
for f in &files {
let tag = match f.outcome {
FileOutcome::Created => {
created += 1;
"create"
}
FileOutcome::Overwritten => {
overwritten += 1;
"overwrite"
}
FileOutcome::Skipped => {
skipped += 1;
"skip"
}
};
println!(" [{tag}] {}", f.path);
}
}
Err(e) => {
eprintln!("error: installing {} integration: {e}", ag.name());
return 1;
}
}
}
println!("\nDone: {created} created, {overwritten} overwritten, {skipped} skipped.");
if skipped > 0 && !a.force {
println!("Re-run with --force to overwrite the skipped files.");
}
0
}
fn print_summary(s: &Summary) {
println!(
"{} finding(s) across {} file(s) — {} error, {} warn{}",
s.total,
s.files_analyzed,
s.errors,
s.warnings,
if s.introduced > 0 {
format!(", {} introduced", s.introduced)
} else {
String::new()
}
);
}
fn print_findings(findings: &[Finding]) {
let refs: Vec<&Finding> = findings.iter().collect();
print_findings_refs(&refs);
}
fn print_findings_refs(findings: &[&Finding]) {
if findings.is_empty() {
println!(" No findings. ✓");
return;
}
for f in findings {
let sev = match f.severity {
Severity::Error => "error",
Severity::Warn => "warn",
Severity::Off => "off",
};
let conf = match f.confidence {
Confidence::Certain => "certain",
Confidence::Likely => "likely",
Confidence::Uncertain => "uncertain",
};
let loc = &f.location;
println!(
" {}:{} [{sev}/{conf}] {} — {} ({})",
loc.path, loc.line, f.rule, f.reason, f.fingerprint
);
}
}
fn exit_code(errors: usize) -> i32 {
if errors > 0 {
1
} else {
0
}
}