use std::cmp::Ordering;
use aristo_core::config::ConfigFile;
use aristo_core::index::{AnnotationId, IndexEntry};
use crate::commands::index::workspace_or_error;
use crate::commands::show::read_index;
use crate::preflight::{emit_advisory_if_stale, freshness_check};
use crate::{CliError, CliResult};
mod fix;
mod rules;
use rules::Severity;
pub(crate) fn run(check: bool, fix: bool, strict: bool) -> CliResult<()> {
if fix {
let ws = workspace_or_error()?;
crate::session::guard::ensure_no_active_session(&ws, "aristo lint --fix")?;
let (issues, files) = self::fix::run_fix(&ws)?;
println!(
"fixed: {issues} whitespace {issue_word} across {files} {file_word}",
issue_word = if issues == 1 { "issue" } else { "issues" },
file_word = if files == 1 { "file" } else { "files" },
);
return Ok(());
}
if !check {
}
let ws = workspace_or_error()?;
emit_advisory_if_stale(&freshness_check(&ws));
let index = read_index(&ws.index_path())?;
let overrides = load_rule_overrides(&ws);
let mut findings: Vec<Finding> = Vec::new();
for (id, entry) in &index.entries {
let text = entry_text(entry);
let file = entry_file(entry).to_string();
for outcome in rules::run_check_rules(text, &overrides) {
findings.push(Finding {
id: id.clone(),
file: file.clone(),
rule: outcome.rule_name,
severity: outcome.severity,
message: outcome.message,
});
}
}
findings.sort_by(|a, b| match a.id.cmp(&b.id) {
Ordering::Equal => a.rule.cmp(b.rule),
other => other,
});
report(&findings, strict)
}
fn report(findings: &[Finding], strict: bool) -> CliResult<()> {
let errors = findings
.iter()
.filter(|f| f.severity == Severity::Error)
.count();
let warns = findings
.iter()
.filter(|f| f.severity == Severity::Warn)
.count();
let infos = findings
.iter()
.filter(|f| f.severity == Severity::Info)
.count();
let fail = errors > 0 || (strict && warns > 0);
if findings.is_empty() {
println!("ok: 0 lint findings.");
return Ok(());
}
if fail {
let triggering = if errors > 0 {
format!("{errors} lint finding (error severity)")
} else {
format!("{warns} lint finding (warn severity, --strict)")
};
println!("error: {triggering}");
for f in findings {
if f.severity == Severity::Error || (strict && f.severity == Severity::Warn) {
let line = "[..]";
println!(
" \u{2022} {id} ({file}:{line}): {rule} ({msg})",
id = f.id,
file = f.file,
rule = f.rule,
msg = f.message,
);
}
}
let _ = (errors, warns, infos);
return Err(CliError::Silent { exit_code: 1 });
}
println!(
"ok: {} finding{} ({errors} error, {warns} warn, {infos} info).",
findings.len(),
if findings.len() == 1 { "" } else { "s" }
);
Ok(())
}
struct Finding {
id: AnnotationId,
file: String,
rule: &'static str,
severity: Severity,
message: String,
}
fn entry_text(entry: &IndexEntry) -> &str {
match entry {
IndexEntry::Intent(e) => &e.text,
IndexEntry::Assume(e) => &e.text,
}
}
fn entry_file(entry: &IndexEntry) -> &str {
match entry {
IndexEntry::Intent(e) => &e.file,
IndexEntry::Assume(e) => &e.file,
}
}
fn load_rule_overrides(ws: &crate::Workspace) -> rules::Overrides {
let path = ws.config_path();
let Ok(text) = std::fs::read_to_string(&path) else {
return rules::Overrides::default();
};
let cfg: Result<ConfigFile, _> = toml::from_str(&text);
let Ok(cfg) = cfg else {
return rules::Overrides::default();
};
rules::Overrides::from_config(&cfg)
}