use super::reject_missing_configured_directories;
use crate::snippets::gaps::{GapConfig, detect_gaps};
use crate::snippets::types::Language;
use std::path::PathBuf;
use std::process::ExitCode;
pub(super) struct GapInvocation<'a> {
pub(super) snippet_dirs: &'a [PathBuf],
pub(super) docs_dirs: &'a [PathBuf],
pub(super) required_languages: Option<&'a [String]>,
pub(super) include_base_paths: &'a [PathBuf],
pub(super) strict: bool,
}
pub(super) fn run_gaps(invocation: &GapInvocation<'_>) -> ExitCode {
let GapInvocation {
snippet_dirs,
docs_dirs,
required_languages,
include_base_paths,
strict,
} = *invocation;
if let Err(code) = reject_missing_configured_directories(snippet_dirs, docs_dirs) {
return code;
}
let required: Vec<Language> = match required_languages
.map(|languages| {
languages
.iter()
.map(|language| crate::snippets::types::resolve_required_language(language))
.collect::<Result<Vec<Language>, String>>()
})
.transpose()
{
Ok(required) => required.unwrap_or_default(),
Err(error) => {
tracing::error!("invalid --required-languages entry: {error}");
return ExitCode::FAILURE;
}
};
let resolved_base_paths: Vec<PathBuf> = if include_base_paths.is_empty() {
docs_dirs.to_vec()
} else {
include_base_paths.to_vec()
};
let configured_references = match crate::snippets::gaps::coverage_ledger_references(snippet_dirs) {
Ok(references) => references,
Err(error) => {
tracing::error!("reading generated snippet coverage: {error}");
return ExitCode::FAILURE;
}
};
let config = GapConfig {
docs_dirs: docs_dirs.to_vec(),
snippet_dirs: snippet_dirs.to_vec(),
required_languages: required,
include_base_paths: resolved_base_paths,
configured_references,
exclude: Vec::new(),
};
let report = match detect_gaps(&config) {
Ok(report) => report,
Err(err) => {
tracing::error!("detecting gaps: {err}");
return ExitCode::FAILURE;
}
};
let unset = crate::snippets::gap_coverage::unset_gap_inputs(
docs_dirs,
&config.required_languages,
include_base_paths,
report.coverage.mkdocs_include_references,
);
for line in report.coverage.report_lines() {
crate::bin_cli::output::line(line);
}
for line in crate::snippets::gap_coverage::unset_input_lines(&unset, strict) {
crate::bin_cli::output::line(line);
}
let unconfigured_failure = strict && crate::snippets::gap_coverage::has_vacuous_input(&unset);
if unconfigured_failure {
tracing::error!(
"--strict: the gap check cannot pass unconfigured — an unset input above left a check class \
with nothing to compare, so a clean result would prove nothing"
);
}
if !report.has_gaps() {
crate::bin_cli::output::line("No gaps found.");
return if unconfigured_failure {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
};
}
if !report.missing_references.is_empty() {
crate::bin_cli::output::line(format!(
"Missing include targets ({}):",
report.missing_references.len()
));
for reference in &report.missing_references {
crate::bin_cli::output::line(format!(
" {}:{} → {}",
reference.source.display(),
reference.line,
reference.target.display()
));
}
}
if !report.unreferenced_snippets.is_empty() {
crate::bin_cli::output::line(format!(
"Unreferenced snippets ({}):",
report.unreferenced_snippets.len()
));
for path in &report.unreferenced_snippets {
crate::bin_cli::output::line(format!(" {}", path.display()));
}
}
if !report.missing_language_variants.is_empty() {
crate::bin_cli::output::line(format!(
"Missing language variants ({}):",
report.missing_language_variants.len()
));
for variant in &report.missing_language_variants {
crate::bin_cli::output::line(format!(" {} — {}", variant.group.display(), variant.language));
}
}
if !report.skips_without_reason.is_empty() {
crate::bin_cli::output::line(format!("Skips without reason ({}):", report.skips_without_reason.len()));
for location in &report.skips_without_reason {
crate::bin_cli::output::line(format!(
" {}:{} (block {})",
location.path.display(),
location.line,
location.block_index
));
}
}
if !report.unknown_languages.is_empty() {
crate::bin_cli::output::line(format!("Unknown languages ({}):", report.unknown_languages.len()));
for unknown in &report.unknown_languages {
crate::bin_cli::output::line(format!(
" {}:{} tag={}",
unknown.path.display(),
unknown.line,
unknown.tag
));
}
}
if report.is_failure(strict) || unconfigured_failure {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}