use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use serde_json::Value;
use xuanji::{Outcome, Violation};
use crate::collect::{collect_item_exposures, collect_trait_impl_exposures};
use crate::containment::matches_forbidden;
use crate::crate_scope::{
child_module_names, dependency_names, external_crate_set, local_type_namespace_names,
};
use crate::driver::run_boundaries;
use crate::dsl::SemanticBoundary;
use crate::emit::{SingleModuleViolationContext, push_single_module_violations};
use crate::file_scope::resolve_crate;
use crate::finding::{ExposureKind, SemanticFact, sort_faceted_facts};
use crate::module_resolve::resolve_module_items_with_files;
use crate::resolve::{
BareFallback, apply_bare_alias_rename, apply_crate_root_rename, bare_local_alias,
canonical_path_str, canonicalize_through_aliases, collect_uses, extern_verbatim_renamed,
renames_shadowed, resolve_path,
};
use crate::rules::SIGNATURE_RULE;
use crate::scan::scan_crate;
pub fn check(boundaries: &[SemanticBoundary], manifest_path: &Path) -> Outcome {
run_boundaries(boundaries, manifest_path, check_boundary)
}
pub(crate) fn check_boundary(
metadata: &Value,
boundary: &SemanticBoundary,
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 = module_findings(
src_dir,
&root_file,
&boundary.module,
&boundary.forbidden,
&boundary.crate_package,
boundary.including_trait_impls,
&dependency_names(package),
)?;
push_single_module_violations(
violations,
SingleModuleViolationContext {
module: &boundary.module,
rule: SIGNATURE_RULE,
reason: &boundary.reason,
severity: boundary.severity,
anchor: boundary.anchor(),
},
findings,
);
Ok(())
}
pub(crate) fn module_findings(
src_dir: &Path,
root_file: &Path,
module: &str,
forbidden: &[String],
crate_package: &str,
include_trait_impls: bool,
dep_names: &[String],
) -> Result<Vec<(SemanticFact, PathBuf)>, String> {
let items_with_files =
resolve_module_items_with_files(src_dir, root_file, module, crate_package)?;
let mut items_by_branch: HashMap<usize, Vec<syn::Item>> = HashMap::new();
for (item, _file, branch) in &items_with_files {
items_by_branch
.entry(*branch)
.or_default()
.push(item.clone());
}
let externs = external_crate_set(dep_names);
let scan = scan_crate(src_dir, root_file, crate_package, &externs)?;
let reexports = scan.reexports;
let aliases = scan.aliases;
let extern_renames = scan.extern_renames;
struct FileScope {
uses: crate::resolve::UseMap,
externs_type: HashSet<String>,
externs_reexport: HashSet<String>,
renames_bare: crate::resolve::ExternRenameMap,
}
let scopes: HashMap<usize, FileScope> = items_by_branch
.iter()
.map(|(branch, file_items)| {
let child_mods = child_module_names(file_items);
let externs_type = externs
.difference(&local_type_namespace_names(file_items))
.cloned()
.collect();
let externs_reexport = externs.difference(&child_mods).cloned().collect();
let renames_bare = renames_shadowed(&extern_renames, &child_mods);
(
*branch,
FileScope {
uses: collect_uses(file_items),
externs_type,
externs_reexport,
renames_bare,
},
)
})
.collect();
let forbidden: Vec<String> = forbidden.iter().map(|f| canonical_path_str(f)).collect();
let mut exposed = Vec::new();
for (ordinal, (item, file, branch)) in items_with_files.iter().enumerate() {
let uses = &scopes[branch].uses;
let mut buf = Vec::new();
collect_item_exposures(item, module, uses, ordinal, &mut buf);
if include_trait_impls {
collect_trait_impl_exposures(item, module, uses, ordinal, &mut buf);
}
exposed.extend(
buf.into_iter()
.map(|exposure| (exposure, file.clone(), *branch)),
);
}
let mut findings: Vec<(SemanticFact, PathBuf)> = exposed
.iter()
.filter_map(|(exposure, file, branch)| {
let scope = &scopes[branch];
let uses = &scope.uses;
let type_externs = if exposure.is_reexport {
&scope.externs_reexport
} else {
&scope.externs_type
};
let resolved = if exposure.path.leading_colon.is_some() {
extern_verbatim_renamed(&exposure.path, &externs, &extern_renames)
} else {
resolve_path(&exposure.path, uses, module, BareFallback::Ignore)
.or_else(|| bare_local_alias(&exposure.path, module, &aliases))
.or_else(|| {
extern_verbatim_renamed(&exposure.path, type_externs, &scope.renames_bare)
})
};
resolved
.map(|canonical| canonicalize_through_aliases(&canonical, &aliases, &reexports))
.map(|canonical| apply_crate_root_rename(canonical, &extern_renames))
.map(|canonical| apply_bare_alias_rename(canonical, &scope.renames_bare))
.filter(|canonical| matches_forbidden(canonical, &forbidden))
.map(|canonical| SemanticFact::Exposed {
kind: ExposureKind::Signature,
subject: canonical,
seam: exposure.seam.clone(),
})
.map(|fact| (fact, file.clone()))
})
.collect();
sort_faceted_facts(&mut findings);
Ok(findings)
}