use mago_codex::metadata::CodebaseMetadata;
use mago_span::Span;
use crate::context::GuardContext;
use crate::matcher;
use crate::path::NamespacePath;
use crate::path::Path;
use crate::path::SymbolSelector;
use crate::report::breach::BoundaryBreach;
use crate::report::breach::BreachReason;
use crate::report::breach::BreachVector;
use crate::settings::PermittedDependency;
use crate::settings::PermittedDependencyKind;
use crate::settings::Settings;
pub fn check_usage(
ctx: &mut GuardContext<'_, '_>,
dependency_fqn: &str,
dependency_kind: PermittedDependencyKind,
vector: BreachVector,
span: Span,
) {
if let Some(reason) = check_allowed(ctx, dependency_fqn, dependency_kind) {
ctx.boundary_breaches.push(BoundaryBreach {
source_namespace: ctx.get_current_namespace().to_string(),
dependency_fqn: dependency_fqn.to_string(),
dependency_kind,
vector,
span,
reason,
});
}
}
fn check_allowed(
ctx: &GuardContext<'_, '_>,
target_fqn: &str,
dependency_kind: PermittedDependencyKind,
) -> Option<BreachReason> {
let rule = ctx
.settings
.perimeter
.rules
.iter()
.filter_map(|rule| {
let specificity = match &rule.namespace {
NamespacePath::Global if ctx.get_current_namespace().is_empty() => 0,
NamespacePath::Specific(rule_namespace)
if matcher::matches(ctx.get_current_namespace(), rule_namespace, false, true) =>
{
rule_namespace.len()
}
_ => return None,
};
Some((rule, specificity))
})
.max_by_key(|(_, specificity)| *specificity)
.map(|(rule, _)| rule);
if let Some(rule) = rule {
for allowed in &rule.permit {
match allowed {
PermittedDependency::Dependency(path) => {
if is_path_allowed(ctx.codebase, ctx.settings, path, ctx.get_current_namespace(), target_fqn) {
return None;
}
}
PermittedDependency::DependencyOfKind { path, kinds } => {
if kinds.contains(&dependency_kind)
&& is_path_allowed(ctx.codebase, ctx.settings, path, ctx.get_current_namespace(), target_fqn)
{
return None;
}
}
}
}
}
let rules: Vec<_> = rule.into_iter().collect();
if !ctx.settings.perimeter.layering.is_empty() {
let source_layer_index = get_layer_index(ctx.get_current_namespace(), ctx.settings);
let target_layer_index = get_layer_index(target_fqn, ctx.settings);
if let (Some(src_idx), Some(tgt_idx)) = (source_layer_index, target_layer_index) {
if src_idx >= tgt_idx {
return None;
}
return Some(BreachReason::Layering {
source_layer: ctx.settings.perimeter.layering[src_idx].clone(),
target_layer: ctx.settings.perimeter.layering[tgt_idx].clone(),
});
}
}
if rules.is_empty() {
Some(BreachReason::NoMatchingRule)
} else {
Some(BreachReason::ForbiddenByRule { rule_namespaces: rules.iter().map(|r| r.namespace.clone()).collect() })
}
}
fn get_root_namespace(fqn: &str) -> &str {
if let Some(pos) = fqn.find('\\') { &fqn[..pos] } else { fqn }
}
fn is_native(codebase: &CodebaseMetadata, fqn: &str) -> bool {
codebase
.get_class_like(fqn)
.map(|c| &c.flags)
.or_else(|| codebase.get_function(fqn).map(|f| &f.flags))
.or_else(|| codebase.get_constant(fqn).map(|c| &c.flags))
.is_some_and(|flags| flags.is_built_in())
}
fn get_layer_index(namespace: &str, settings: &Settings) -> Option<usize> {
for (i, layer_namespace) in settings.perimeter.layering.iter().enumerate() {
match layer_namespace {
NamespacePath::Global if namespace.is_empty() => {
return Some(i);
}
NamespacePath::Specific(ns) if matcher::matches(namespace, ns, false, true) => {
return Some(i);
}
_ => {}
}
}
None
}
fn is_path_allowed(
codebase: &CodebaseMetadata,
settings: &Settings,
path: &Path,
source_namespace: &str,
target_fqn: &str,
) -> bool {
match path {
Path::All => true,
Path::Native => is_native(codebase, target_fqn),
Path::Self_ => {
matcher::matches(target_fqn, source_namespace, false, false)
|| get_root_namespace(source_namespace).eq_ignore_ascii_case(get_root_namespace(target_fqn))
}
Path::Layer(layer_name) => settings.perimeter.layers.get(layer_name).is_some_and(|layer_patterns| {
layer_patterns
.iter()
.any(|pattern| is_path_allowed(codebase, settings, pattern, source_namespace, target_fqn))
}),
Path::Selector(selector) => match selector {
SymbolSelector::Namespace(ns) => match ns {
NamespacePath::Global => !target_fqn.contains('\\'),
NamespacePath::Specific(pattern) => matcher::matches(target_fqn, pattern, false, true),
},
SymbolSelector::Symbol(sn) => target_fqn.eq_ignore_ascii_case(sn),
SymbolSelector::Pattern(p) => matcher::matches(target_fqn, p, false, false),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_root_namespace() {
assert_eq!(get_root_namespace("Foo\\Bar\\Baz"), "Foo");
assert_eq!(get_root_namespace("Foo\\Bar"), "Foo");
assert_eq!(get_root_namespace("Foo"), "Foo");
}
}