use crate::*;
fn reject_unsupported_advisory_format(
format: OutputFormat,
command_name: &str,
) -> Result<(), CliError> {
if matches!(format, OutputFormat::Sarif | OutputFormat::Markdown) {
return Err(unsupported_format(command_name, format, "tty, json"));
}
Ok(())
}
fn to_json_string<T: Serialize + ?Sized>(value: &T) -> Result<String, CliError> {
Ok(serde_json::to_string_pretty(value)?)
}
fn to_cli_result(result: std::io::Result<()>) -> Result<(), CliError> {
Ok(result?)
}
fn render_advisory(
out: &mut dyn Write,
format: OutputFormat,
json: impl FnOnce() -> Result<String, CliError>,
tty: impl FnOnce(&mut dyn Write) -> Result<(), CliError>,
) -> Result<CommandOutcome, CliError> {
match format {
OutputFormat::Json => writeln!(out, "{}", json()?)?,
OutputFormat::Sarif | OutputFormat::Markdown => {
unreachable!("rejected above before dispatch")
}
OutputFormat::Tty => tty(out)?,
}
Ok(CommandOutcome::Clean)
}
fn collect_pattern_candidates(
workspace: &judge::ingest::Workspace,
clippy_json: Option<&Path>,
) -> Result<Vec<judge::rules::pattern::PatternCandidate>, CliError> {
let slop_source_files = workspace
.crates
.iter()
.flat_map(|krate| krate.source_files.iter());
let rules_config = load_judge_toml(&workspace.root)?.rules;
let slop = judge::rules::slop::analyze_workspace(
slop_source_files,
false,
rules_config.catch_all_error.allow_anyhow_at_boundary,
);
let clippy_hits = match clippy_json {
Some(path) => judge::advisory::clippy_import::read_clippy_report(path)?,
None => Vec::new(),
};
Ok(judge::rules::pattern::analyze_workspace_with_clippy(
workspace,
&slop.findings,
&clippy_hits,
))
}
pub(crate) fn run_patterns(
options: PatternsOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let PatternsOptions {
format,
clippy_json,
save_pattern_baseline,
pattern_baseline,
} = options;
reject_unsupported_advisory_format(format, "`patterns`")?;
let workspace = judge::ingest::load(None)?;
let candidates = collect_pattern_candidates(&workspace, clippy_json.as_deref())?;
if save_pattern_baseline {
let baseline = judge::pattern_baseline::PatternBaseline::new(&candidates);
let save_path = workspace.root.join(DEFAULT_PATTERN_BASELINE);
judge::pattern_baseline::save(&save_path, &baseline)?;
writeln!(
out,
"pattern baseline saved: {} ({} candidates)",
save_path.display(),
candidates.len()
)?;
return Ok(CommandOutcome::Clean);
}
if let Some(path) = &pattern_baseline {
let baseline = judge::pattern_baseline::load(path)?;
let delta = judge::pattern_baseline::diff_patterns(&candidates, &baseline);
return render_advisory(
out,
format,
|| to_json_string(&serde_json::json!({ "delta": delta })),
|out| to_cli_result(print_pattern_delta_tty(out, &delta)),
);
}
render_advisory(
out,
format,
|| to_json_string(&serde_json::json!({ "candidates": candidates })),
|out| {
writeln!(
out,
"heuristic pattern suggestions — advisory, no verdict effect: {}",
candidates.len()
)?;
for candidate in &candidates {
writeln!(
out,
" [{}] {} crate: {}",
candidate.id, candidate.pattern, candidate.scope.krate
)?;
}
Ok(())
},
)
}
fn collect_principle_heuristics(
workspace: &judge::ingest::Workspace,
) -> Result<Vec<judge::rules::principle::PrincipleHeuristic>, CliError> {
let boundary_config = load_judge_toml(&workspace.root)?;
let source_files = workspace
.crates
.iter()
.flat_map(|krate| krate.source_files.iter());
let complexity = judge::rules::complexity::analyze_workspace(source_files, false);
Ok(judge::rules::principle::analyze_workspace(
workspace,
&complexity,
Some(&boundary_config),
)?)
}
pub(crate) fn run_principles(
options: PrinciplesOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let PrinciplesOptions { format } = options;
reject_unsupported_advisory_format(format, "`principles`")?;
let workspace = judge::ingest::load(None)?;
let heuristics = collect_principle_heuristics(&workspace)?;
render_advisory(
out,
format,
|| to_json_string(&serde_json::json!({ "heuristics": heuristics })),
|out| {
writeln!(
out,
"design principle heuristics — advisory, no verdict effect, always a judgment \
call: {}",
heuristics.len()
)?;
for heuristic in &heuristics {
writeln!(
out,
" [{}] {} crate: {}",
heuristic.id, heuristic.principle, heuristic.scope.krate
)?;
for module in &heuristic.scope.modules {
writeln!(out, " - {module}")?;
}
}
Ok(())
},
)
}
fn find_pattern_candidate(
workspace: &judge::ingest::Workspace,
id: &str,
) -> Result<judge::rules::pattern::PatternCandidate, CliError> {
collect_pattern_candidates(workspace, None)?
.into_iter()
.find(|candidate| candidate.id.as_str() == id)
.ok_or_else(|| CliError::Analyzer(format!("unknown pattern candidate id: {id}")))
}
fn load_pattern_candidate(
id: &str,
format: OutputFormat,
command_name: &str,
) -> Result<judge::rules::pattern::PatternCandidate, CliError> {
reject_unsupported_advisory_format(format, command_name)?;
let workspace = judge::ingest::load(None)?;
find_pattern_candidate(&workspace, id)
}
fn find_principle_heuristic(
workspace: &judge::ingest::Workspace,
id: &str,
) -> Result<judge::rules::principle::PrincipleHeuristic, CliError> {
collect_principle_heuristics(workspace)?
.into_iter()
.find(|heuristic| heuristic.id.as_str() == id)
.ok_or_else(|| CliError::Analyzer(format!("unknown principle heuristic id: {id}")))
}
fn print_scope_tty(
out: &mut dyn Write,
scope: &judge::rules::pattern::CodeScope,
) -> std::io::Result<()> {
writeln!(out, " scope: crate `{}`", scope.krate)?;
if !scope.modules.is_empty() {
writeln!(out, " modules:")?;
for module in &scope.modules {
writeln!(out, " - {module}")?;
}
}
Ok(())
}
fn print_migration_step_tty(
out: &mut dyn Write,
step: &judge::rules::pattern::MigrationStep,
indent: &str,
) -> std::io::Result<()> {
writeln!(out, "{indent}{}. {}", step.step, step.description)?;
for path in &step.affected_paths {
writeln!(out, "{indent} - {}", path.display())?;
}
Ok(())
}
fn print_contraindications_tty(
out: &mut dyn Write,
contraindications: &[judge::rules::pattern::Contraindication],
) -> std::io::Result<()> {
writeln!(out, " contraindications:")?;
for contraindication in contraindications {
writeln!(out, " - {}", contraindication.description)?;
}
Ok(())
}
fn print_evidence_tty(
out: &mut dyn Write,
label: &str,
evidence: &judge::rules::pattern::Evidence,
) -> std::io::Result<()> {
writeln!(out, " evidence ({label}): {}", evidence.description)?;
for location in &evidence.locations {
match &location.item_path {
Some(item_path) => writeln!(out, " - {} {item_path}", location.file.display())?,
None => writeln!(out, " - {}", location.file.display())?,
}
}
Ok(())
}
fn print_pattern_candidate_tty(
out: &mut dyn Write,
candidate: &judge::rules::pattern::PatternCandidate,
) -> std::io::Result<()> {
writeln!(out, "pattern candidate: {}", candidate.id)?;
writeln!(out, " pattern: {}", candidate.pattern)?;
print_scope_tty(out, &candidate.scope)?;
print_evidence_tty(out, "primary", &candidate.evidence.primary)?;
print_evidence_tty(out, "independent", &candidate.evidence.independent)?;
for extra in &candidate.evidence.additional {
print_evidence_tty(out, "additional", extra)?;
}
writeln!(out, " preconditions:")?;
for precondition in &candidate.preconditions {
writeln!(out, " - {}", precondition.description)?;
}
print_contraindications_tty(out, &candidate.contraindications)?;
writeln!(out, " migration plan (no patch — text only):")?;
for step in &candidate.migration {
print_migration_step_tty(out, step, " ")?;
}
writeln!(out, " related findings:")?;
for finding_id in &candidate.related_findings {
writeln!(out, " - {finding_id}")?;
}
Ok(())
}
pub(crate) fn run_explain_pattern(
options: ExplainPatternOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let ExplainPatternOptions { id, format } = options;
let candidate = load_pattern_candidate(&id, format, "`explain-pattern`")?;
render_advisory(
out,
format,
|| to_json_string(&candidate),
|out| to_cli_result(print_pattern_candidate_tty(out, &candidate)),
)
}
fn print_principle_heuristic_tty(
out: &mut dyn Write,
heuristic: &judge::rules::principle::PrincipleHeuristic,
) -> std::io::Result<()> {
writeln!(out, "principle heuristic: {}", heuristic.id)?;
writeln!(out, " principle: {}", heuristic.principle)?;
print_scope_tty(out, &heuristic.scope)?;
for (index, evidence) in heuristic.evidence.iter().enumerate() {
print_evidence_tty(out, &(index + 1).to_string(), evidence)?;
}
writeln!(out, " interpretation: {}", heuristic.interpretation)?;
print_contraindications_tty(out, &heuristic.contraindications)?;
writeln!(out, " missing evidence:")?;
for missing in &heuristic.missing_evidence {
writeln!(out, " - {}", missing.description)?;
}
writeln!(out, " alternatives:")?;
for alternative in &heuristic.alternatives {
writeln!(out, " - {}", alternative.description)?;
}
writeln!(out, " related findings:")?;
for finding_id in &heuristic.related_findings {
writeln!(out, " - {finding_id}")?;
}
Ok(())
}
pub(crate) fn run_explain_principle(
options: ExplainPrincipleOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let ExplainPrincipleOptions { id, format } = options;
reject_unsupported_advisory_format(format, "`explain-principle`")?;
let workspace = judge::ingest::load(None)?;
let heuristic = find_principle_heuristic(&workspace, &id)?;
render_advisory(
out,
format,
|| to_json_string(&heuristic),
|out| to_cli_result(print_principle_heuristic_tty(out, &heuristic)),
)
}
pub(crate) fn run_fix_preview(
options: FixPreviewOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let FixPreviewOptions { id, format } = options;
let candidate = load_pattern_candidate(&id, format, "`fix-preview`")?;
render_advisory(
out,
format,
|| {
let json = serde_json::json!({
"id": candidate.id,
"pattern": candidate.pattern,
"migration": candidate.migration,
"related_findings": candidate.related_findings,
"patch": serde_json::Value::Null,
"note": "migration plan only — no patch is generated (see todo.md §16.5)",
});
to_json_string(&json)
},
|out| {
writeln!(
out,
"fix preview for {} ({}) — no patch is generated, migration plan only:",
candidate.id, candidate.pattern
)?;
for step in &candidate.migration {
print_migration_step_tty(out, step, " ")?;
}
writeln!(out)?;
writeln!(
out,
"related findings (call sites): {}",
candidate.related_findings.len()
)?;
for finding_id in &candidate.related_findings {
writeln!(out, " {finding_id}")?;
}
Ok(())
},
)
}
pub(crate) fn run_explain_rule(
options: ExplainRuleOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let ExplainRuleOptions { id, format } = options;
reject_unsupported_advisory_format(format, "`explain-rule`")?;
let entry = judge::rule_registry::lookup(&id)
.ok_or_else(|| CliError::Analyzer(format!("unknown rule id: {id}")))?;
render_advisory(
out,
format,
|| {
let example = entry.example.map(|example| {
serde_json::json!({
"before": example.before,
"why_it_matters": example.why_it_matters,
})
});
let json = serde_json::json!({
"id": entry.id,
"evidence_class": entry.evidence_class,
"verdict_effect": entry.verdict_effect.label(),
"preconditions": entry.preconditions,
"exclusions": entry.exclusions,
"allowed_wording": entry.allowed_wording,
"example": example,
});
to_json_string(&json)
},
|out| {
let evidence_class = serde_json::to_value(entry.evidence_class)?;
writeln!(out, "rule: {}", entry.id)?;
writeln!(
out,
" evidence class: {}",
evidence_class.as_str().unwrap_or_default()
)?;
writeln!(out, " verdict effect: {}", entry.verdict_effect.label())?;
writeln!(out, " preconditions: {}", entry.preconditions)?;
writeln!(out, " exclusions: {}", entry.exclusions)?;
writeln!(out, " allowed wording: {}", entry.allowed_wording)?;
if let Some(example) = entry.example {
writeln!(out, " example:")?;
for line in example.before.lines() {
writeln!(out, " {line}")?;
}
writeln!(out, " why it matters: {}", example.why_it_matters)?;
}
Ok(())
},
)
}