#![cfg_attr(test, allow(dead_code))]
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use crate::adapters::shared::use_tree::gather_imports;
use globset::{GlobMatcher, GlobSet};
#[derive(Debug)]
pub struct CompiledForbiddenRule {
pub from: GlobMatcher,
pub to: GlobMatcher,
pub except: GlobSet,
pub reason: String,
}
pub fn check_forbidden_rules(
files: &[(String, &syn::File)],
rules: &[CompiledForbiddenRule],
) -> Vec<MatchLocation> {
files
.iter()
.flat_map(|(path, ast)| file_hits(path, ast, rules))
.collect()
}
fn file_hits(path: &str, ast: &syn::File, rules: &[CompiledForbiddenRule]) -> Vec<MatchLocation> {
let imports = gather_imports(ast);
rules
.iter()
.filter(|r| r.from.is_match(path))
.flat_map(|r| {
imports
.iter()
.filter_map(|(segments, span)| evaluate_import(path, segments, *span, r))
})
.collect()
}
fn evaluate_import(
path: &str,
segments: &[String],
span: proc_macro2::Span,
rule: &CompiledForbiddenRule,
) -> Option<MatchLocation> {
let inner = resolve_to_crate_absolute(path, segments)?;
let candidates = candidate_paths(&inner);
let to_hits = candidates.iter().any(|c| rule.to.is_match(c));
if !to_hits {
return None;
}
let except_hits = candidates.iter().any(|c| rule.except.is_match(c));
if except_hits {
return None;
}
let start = span.start();
Some(MatchLocation {
file: path.to_string(),
line: start.line,
column: start.column,
kind: ViolationKind::ForbiddenEdge {
reason: rule.reason.clone(),
imported_path: segments.join("::"),
},
})
}
fn resolve_to_crate_absolute(importing_file: &str, segments: &[String]) -> Option<Vec<String>> {
let first = segments.first()?;
let resolved = match first.as_str() {
"crate" => segments[1..].to_vec(),
"self" => {
let mut base = file_to_module_segments(importing_file);
base.extend_from_slice(&segments[1..]);
base
}
"super" => {
let mut base = file_to_module_segments(importing_file);
let mut i = 0;
while segments.get(i).is_some_and(|s| s == "super") {
base.pop()?;
i += 1;
}
base.extend_from_slice(&segments[i..]);
base
}
_ => return None,
};
if resolved.iter().any(|s| s == "*") {
return None;
}
Some(resolved)
}
fn file_to_module_segments(path: &str) -> Vec<String> {
let normalised = path.replace('\\', "/");
let stripped = normalised.strip_prefix("src/").unwrap_or(&normalised);
let without_ext = stripped.strip_suffix(".rs").unwrap_or(stripped);
if without_ext == "lib" || without_ext == "main" {
return Vec::new();
}
let mut parts: Vec<String> = without_ext.split('/').map(String::from).collect();
if parts.last().is_some_and(|s| s == "mod") {
parts.pop();
}
parts
}
fn candidate_paths(inner: &[String]) -> Vec<String> {
let mut candidates = Vec::new();
for len in (1..=inner.len()).rev() {
let head = &inner[..len];
let joined = head.join("/");
candidates.push(format!("src/{joined}.rs"));
candidates.push(format!("src/{joined}/mod.rs"));
}
if inner.len() == 1 {
candidates.push("src/lib.rs".to_string());
candidates.push("src/main.rs".to_string());
}
candidates
}