use crate::core::config::ResolvedCrateConfig;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub(crate) const POLY_EXCLUDE_GLOBS: [&str; 3] = ["**/*.ex", "**/*.exs", "**/*.cs"];
pub(crate) fn push_poly_format_excludes(args: &mut Vec<String>) {
for glob in POLY_EXCLUDE_GLOBS {
args.push("--exclude".to_owned());
args.push(glob.to_owned());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FormattingOwner {
Poly,
Residual(&'static str),
E2eOverride(String),
None,
}
pub(crate) struct PolyCoverage {
scopes: Option<Vec<PolyScope>>,
}
struct PolyScope {
root: PathBuf,
covered: Option<HashSet<PathBuf>>,
}
impl PolyCoverage {
pub(crate) fn probe(roots: &[PathBuf], base_dir: &Path) -> Self {
if roots.is_empty() || !super::is_tool_available("poly") {
return Self { scopes: None };
}
let scopes = roots
.iter()
.map(|root| {
let root = normalize_path(root, base_dir);
let mut args: Vec<String> = vec![
"fmt".to_owned(),
"--check".to_owned(),
"--format".to_owned(),
"json".to_owned(),
"--fix-generated".to_owned(),
root.to_string_lossy().into_owned(),
];
push_poly_format_excludes(&mut args);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let covered = match super::run_poly_capturing(&arg_refs, &root) {
Ok(stdout) => parse_poly_coverage(&stdout, &root),
Err(error) => {
tracing::debug!(
scope = %root.display(),
"poly coverage probe failed ({error:#}); drift check will not claim coverage in this scope"
);
None
}
};
PolyScope { root, covered }
})
.collect();
Self { scopes: Some(scopes) }
}
#[cfg(test)]
pub(crate) fn covering(paths: impl IntoIterator<Item = PathBuf>) -> Self {
Self {
scopes: Some(vec![PolyScope {
root: PathBuf::new(),
covered: Some(paths.into_iter().collect()),
}]),
}
}
#[cfg(test)]
pub(crate) fn covering_scopes(scopes: impl IntoIterator<Item = (PathBuf, Vec<PathBuf>)>) -> Self {
Self {
scopes: Some(
scopes
.into_iter()
.map(|(root, paths)| PolyScope {
root,
covered: Some(paths.into_iter().collect()),
})
.collect(),
),
}
}
#[cfg(test)]
pub(crate) fn unavailable() -> Self {
Self { scopes: None }
}
pub(crate) fn covers(&self, path: &Path) -> Option<bool> {
let normalized = normalize_path(path, Path::new(""));
self.scope_for(&normalized)?
.covered
.as_ref()
.map(|covered| covered.contains(&normalized))
}
pub(crate) fn format_context(&self, path: &Path) -> Option<&Path> {
let normalized = normalize_path(path, Path::new(""));
self.scope_for(&normalized).map(|scope| scope.root.as_path())
}
fn scope_for(&self, path: &Path) -> Option<&PolyScope> {
self.scopes
.as_ref()?
.iter()
.filter(|scope| path.starts_with(&scope.root))
.max_by_key(|scope| scope.root.components().count())
}
}
fn parse_poly_coverage(stdout: &str, base_dir: &Path) -> Option<HashSet<PathBuf>> {
let payload: serde_json::Value = serde_json::from_str(stdout).ok()?;
let results = payload.get("results")?.as_array()?;
let mut covered = HashSet::new();
for entry in results {
if entry.get("skipped").is_some_and(|reason| !reason.is_null()) {
continue;
}
let Some(path) = entry.get("path").and_then(serde_json::Value::as_str) else {
continue;
};
covered.insert(normalize_reported_path(path, base_dir));
}
Some(covered)
}
fn normalize_reported_path(reported: &str, base_dir: &Path) -> PathBuf {
normalize_path(Path::new(reported), base_dir)
}
fn normalize_path(raw: &Path, base_dir: &Path) -> PathBuf {
let joined = if raw.is_absolute() {
raw.to_path_buf()
} else {
base_dir.join(raw)
};
let mut normalized = PathBuf::new();
for component in joined.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
normalized.pop();
}
other => normalized.push(other),
}
}
normalized
}
pub(crate) fn formatting_owner(
path: &Path,
config: &ResolvedCrateConfig,
base_dir: &Path,
coverage: &PolyCoverage,
) -> FormattingOwner {
if let Some(command) = e2e_override_for(path, config, base_dir) {
return FormattingOwner::E2eOverride(command);
}
if let Some(residual) = residual_for(path, config, base_dir) {
return FormattingOwner::Residual(residual);
}
match coverage.covers(path) {
Some(true) => FormattingOwner::Poly,
Some(false) => FormattingOwner::None,
None => FormattingOwner::Poly,
}
}
fn residual_for(path: &Path, config: &ResolvedCrateConfig, base_dir: &Path) -> Option<&'static str> {
let owned_by_dart = super::dart_format_roots(config, base_dir).into_iter().any(|root| {
path.starts_with(root) && path.extension().and_then(|extension| extension.to_str()) == Some("dart")
});
if owned_by_dart {
return Some("dart format");
}
let owned_by_zig = super::zig_format_roots(config, base_dir).into_iter().any(|root| {
(path.starts_with(root.join("src")) && path.extension().and_then(|extension| extension.to_str()) == Some("zig"))
|| path == root.join("build.zig")
});
if owned_by_zig {
return Some("zig fmt");
}
match path.extension().and_then(|extension| extension.to_str()) {
Some("ex" | "exs") => Some("mix format"),
_ if path.file_name().is_some_and(|name| name == "Cargo.toml") => Some("cargo sort -n -w"),
_ => None,
}
}
fn e2e_override_for(path: &Path, config: &ResolvedCrateConfig, base_dir: &Path) -> Option<String> {
if let Some(e2e) = &config.e2e {
for root in e2e_output_roots(e2e) {
let root_path = base_dir.join(root);
let Ok(relative) = path.strip_prefix(&root_path) else {
continue;
};
let Some(language) = relative.components().next() else {
continue;
};
let language = language.as_os_str().to_string_lossy();
if let Some(command) = e2e.format.get(language.as_ref()) {
return Some(command.clone());
}
}
}
None
}
fn e2e_output_roots(e2e: &crate::core::config::E2eConfig) -> [&str; 2] {
[e2e.output.as_str(), e2e.registry.output.as_str()]
}
#[cfg(test)]
#[path = "owner_tests.rs"]
mod tests;