use crate::*;
pub(crate) fn workspace_source_files(
workspace: &judge::ingest::Workspace,
) -> impl Iterator<Item = &judge::ingest::SourceFile> {
workspace
.crates
.iter()
.flat_map(|krate| krate.source_files.iter())
}
pub(crate) fn analyze_slop_workspace(
workspace: &judge::ingest::Workspace,
include_generated: bool,
) -> Result<judge::rules::slop::WorkspaceSlop, CliError> {
let source_files = workspace_source_files(workspace);
let config = load_judge_toml(&workspace.root)?.rules;
Ok(judge::rules::slop::analyze_workspace(
source_files,
include_generated,
config.catch_all_error.allow_anyhow_at_boundary,
))
}
pub(crate) fn write_error_list<E: std::fmt::Display>(
out: &mut dyn Write,
label: &str,
errors: &[E],
) -> std::io::Result<()> {
if errors.is_empty() {
return Ok(());
}
writeln!(out, "{label}: {}", errors.len())?;
for error in errors {
writeln!(out, " {error}")?;
}
Ok(())
}
pub(crate) fn write_suppressed_inline_line(
out: &mut dyn Write,
suppressed_inline: usize,
) -> std::io::Result<()> {
if suppressed_inline > 0 {
writeln!(out, "suppressed (inline judge-ignore): {suppressed_inline}")?;
}
Ok(())
}
pub(crate) fn write_excluded_and_suppressed_lines(
out: &mut dyn Write,
excluded_generated: usize,
suppressed_inline: usize,
) -> std::io::Result<()> {
if excluded_generated > 0 {
writeln!(
out,
"excluded (generated): {excluded_generated} (see --include-generated)"
)?;
}
write_suppressed_inline_line(out, suppressed_inline)
}
pub(crate) fn write_json_with_suppressed(
out: &mut dyn Write,
findings: Vec<Finding>,
analysis_errors: Vec<String>,
suppressed_inline: usize,
) -> Result<(), CliError> {
let report =
Report::with_errors(findings, analysis_errors).with_suppressed_inline(suppressed_inline);
write_json(out, &report)?;
Ok(())
}
#[cfg(not(feature = "deep"))]
pub(crate) fn deep_tier_unreachable() -> ! {
unreachable!("AnalysisTier::Deep.is_available() is compile-time false without the deep feature")
}
pub(crate) fn run_dupes(
options: DupesOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let DupesOptions {
mode,
min_tokens,
baseline_args,
include_generated,
include_tests,
} = options;
let format = baseline_args.format;
let workspace = judge::ingest::load(None)?;
let source_files = workspace_source_files(&workspace);
let report = judge::rules::duplication::analyze_workspace_with_options(
source_files,
mode.into(),
min_tokens,
include_generated,
include_tests,
);
let refactoring_summary_limit = match format {
OutputFormat::Json => report.families.len(),
OutputFormat::Tty | OutputFormat::Sarif | OutputFormat::Markdown => 5,
};
let refactoring_summary =
report.refactoring_summary(&workspace.root, refactoring_summary_limit);
let analysis_errors = analysis_errors(&report.errors);
let (findings, suppressed_inline) = match suppress_and_baseline(
report.to_findings(),
&workspace,
&analysis_errors,
&baseline_args,
std::collections::HashMap::from([(
judge::rules::duplication::DUPLICATE_RULE.to_string(),
judge::rules::duplication::DUPLICATE_RULE_REVISION,
)]),
Path::new(DEFAULT_BASELINE_DUPES),
out,
)? {
std::ops::ControlFlow::Break(outcome) => return Ok(outcome),
std::ops::ControlFlow::Continue(rest) => rest,
};
match format {
OutputFormat::Json => {
let report = Report::with_errors(findings, analysis_errors)
.with_suppressed_inline(suppressed_inline);
let mut envelope = serde_json::to_value(&report)?;
envelope["refactoring_summary"] = serde_json::to_value(refactoring_summary)?;
write_json(out, &envelope)?;
}
OutputFormat::Sarif => {
write_sarif(out, &workspace.root, findings, analysis_errors, None)?;
}
OutputFormat::Markdown => {
return Err(unsupported_format("`dupes`", format, "tty, json, sarif"));
}
OutputFormat::Tty => {
writeln!(out, "mode: {}", dupe_mode_label(mode))?;
writeln!(out, "min tokens: {min_tokens}")?;
writeln!(out, "clone families: {}", report.families.len())?;
print_duplication_refactoring_summary(out, &refactoring_summary)?;
write_error_list(out, "files skipped (parse errors)", &report.errors)?;
write_excluded_and_suppressed_lines(out, report.excluded_generated, suppressed_inline)?;
write_dupe_families_tty(out, &report, &workspace.root)?;
}
}
Ok(CommandOutcome::Clean)
}
fn dupe_mode_label(mode: DupeModeArg) -> &'static str {
match mode {
DupeModeArg::Strict => "strict",
DupeModeArg::Mild => "mild",
DupeModeArg::Weak => "weak",
DupeModeArg::Semantic => "semantic",
}
}
fn write_dupe_families_tty(
out: &mut dyn Write,
report: &judge::rules::duplication::WorkspaceDuplication,
workspace_root: &Path,
) -> std::io::Result<()> {
for (index, family) in report
.refactoring_order(workspace_root)
.into_iter()
.take(DUPE_FAMILY_TTY_LIMIT)
.enumerate()
{
writeln!(out)?;
writeln!(
out,
"family #{} — {} members",
index + 1,
family.members.len()
)?;
for member in &family.members {
writeln!(
out,
" {:>4} tokens {}:{}-{} {}",
member.token_count,
member.file.display(),
member.start_line,
member.end_line,
member.qualified_name
)?;
}
}
if report.families.len() > DUPE_FAMILY_TTY_LIMIT {
writeln!(
out,
"\n... and {} more families (see --format json for the full list)",
report.families.len() - DUPE_FAMILY_TTY_LIMIT
)?;
}
Ok(())
}
fn print_duplication_refactoring_summary(
out: &mut dyn Write,
summary: &judge::rules::duplication::RefactoringSummary,
) -> std::io::Result<()> {
writeln!(
out,
"refactoring summary: {} clone families, {} members (repeated tokens, not an automatic merge recommendation)",
summary.clone_families, summary.clone_members
)?;
for family in &summary.top_families {
writeln!(
out,
" #{} {} members × {} tokens = {} repeated tokens across {} files {}",
family.rank,
family.members,
family.tokens_per_member,
family.duplicated_token_mass,
family.files.len(),
family.representative_items.join(", ")
)?;
}
Ok(())
}
fn apply_crates_io_checks(
workspace: &judge::ingest::Workspace,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) -> Result<(), CliError> {
let slopsquat_config = load_judge_toml(&workspace.root)?.slopsquat;
let cache_root = workspace.root.join("target/judge/slopsquat-cache");
let index_client = judge::rules::slopsquat::SparseIndexClient::new(cache_root.clone());
let metadata_client = judge::rules::slopsquat::RestMetadataClient::new(cache_root.clone());
let owners_client = judge::rules::slopsquat::RestOwnersClient::new(cache_root);
apply_phantom_dependency_checks(
workspace,
&index_client,
findings,
analysis_errors,
rule_revisions,
);
apply_fresh_low_reputation_check(
workspace,
&metadata_client,
&slopsquat_config,
findings,
analysis_errors,
rule_revisions,
);
apply_yanked_dependency_check(
workspace,
&index_client,
findings,
analysis_errors,
rule_revisions,
);
apply_single_maintainer_check(
workspace,
&owners_client,
findings,
analysis_errors,
rule_revisions,
);
Ok(())
}
fn apply_phantom_dependency_checks(
workspace: &judge::ingest::Workspace,
index_client: &judge::rules::slopsquat::SparseIndexClient,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) {
let phantom_report =
judge::rules::slopsquat::analyze_phantom_dependencies(workspace, index_client);
findings.extend(phantom_report.findings);
analysis_errors.extend(phantom_report.errors);
rule_revisions.insert(
judge::rules::slopsquat::PHANTOM_CRATE_RULE.to_string(),
judge::rules::slopsquat::PHANTOM_CRATE_RULE_REVISION,
);
rule_revisions.insert(
judge::rules::slopsquat::PHANTOM_VERSION_RULE.to_string(),
judge::rules::slopsquat::PHANTOM_VERSION_RULE_REVISION,
);
}
fn apply_fresh_low_reputation_check(
workspace: &judge::ingest::Workspace,
metadata_client: &judge::rules::slopsquat::RestMetadataClient,
slopsquat_config: &judge::rules::slopsquat::SlopsquatConfig,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) {
let fresh_report = judge::rules::slopsquat::analyze_fresh_low_reputation(
workspace,
metadata_client,
slopsquat_config,
);
findings.extend(fresh_report.findings);
analysis_errors.extend(fresh_report.errors);
rule_revisions.insert(
judge::rules::slopsquat::FRESH_LOW_REPUTATION_DEP_RULE.to_string(),
judge::rules::slopsquat::FRESH_LOW_REPUTATION_DEP_RULE_REVISION,
);
}
fn apply_yanked_dependency_check(
workspace: &judge::ingest::Workspace,
index_client: &judge::rules::slopsquat::SparseIndexClient,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) {
let yanked_report =
judge::rules::slopsquat::analyze_yanked_dependencies(workspace, index_client);
findings.extend(yanked_report.findings);
analysis_errors.extend(yanked_report.errors);
rule_revisions.insert(
judge::rules::slopsquat::YANKED_DEPENDENCY_RULE.to_string(),
judge::rules::slopsquat::YANKED_DEPENDENCY_RULE_REVISION,
);
}
fn apply_single_maintainer_check(
workspace: &judge::ingest::Workspace,
owners_client: &judge::rules::slopsquat::RestOwnersClient,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) {
let single_maintainer_report =
judge::rules::slopsquat::analyze_single_maintainer_dependencies(workspace, owners_client);
findings.extend(single_maintainer_report.findings);
analysis_errors.extend(single_maintainer_report.errors);
rule_revisions.insert(
judge::rules::slopsquat::DEP_SINGLE_MAINTAINER_RULE.to_string(),
judge::rules::slopsquat::DEP_SINGLE_MAINTAINER_RULE_REVISION,
);
}
fn write_deps_tty_findings(
out: &mut dyn Write,
workspace: &judge::ingest::Workspace,
findings: &[Finding],
) -> std::io::Result<()> {
for finding in findings {
let krate = workspace
.crates
.iter()
.find(|krate| krate.manifest_path == finding.location.file);
let crate_name = krate.map_or("?", |krate| krate.name.as_str());
if finding.rule == judge::rules::deps::MISPLACED_DEPENDENCY_KIND_RULE {
let is_build_dep = krate.is_some_and(|krate| {
krate.dependencies.iter().any(|dep| {
dep.name == finding.location.item_path
&& dep.kind == judge::ingest::DependencyKind::Build
})
});
let direction = if is_build_dep {
"build-dependency appears unused by build.rs"
} else {
"should probably be a dev-dependency"
};
writeln!(
out,
" {} {} — {direction}",
crate_name, finding.location.item_path
)?;
} else {
writeln!(
out,
" [{}] {} {}",
finding.rule, crate_name, finding.location.item_path
)?;
}
}
Ok(())
}
fn run_deps_why(
dependency_name: &str,
format: OutputFormat,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
if matches!(format, OutputFormat::Sarif | OutputFormat::Markdown) {
return Err(unsupported_format("`deps --why`", format, "tty, json"));
}
let workspace = judge::ingest::load(None)?;
let report = judge::rules::deps::why(&workspace, dependency_name);
match format {
OutputFormat::Json => {
let value = serde_json::json!({
"dependency_name": report.dependency_name,
"found": report.found,
"declared_by": report.declared_by,
"graph_paths": report.graph_paths,
"public_api_exposure": report.public_api_exposure,
"errors": analysis_errors(&report.errors),
});
write_json(out, &value)?;
}
OutputFormat::Tty => print_deps_why_tty(out, &report)?,
OutputFormat::Sarif | OutputFormat::Markdown => unreachable!("rejected above"),
}
Ok(CommandOutcome::Clean)
}
fn print_deps_why_tty(
out: &mut dyn Write,
report: &judge::rules::deps::DependencyWhyReport,
) -> std::io::Result<()> {
if !report.found {
writeln!(
out,
"`{}` was not found anywhere in the resolved workspace dependency graph",
report.dependency_name
)?;
return Ok(());
}
writeln!(out, "{}", report.dependency_name)?;
if report.declared_by.is_empty() {
writeln!(
out,
" not declared directly by any workspace crate (transitive only — see graph paths below)"
)?;
}
for declaration in &report.declared_by {
let target_note = declaration
.target
.as_deref()
.map(|target| format!(" (target: {target})"))
.unwrap_or_default();
writeln!(
out,
" declared by {} as a {} dependency{target_note}",
declaration.crate_name,
declaration.kind.label(),
)?;
if declaration.features.is_empty() {
writeln!(out, " features: (none declared)")?;
} else {
writeln!(out, " features: {}", declaration.features.join(", "))?;
}
writeln!(
out,
" default-features = true (explicit): {}",
declaration.default_features_explicit
)?;
if declaration.usages.is_empty() {
writeln!(out, " source usages: none found in the examined view")?;
} else {
writeln!(out, " source usages: {}", declaration.usages.len())?;
for usage in &declaration.usages {
writeln!(
out,
" {}:{} ({})",
usage.file.display(),
usage.line,
usage.domain.label(),
)?;
}
}
}
writeln!(out, " public API exposure: {}", report.public_api_exposure)?;
writeln!(out, " resolved graph paths:")?;
let mut crate_names: Vec<&String> = report.graph_paths.keys().collect();
crate_names.sort();
for crate_name in crate_names {
let path = &report.graph_paths[crate_name];
if path.is_empty() {
writeln!(out, " {crate_name}: (no path found)")?;
} else {
writeln!(out, " {crate_name} -> {}", path.join(" -> "))?;
}
}
write_error_list(out, "errors", &report.errors)
}
pub(crate) fn run_deps(
options: DepsOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let DepsOptions {
baseline_args,
check_crates_io,
check_rustc_lints,
audit_json,
why,
} = options;
let format = baseline_args.format;
if let Some(dependency_name) = why {
return run_deps_why(&dependency_name, format, out);
}
let workspace = judge::ingest::load(None)?;
let report = judge::rules::deps::analyze_workspace(&workspace);
let mut analysis_errors = analysis_errors(&report.errors);
let mut findings = report.findings;
#[cfg_attr(not(feature = "deep"), allow(unused_mut))]
let mut rule_revisions = super::combined_analysis::deps_rule_revisions();
findings.extend(judge::rules::slopsquat::analyze_name_collision(&workspace));
let dep_graph_report = judge::rules::dep_graph::analyze_workspace(&workspace);
append_analysis_errors(&mut analysis_errors, &dep_graph_report.errors);
findings.extend(dep_graph_report.findings);
if check_crates_io {
apply_crates_io_checks(
&workspace,
&mut findings,
&mut analysis_errors,
&mut rule_revisions,
)?;
}
if check_rustc_lints {
let rustc_lint_report = judge::rules::deps::analyze_rustc_unused_dependencies(&workspace);
findings.extend(rustc_lint_report.findings);
append_analysis_errors(&mut analysis_errors, &rustc_lint_report.errors);
rule_revisions.insert(
judge::rules::deps::UNUSED_DEPENDENCY_RULE.to_string(),
judge::rules::deps::UNUSED_DEPENDENCY_RULE_REVISION,
);
}
if let Some(audit_json_path) = audit_json {
let vulnerabilities = judge::advisory::advisories::read_audit_report(&audit_json_path)?;
let advisory_report =
judge::advisory::advisories::analyze_vulnerabilities(&workspace, &vulnerabilities);
findings.extend(advisory_report.findings);
analysis_errors.extend(advisory_report.errors);
rule_revisions.insert(
judge::advisory::advisories::KNOWN_VULNERABILITY_RULE.to_string(),
judge::advisory::advisories::KNOWN_VULNERABILITY_RULE_REVISION,
);
}
let (findings, suppressed_inline) = match suppress_and_baseline(
findings,
&workspace,
&analysis_errors,
&baseline_args,
rule_revisions,
Path::new(DEFAULT_BASELINE_DEPS),
out,
)? {
std::ops::ControlFlow::Break(outcome) => return Ok(outcome),
std::ops::ControlFlow::Continue(rest) => rest,
};
match format {
OutputFormat::Json => {
let envelope = serde_json::json!({
"schema_version": judge::finding::SCHEMA_VERSION,
"findings": findings,
"feature_only_candidates": report.feature_only_candidates,
"errors": analysis_errors,
"suppressed_inline": suppressed_inline,
});
write_json(out, &envelope)?;
}
OutputFormat::Sarif => {
write_sarif(out, &workspace.root, findings, analysis_errors, None)?;
}
OutputFormat::Markdown => {
return Err(unsupported_format("`deps`", format, "tty, json, sarif"));
}
OutputFormat::Tty => {
writeln!(out, "dependency findings: {}", findings.len())?;
write_error_list(out, "errors", &analysis_errors)?;
write_suppressed_inline_line(out, suppressed_inline)?;
write_deps_tty_findings(out, &workspace, &findings)?;
if !report.feature_only_candidates.is_empty() {
writeln!(out)?;
writeln!(
out,
"feature-only candidates (no code usage found; see unused-feature-flag findings above for detail): {}",
report.feature_only_candidates.join(", ")
)?;
}
}
}
Ok(CommandOutcome::Clean)
}
pub(crate) fn run_coverage(
options: CoverageOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let CoverageOptions {
lcov,
mutants_json,
baseline_args,
} = options;
let format = baseline_args.format;
let workspace = judge::ingest::load(None)?;
let coverage = judge::advisory::coverage::read_lcov(&lcov, &workspace.root)?;
let complexity_source_files = workspace_source_files(&workspace);
let complexity_report =
judge::rules::complexity::analyze_workspace(complexity_source_files, false);
let mut analysis_errors = analysis_errors(&complexity_report.errors);
for missing in &coverage.missing_files {
analysis_errors.push(format!(
"{}: coverage data references this file, but it no longer exists in the workspace",
missing.display()
));
}
let mut findings = judge::advisory::coverage::untested_hotspots(
&complexity_report.functions,
&std::collections::HashMap::new(),
&coverage,
&workspace.root,
);
let mut rule_revisions = std::collections::HashMap::from([(
judge::advisory::coverage::UNTESTED_HOTSPOT_RULE.to_string(),
judge::advisory::coverage::UNTESTED_HOTSPOT_RULE_REVISION,
)]);
if let Some(mutants_json_path) = mutants_json {
apply_mutants_report(
&mutants_json_path,
&mut findings,
&mut analysis_errors,
&mut rule_revisions,
)?;
}
let no_coverage_data_source_files = workspace_source_files(&workspace);
let no_coverage_data = coverage.files_without_coverage_data(
&workspace.root,
no_coverage_data_source_files.map(|file| file.path.as_path()),
);
let test_ratios = judge::advisory::coverage::test_ratios(&workspace);
let (findings, _suppressed_inline) = match suppress_and_baseline(
findings,
&workspace,
&analysis_errors,
&baseline_args,
rule_revisions,
Path::new(DEFAULT_BASELINE_COVERAGE),
out,
)? {
std::ops::ControlFlow::Break(outcome) => return Ok(outcome),
std::ops::ControlFlow::Continue(rest) => rest,
};
match format {
OutputFormat::Json => {
let value =
coverage_json_value(findings, analysis_errors, &no_coverage_data, &test_ratios)?;
write_json(out, &value)?;
}
OutputFormat::Sarif => {
write_sarif(out, &workspace.root, findings, analysis_errors, None)?;
}
OutputFormat::Markdown => {
return Err(unsupported_format("`coverage`", format, "tty, json, sarif"));
}
OutputFormat::Tty => {
writeln!(out, "untested hotspots: {}", findings.len())?;
write_error_list(out, "errors", &analysis_errors)?;
for finding in &findings {
writeln!(
out,
" {}:{} {}",
finding.location.file.display(),
finding.location.line,
finding.location.item_path
)?;
}
write_coverage_no_data_tty(out, &no_coverage_data)?;
write_coverage_test_ratios_tty(out, &test_ratios)?;
}
}
Ok(CommandOutcome::Clean)
}
fn apply_mutants_report(
mutants_json_path: &Path,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) -> Result<(), CliError> {
let mutants_report = judge::advisory::mutants::read_mutants_report(mutants_json_path)?;
findings.extend(mutants_report.findings);
analysis_errors.extend(mutants_report.errors);
rule_revisions.insert(
judge::advisory::mutants::MUTATION_SURVIVOR_RULE.to_string(),
judge::advisory::mutants::MUTATION_SURVIVOR_RULE_REVISION,
);
Ok(())
}
fn coverage_json_value(
findings: Vec<Finding>,
analysis_errors: Vec<String>,
no_coverage_data: &[PathBuf],
test_ratios: &[judge::advisory::coverage::CrateTestRatio],
) -> Result<serde_json::Value, CliError> {
let report = Report::with_errors(findings, analysis_errors);
let mut value = serde_json::to_value(&report)?;
value["files_without_coverage_data"] = serde_json::to_value(
no_coverage_data
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>(),
)?;
value["test_ratios"] = serde_json::to_value(
test_ratios
.iter()
.map(|ratio| {
serde_json::json!({
"crate": ratio.crate_name,
"production_loc": ratio.production_loc,
"test_loc": ratio.test_loc,
"ratio": ratio.ratio(),
})
})
.collect::<Vec<_>>(),
)?;
Ok(value)
}
fn write_coverage_no_data_tty(
out: &mut dyn Write,
no_coverage_data: &[PathBuf],
) -> std::io::Result<()> {
if no_coverage_data.is_empty() {
return Ok(());
}
writeln!(out)?;
writeln!(
out,
"no coverage data (not asserted as 0%): {}",
no_coverage_data.len()
)?;
for file in no_coverage_data {
writeln!(out, " {}", file.display())?;
}
Ok(())
}
fn write_coverage_test_ratios_tty(
out: &mut dyn Write,
test_ratios: &[judge::advisory::coverage::CrateTestRatio],
) -> std::io::Result<()> {
if test_ratios.is_empty() {
return Ok(());
}
writeln!(out)?;
writeln!(out, "test-to-code LOC ratio (metric only, no verdict):")?;
for ratio in test_ratios {
match ratio.ratio() {
Some(value) => writeln!(
out,
" {}: {:.2} (test {} / production {})",
ratio.crate_name, value, ratio.test_loc, ratio.production_loc
)?,
None => writeln!(
out,
" {}: undefined (test {} / production 0)",
ratio.crate_name, ratio.test_loc
)?,
}
}
Ok(())
}
#[cfg_attr(not(feature = "deep"), allow(unused_variables))]
fn apply_boundaries_deep_tier(
workspace: &judge::ingest::Workspace,
config: &judge::rules::boundaries::BoundaryConfig,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
rule_revisions: &mut std::collections::HashMap<String, u32>,
) -> Result<(), CliError> {
if judge::AnalysisTier::Deep.is_available() {
#[cfg(feature = "deep")]
{
let deep_report = judge::rules::boundaries_deep::analyze_workspace(workspace, config)
.map_err(|err| CliError::Analyzer(err.to_string()))?;
findings.extend(deep_report.findings);
append_analysis_errors(analysis_errors, &deep_report.errors);
rule_revisions.insert(
judge::rules::boundaries_deep::MODULE_BOUNDARY_VIOLATION_DEEP_RULE.to_string(),
judge::rules::boundaries_deep::MODULE_BOUNDARY_VIOLATION_DEEP_RULE_REVISION,
);
}
#[cfg(not(feature = "deep"))]
{
deep_tier_unreachable();
}
}
Ok(())
}
pub(crate) fn run_boundaries(
options: BoundariesOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let BoundariesOptions {
config: config_path,
baseline_args,
graph,
} = options;
let format = baseline_args.format;
if let Some(graph_format) = graph {
let crate_graph = judge::rules::boundaries::build_crate_graph(None)?;
let rendered = match graph_format {
GraphFormat::Dot => crate_graph.to_dot(),
GraphFormat::Mermaid => crate_graph.to_mermaid(),
};
write!(out, "{rendered}")?;
return Ok(CommandOutcome::Clean);
}
let workspace = judge::ingest::load(None)?;
let config_path = config_path.unwrap_or_else(|| workspace.root.join("judge.toml"));
if !config_path.exists() {
writeln!(
out,
"no judge.toml found — boundaries are opt-in, nothing to check"
)?;
return Ok(CommandOutcome::Clean);
}
let config: judge::rules::boundaries::BoundaryConfig = parse_boundary_config(&config_path)?;
let boundaries = judge::rules::boundaries::evaluate(&workspace, &config)?;
let mut findings = boundaries.findings;
let mut analysis_errors: Vec<String> = Vec::new();
let mut rule_revisions = super::combined_analysis::boundaries_rule_revisions();
rule_revisions.insert(
judge::rules::boundaries::MODULE_BOUNDARY_VIOLATION_RULE.to_string(),
judge::rules::boundaries::MODULE_BOUNDARY_VIOLATION_RULE_REVISION,
);
apply_boundaries_deep_tier(
&workspace,
&config,
&mut findings,
&mut analysis_errors,
&mut rule_revisions,
)?;
let (findings, suppressed_inline) = match suppress_and_baseline(
findings,
&workspace,
&analysis_errors,
&baseline_args,
rule_revisions,
Path::new(DEFAULT_BASELINE_BOUNDARIES),
out,
)? {
std::ops::ControlFlow::Break(outcome) => return Ok(outcome),
std::ops::ControlFlow::Continue(rest) => rest,
};
match format {
OutputFormat::Json => {
write_json_with_suppressed(out, findings, analysis_errors, suppressed_inline)?
}
OutputFormat::Sarif => {
write_sarif(out, &workspace.root, findings, analysis_errors, None)?;
}
OutputFormat::Markdown => {
return Err(unsupported_format(
"`boundaries`",
format,
"tty, json, sarif",
));
}
OutputFormat::Tty => {
writeln!(out, "boundary rules: {}", config.boundaries.len())?;
writeln!(out, "findings: {}", findings.len())?;
write_error_list(out, "analysis errors", &analysis_errors)?;
write_suppressed_inline_line(out, suppressed_inline)?;
for finding in &findings {
writeln!(
out,
" [{}] {} — {}",
severity_label(finding.severity),
finding.rule,
finding.location.item_path
)?;
}
}
}
Ok(CommandOutcome::Clean)
}
pub(crate) fn run_module_graph(
options: ModuleGraphOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let ModuleGraphOptions {
baseline_args,
include_generated,
} = options;
let format = baseline_args.format;
let workspace = judge::ingest::load(None)?;
let report = judge::rules::module_graph::analyze_workspace(&workspace, include_generated);
let analysis_errors = analysis_errors(&report.errors);
let excluded_generated = report.excluded_generated;
let (findings, suppressed_inline) = match suppress_and_baseline(
report.findings,
&workspace,
&analysis_errors,
&baseline_args,
std::collections::HashMap::from([
(
judge::rules::module_graph::UNLINKED_FILE_RULE.to_string(),
judge::rules::module_graph::UNLINKED_FILE_RULE_REVISION,
),
(
judge::rules::module_graph::ORPHAN_MODULE_RULE.to_string(),
judge::rules::module_graph::ORPHAN_MODULE_RULE_REVISION,
),
]),
Path::new(DEFAULT_BASELINE_MODULE_GRAPH),
out,
)? {
std::ops::ControlFlow::Break(outcome) => return Ok(outcome),
std::ops::ControlFlow::Continue(rest) => rest,
};
match format {
OutputFormat::Json => {
write_json_with_suppressed(out, findings, analysis_errors, suppressed_inline)?
}
OutputFormat::Sarif => {
write_sarif(out, &workspace.root, findings, analysis_errors, None)?;
}
OutputFormat::Markdown => {
return Err(unsupported_format(
"`module-graph`",
format,
"tty, json, sarif",
));
}
OutputFormat::Tty => write_module_graph_tty(
out,
&findings,
&analysis_errors,
excluded_generated,
suppressed_inline,
)?,
}
Ok(CommandOutcome::Clean)
}
fn write_module_graph_tty(
out: &mut dyn Write,
findings: &[Finding],
analysis_errors: &[String],
excluded_generated: usize,
suppressed_inline: usize,
) -> std::io::Result<()> {
let (unlinked, orphaned): (Vec<&Finding>, Vec<&Finding>) = findings
.iter()
.partition(|finding| finding.rule == judge::rules::module_graph::UNLINKED_FILE_RULE);
write_error_list(out, "files skipped (parse errors)", analysis_errors)?;
write_excluded_and_suppressed_lines(out, excluded_generated, suppressed_inline)?;
writeln!(out, "unlinked-file findings: {}", unlinked.len())?;
for finding in &unlinked {
writeln!(
out,
" [{}] {}",
severity_label(finding.severity),
finding.location.item_path
)?;
}
writeln!(out)?;
writeln!(out, "orphan-module findings: {}", orphaned.len())?;
for finding in &orphaned {
writeln!(
out,
" [{}] {}",
severity_label(finding.severity),
finding.location.item_path
)?;
}
Ok(())
}
pub(crate) fn run_unsafe(
options: FocusedAnalysisOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let workspace = judge::ingest::load(None)?;
let source_files = workspace_source_files(&workspace);
let report = judge::rules::security::analyze_workspace(source_files, options.include_generated);
let findings = findings_matching(
report.findings,
&[
judge::rules::security::UNSAFE_SURFACE_RULE,
judge::rules::security::UNSAFE_DENSITY_RULE,
],
);
render_focused(
"unsafe",
options.format,
&workspace,
findings,
analysis_errors(&report.errors),
out,
)
}
pub(crate) fn run_errors(
options: FocusedAnalysisOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let workspace = judge::ingest::load(None)?;
let report = analyze_slop_workspace(&workspace, options.include_generated)?;
let findings = findings_matching(
report.findings,
&[
judge::rules::slop::SWALLOWED_RESULT_RULE,
judge::rules::slop::EMPTY_ERROR_ARM_RULE,
judge::rules::slop::CATCH_ALL_ERROR_RULE,
judge::rules::slop::CONTEXT_FREE_PROPAGATION_RULE,
judge::rules::slop::SILENT_DEFAULT_RULE,
],
);
render_focused(
"errors",
options.format,
&workspace,
findings,
analysis_errors(&report.errors),
out,
)
}
pub(crate) fn run_tests(
options: FocusedAnalysisOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let workspace = judge::ingest::load(None)?;
let source_files = workspace_source_files(&workspace);
let report =
judge::rules::slop::analyze_workspace(source_files, options.include_generated, false);
let findings = findings_matching(
report.findings,
&[
judge::rules::slop::ASSERTION_FREE_TEST_RULE,
judge::rules::slop::TAUTOLOGICAL_TEST_RULE,
judge::rules::slop::IGNORED_TEST_ACCUMULATION_RULE,
],
);
render_focused(
"tests",
options.format,
&workspace,
findings,
analysis_errors(&report.errors),
out,
)
}
pub(crate) fn run_slop(
options: FocusedAnalysisOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let workspace = judge::ingest::load(None)?;
let report = analyze_slop_workspace(&workspace, options.include_generated)?;
render_focused(
"slop",
options.format,
&workspace,
report.findings,
analysis_errors(&report.errors),
out,
)
}
fn findings_matching(findings: Vec<Finding>, rules: &[&str]) -> Vec<Finding> {
findings
.into_iter()
.filter(|finding| rules.contains(&finding.rule.as_str()))
.collect()
}
fn render_focused(
label: &str,
format: OutputFormat,
workspace: &judge::ingest::Workspace,
mut findings: Vec<Finding>,
errors: Vec<String>,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
judge::finding::sort_by_severity_desc(&mut findings);
match format {
OutputFormat::Json => write_json(
out,
&serde_json::json!({
"schema_version": judge::finding::SCHEMA_VERSION,
"analysis": label,
"scope": {
"tier": "fast",
"claim": "Findings are current-state syntax and workspace facts. Absence of a finding is not proof of absence in unanalysed generated or semantic code."
},
"report": Report::with_errors(findings, errors),
}),
)?,
OutputFormat::Sarif => write_sarif(out, &workspace.root, findings, errors, None)?,
OutputFormat::Tty => judge::report::write_findings_tty(
out,
&workspace.root,
format!("Judge {label}"),
&findings,
&errors,
)?,
OutputFormat::Markdown => {
return Err(unsupported_format(label, format, "tty, json, sarif"));
}
}
Ok(CommandOutcome::Clean)
}
#[cfg_attr(not(feature = "deep"), allow(unused_variables))]
fn apply_deep_api_surface_checks(
workspace: &judge::ingest::Workspace,
boundary_config: &judge::rules::boundaries::BoundaryConfig,
findings: &mut Vec<Finding>,
analysis_errors: &mut Vec<String>,
) -> Result<(Vec<String>, Option<usize>), CliError> {
#[cfg_attr(not(feature = "deep"), allow(unused_mut))]
let mut deep_errors: Vec<String> = Vec::new();
#[cfg_attr(not(feature = "deep"), allow(unused_mut))]
let mut deep_checked: Option<usize> = None;
if judge::AnalysisTier::Deep.is_available() {
#[cfg(feature = "deep")]
{
let deep_report = judge::rules::api_surface_deep::analyze_workspace(
workspace,
&boundary_config.internal_crates,
)
.map_err(|err| CliError::Analyzer(err.to_string()))?;
deep_checked = Some(deep_report.checked);
findings.extend(deep_report.findings);
deep_errors = super::baseline_output::analysis_errors(&deep_report.errors);
analysis_errors.extend(deep_errors.iter().cloned());
}
#[cfg(not(feature = "deep"))]
{
deep_tier_unreachable();
}
}
Ok((deep_errors, deep_checked))
}
pub(crate) fn run_api_surface(
options: ApiSurfaceOptions,
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
let ApiSurfaceOptions {
baseline_args:
BaselineArgs {
format,
save_baseline,
baseline,
},
include_generated,
} = options;
let workspace = judge::ingest::load(None)?;
let boundary_config = load_judge_toml(&workspace.root)?;
judge::rules::boundaries::validate_internal_crates(&workspace, &boundary_config)?;
let report =
judge::rules::api_surface::analyze_workspace(workspace.crates.iter(), include_generated);
let mut findings = report.findings;
let mut analysis_errors = analysis_errors(&report.errors);
let (deep_errors, deep_checked) =
apply_deep_api_surface_checks(&workspace, &boundary_config, &mut findings, &mut analysis_errors)?;
let (findings, suppressed_inline) =
judge::suppression::apply_inline_suppressions(findings, &workspace.root)?;
let baseline_size = if !save_baseline && let Some(path) = &baseline {
judge::baseline::load(path)?.api_surface_size
} else {
None
};
let size_trend =
judge::rules::api_surface::size_trend(&report.api_surface_size, baseline_size.as_ref());
if matches!(format, OutputFormat::Tty) {
print_api_surface_size(out, &size_trend, baseline.is_some() && !save_baseline)?;
}
if save_baseline || baseline.is_some() {
return save_or_compare_api_surface_baseline(
&workspace,
&findings,
&analysis_errors,
save_baseline,
baseline.as_deref(),
format,
&size_trend,
out,
);
}
match format {
OutputFormat::Json => {
let report = Report::with_errors(findings, analysis_errors)
.with_suppressed_inline(suppressed_inline)
.with_api_surface_size(
size_trend
.iter()
.map(|trend| (trend.crate_name.clone(), trend.item_count))
.collect(),
);
write_json(out, &report)?;
}
OutputFormat::Sarif => {
write_sarif(out, &workspace.root, findings, analysis_errors, None)?;
}
OutputFormat::Markdown => {
return Err(unsupported_format(
"`api-surface`",
format,
"tty, json, sarif",
));
}
OutputFormat::Tty => write_api_surface_tty(
out,
&findings,
&report.errors,
&deep_errors,
deep_checked,
report.excluded_generated,
suppressed_inline,
)?,
}
Ok(CommandOutcome::Clean)
}
fn save_or_compare_api_surface_baseline(
workspace: &judge::ingest::Workspace,
findings: &[Finding],
analysis_errors: &[String],
save_baseline: bool,
baseline: Option<&Path>,
format: OutputFormat,
size_trend: &[judge::rules::api_surface::CrateSizeTrend],
out: &mut dyn Write,
) -> Result<CommandOutcome, CliError> {
#[cfg_attr(not(feature = "deep"), allow(unused_mut))]
let mut rule_revisions = std::collections::HashMap::from([
(
judge::rules::api_surface::UNDOCUMENTED_PUBLIC_ITEM_RULE.to_string(),
judge::rules::api_surface::UNDOCUMENTED_PUBLIC_ITEM_RULE_REVISION,
),
(
judge::rules::api_surface::SEMVER_HAZARD_RULE.to_string(),
judge::rules::api_surface::SEMVER_HAZARD_RULE_REVISION,
),
]);
#[cfg(feature = "deep")]
rule_revisions.insert(
judge::rules::api_surface_deep::INTERNAL_LEAK_RULE.to_string(),
judge::rules::api_surface_deep::INTERNAL_LEAK_RULE_REVISION,
);
#[cfg(feature = "deep")]
rule_revisions.insert(
judge::rules::api_surface_deep::RE_EXPORT_CHAIN_RULE.to_string(),
judge::rules::api_surface_deep::RE_EXPORT_CHAIN_RULE_REVISION,
);
let current_size: std::collections::HashMap<String, usize> = size_trend
.iter()
.map(|trend| (trend.crate_name.clone(), trend.item_count))
.collect();
handle_baseline_with_trend(
&workspace.root,
findings,
analysis_errors,
BaselineOptions {
rule_revisions,
save: save_baseline,
compare_path: baseline,
default_save_path: Path::new(DEFAULT_BASELINE_API_SURFACE),
format,
total_loc: judge::health_score::total_authored_loc(workspace),
},
None,
Some(¤t_size),
out,
)
}
fn write_api_surface_tty(
out: &mut dyn Write,
findings: &[Finding],
parse_errors: &[judge::rules::api_surface::ApiSurfaceError],
deep_errors: &[String],
deep_checked: Option<usize>,
excluded_generated: usize,
suppressed_inline: usize,
) -> std::io::Result<()> {
writeln!(out, "undocumented public items: {}", findings.len())?;
if let Some(checked) = deep_checked {
writeln!(out, "pub fns checked (leaked_dependency_type): {checked}")?;
}
write_error_list(out, "files skipped (parse errors)", parse_errors)?;
write_error_list(out, "leaked-dependency-type analysis errors", deep_errors)?;
write_excluded_and_suppressed_lines(out, excluded_generated, suppressed_inline)?;
for finding in findings {
writeln!(
out,
" [{}] {}:{} {}",
severity_label(finding.severity),
finding.location.file.display(),
finding.location.line,
finding.location.item_path
)?;
}
Ok(())
}
fn print_api_surface_size(
out: &mut dyn Write,
trend: &[judge::rules::api_surface::CrateSizeTrend],
baseline_requested: bool,
) -> std::io::Result<()> {
for crate_trend in trend {
match crate_trend.delta {
Some(delta) => writeln!(
out,
"api surface: {} {} items (\u{394}{delta:+} vs baseline)",
crate_trend.crate_name, crate_trend.item_count
)?,
None if baseline_requested => writeln!(
out,
"api surface: {} {} items (not comparable to baseline)",
crate_trend.crate_name, crate_trend.item_count
)?,
None => writeln!(
out,
"api surface: {} {} items",
crate_trend.crate_name, crate_trend.item_count
)?,
}
}
Ok(())
}
fn suppress_and_baseline(
findings: Vec<Finding>,
workspace: &judge::ingest::Workspace,
analysis_errors: &[String],
baseline_args: &BaselineArgs,
rule_revisions: std::collections::HashMap<String, u32>,
default_save_path: &Path,
out: &mut dyn Write,
) -> Result<std::ops::ControlFlow<CommandOutcome, (Vec<Finding>, usize)>, CliError> {
let (findings, suppressed_inline) =
judge::suppression::apply_inline_suppressions(findings, &workspace.root)?;
let baseline_request = BaselineRequest::new(
baseline_args.save_baseline,
baseline_args.baseline.as_deref(),
baseline_args.format,
);
if let Some(result) = baseline_request.handle(
BaselineInput {
workspace_root: &workspace.root,
findings: &findings,
analysis_errors,
rule_revisions,
default_save_path,
total_loc: judge::health_score::total_authored_loc(workspace),
},
out,
) {
return result.map(std::ops::ControlFlow::Break);
}
Ok(std::ops::ControlFlow::Continue((
findings,
suppressed_inline,
)))
}