use std::collections::{HashMap, HashSet};
use std::path::Path;
use serde_json::Value;
use xuanji::{BoundaryKind, Outcome, Polarity, Violation};
use crate::containment::matches_allowed;
use crate::driver::run_boundaries;
use crate::dsl::TraitImplBoundary;
use crate::errors::unknown_trait_error;
use crate::file_scope::{per_finding_file, resolve_crate};
use crate::finding::SemanticFinding;
use crate::resolve::{
BareFallback, canonical_path_str, canonical_self_owner, canonicalize_through_reexports,
render_last_segment_args, resolve_path,
};
use crate::rules::TRAIT_IMPL_RULE;
use crate::scan::scan_crate;
pub fn check_trait_impl_locality(
boundaries: &[TraitImplBoundary],
manifest_path: &Path,
) -> Outcome {
run_boundaries(boundaries, manifest_path, check_trait_impl_boundary)
}
pub(crate) fn check_trait_impl_boundary(
metadata: &Value,
boundary: &TraitImplBoundary,
violations: &mut Vec<Violation>,
) -> Result<(), String> {
let (_package, root_file, src_dir) = resolve_crate(metadata, &boundary.crate_package)?;
let src_dir = src_dir.as_path();
let findings = trait_impl_findings(
src_dir,
&root_file,
&boundary.trait_path,
&boundary.allowed_locations,
&boundary.crate_package,
)?;
let rule = TRAIT_IMPL_RULE.to_string();
let mut file_cache: HashMap<String, Option<String>> = HashMap::new();
for (finding, module) in findings {
let file = per_finding_file(
&module,
src_dir,
&root_file,
&boundary.crate_package,
&mut file_cache,
);
violations.push(
Violation::new(
BoundaryKind::Semantic,
canonical_path_str(&boundary.trait_path),
rule.clone(),
finding,
boundary.reason.clone(),
boundary.severity,
)
.with_file(file)
.with_anchor(boundary.anchor.clone())
.with_polarity(Polarity::AllowlistGap),
);
}
Ok(())
}
pub(crate) fn trait_impl_findings(
src_dir: &Path,
root_file: &Path,
trait_path: &str,
allowed: &[String],
crate_package: &str,
) -> Result<Vec<(String, String)>, String> {
let scan = scan_crate(src_dir, root_file, crate_package, &HashSet::new())?;
let given = canonical_path_str(trait_path);
let true_anchor = canonicalize_through_reexports(&given, &scan.reexports);
if !scan.trait_defs.contains(&true_anchor) {
return Err(unknown_trait_error(trait_path, crate_package));
}
let allowed: Vec<String> = allowed.iter().map(|a| canonical_path_str(a)).collect();
let mut findings = Vec::new();
for (ordinal, site) in scan.impls.iter().enumerate() {
let Some(resolved) = resolve_path(
&site.trait_path,
&site.uses,
&site.module,
BareFallback::CurrentModule,
) else {
continue;
};
let canonical = canonicalize_through_reexports(&resolved, &scan.reexports);
if canonical != true_anchor {
continue;
}
if matches_allowed(&site.module, &allowed) {
continue;
}
let owner = canonical_self_owner(&site.self_ty, &site.uses, &site.module, ordinal);
let trait_ref = format!(
"{canonical}{}",
render_last_segment_args(&site.trait_path).unwrap_or_else(|| format!("<_#{ordinal}>"))
);
findings.push((
SemanticFinding::MisplacedImpl {
module: site.module.clone(),
trait_ref,
owner,
}
.to_string(),
site.module.clone(),
));
}
findings.sort();
findings.dedup_by(|a, b| a.0 == b.0);
Ok(findings)
}