use std::borrow::Cow;
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::SignatureBoundary;
use crate::emit::{SingleModuleViolationContext, push_single_module_violations};
use crate::errors::unknown_module_error;
use crate::file_scope::{is_anchor_absent_from_unit, resolve_crate_units};
use crate::finding::{ExposureKind, PathExposure, SemanticFact, sort_faceted_facts};
use crate::module_resolve::resolve_module_items_with_cfg_tags;
use crate::resolve::{
AliasMap, BareFallback, ExternRenameMap, ReexportMap, UseMap, apply_bare_alias_rename,
apply_crate_root_rename, bare_local_alias, canonical_path_str, collect_uses,
expand_canonical_paths, extern_verbatim_renamed, renames_shadowed, resolve_path_all,
validate_path_operands,
};
use crate::rules::SIGNATURE_RULE;
use crate::scan::scan_crate;
use crate::syn_util::{FlatItem, child_module_decls, reexport_externs_for, reexport_renames_for};
pub fn check(boundaries: &[SignatureBoundary], manifest_path: &Path) -> Outcome {
run_boundaries(boundaries, manifest_path, check_boundary)
}
pub(crate) fn check_boundary(
metadata: &Value,
boundary: &SignatureBoundary,
violations: &mut Vec<Violation>,
) -> Result<(), String> {
let (package, units) = resolve_crate_units(metadata, &boundary.crate_package)?;
let mut governed_somewhere = false;
let mut deferred: Option<String> = None;
for (root_file, src_dir, unit) in &units {
let unit_outcome = (|| -> Result<(), String> {
let src_dir = src_dir.as_path();
let unit = unit.as_str();
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,
rule_key: boundary.rule_key(),
reason: &boundary.reason,
severity: boundary.severity,
anchor: boundary.anchor(),
crate_package: &boundary.crate_package,
unit,
},
findings,
);
Ok(())
})();
match unit_outcome {
Ok(()) => governed_somewhere = true,
Err(reason)
if is_anchor_absent_from_unit(
&reason,
&unknown_module_error(&boundary.module, &boundary.crate_package),
) =>
{
if deferred.is_none() {
deferred = Some(reason);
}
}
Err(reason) => return Err(reason),
}
}
match deferred {
Some(reason) if !governed_somewhere => Err(reason),
_ => Ok(()),
}
}
struct FileScope {
uses: UseMap,
externs_type: HashSet<String>,
mod_decls: Vec<(String, FlatItem)>,
renames_bare: ExternRenameMap,
}
fn build_file_scopes(
items_by_branch: &HashMap<usize, Vec<FlatItem>>,
externs: &HashSet<String>,
extern_renames: &ExternRenameMap,
) -> HashMap<usize, FileScope> {
items_by_branch
.iter()
.map(|(branch, flat_items)| {
let items: Vec<syn::Item> = flat_items.iter().map(|f| f.item.clone()).collect();
let child_mods = child_module_names(&items);
let externs_type = externs
.difference(&local_type_namespace_names(&items))
.cloned()
.collect();
let renames_bare = renames_shadowed(extern_renames, &child_mods);
(
*branch,
FileScope {
uses: collect_uses(&items),
externs_type,
mod_decls: child_module_decls(flat_items),
renames_bare,
},
)
})
.collect()
}
fn collect_all_exposures(
items_with_files: &[(FlatItem, PathBuf, usize)],
scopes: &HashMap<usize, FileScope>,
module: &str,
include_trait_impls: bool,
) -> Vec<(PathExposure, PathBuf, usize, FlatItem)> {
let mut exposed = Vec::new();
for (ordinal, (flat, file, branch)) in items_with_files.iter().enumerate() {
let uses = &scopes[branch].uses;
let mut buf = Vec::new();
collect_item_exposures(&flat.item, module, uses, ordinal, &mut buf);
if include_trait_impls {
collect_trait_impl_exposures(&flat.item, module, uses, ordinal, &mut buf);
}
exposed.extend(
buf.into_iter()
.map(|exposure| (exposure, file.clone(), *branch, flat.clone())),
);
}
exposed
}
#[allow(clippy::too_many_arguments)]
fn resolve_exposure_to_findings(
exposure: &PathExposure,
file: &Path,
branch: usize,
origin: &FlatItem,
scopes: &HashMap<usize, FileScope>,
module: &str,
aliases: &AliasMap,
reexports: &ReexportMap,
extern_renames: &ExternRenameMap,
externs: &HashSet<String>,
forbidden: &[String],
) -> Vec<(SemanticFact, PathBuf)> {
let scope = &scopes[&branch];
let uses = &scope.uses;
let type_externs: Cow<HashSet<String>> = if exposure.is_reexport {
Cow::Owned(reexport_externs_for(externs, &scope.mod_decls, origin))
} else {
Cow::Borrowed(&scope.externs_type)
};
let renames_for_item: Cow<HashMap<String, String>> = if exposure.is_reexport {
Cow::Owned(reexport_renames_for(
extern_renames,
&scope.mod_decls,
origin,
))
} else {
Cow::Borrowed(&scope.renames_bare)
};
let resolved: Vec<String> = if exposure.path.leading_colon.is_some() {
extern_verbatim_renamed(&exposure.path, externs, extern_renames)
.into_iter()
.collect()
} else {
let use_map_candidates =
resolve_path_all(&exposure.path, uses, module, BareFallback::Ignore);
if !use_map_candidates.is_empty() {
use_map_candidates
} else {
bare_local_alias(&exposure.path, module, aliases)
.or_else(|| {
extern_verbatim_renamed(&exposure.path, &type_externs, &renames_for_item)
})
.into_iter()
.collect()
}
};
let canonicals: Vec<String> = resolved
.iter()
.flat_map(|canonical| expand_canonical_paths(canonical, aliases, reexports))
.collect();
canonicals
.into_iter()
.map(|canonical| apply_crate_root_rename(canonical, extern_renames))
.map(|canonical| apply_bare_alias_rename(canonical, &renames_for_item))
.filter(|canonical| matches_forbidden(canonical, forbidden))
.map(|canonical| {
(
SemanticFact::Exposed {
kind: ExposureKind::Signature,
subject: canonical,
seam: exposure.seam.clone(),
},
file.to_path_buf(),
)
})
.collect()
}
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> {
validate_path_operands(forbidden)?;
let items_with_files =
resolve_module_items_with_cfg_tags(src_dir, root_file, module, crate_package)?;
let mut items_by_branch: HashMap<usize, Vec<FlatItem>> = HashMap::new();
for (flat, _file, branch) in &items_with_files {
items_by_branch
.entry(*branch)
.or_default()
.push(flat.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;
let scopes = build_file_scopes(&items_by_branch, &externs, &extern_renames);
let forbidden: Vec<String> = forbidden.iter().map(|f| canonical_path_str(f)).collect();
let exposed = collect_all_exposures(&items_with_files, &scopes, module, include_trait_impls);
let mut findings: Vec<(SemanticFact, PathBuf)> = exposed
.iter()
.flat_map(|(exposure, file, branch, origin)| {
resolve_exposure_to_findings(
exposure,
file,
*branch,
origin,
&scopes,
module,
&aliases,
&reexports,
&extern_renames,
&externs,
&forbidden,
)
})
.collect();
sort_faceted_facts(&mut findings)?;
Ok(findings)
}