#![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("::"),
},
})
}
pub(crate) fn resolve_to_crate_absolute(
importing_file: &str,
segments: &[String],
) -> Option<Vec<String>> {
resolve_to_crate_absolute_in(importing_file, &[], segments)
}
pub(crate) fn resolve_to_crate_absolute_in(
importing_file: &str,
mod_stack: &[String],
segments: &[String],
) -> Option<Vec<String>> {
let first = segments.first()?;
let mut base = file_to_module_segments(importing_file);
base.extend_from_slice(mod_stack);
let resolved = match first.as_str() {
"crate" => segments[1..].to_vec(),
"self" => {
base.extend_from_slice(&segments[1..]);
base
}
"super" => {
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)
}
pub(crate) 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
}
pub(crate) fn build_module_segs_to_path_map<'a>(
files: &[(&'a str, &syn::File)],
) -> std::collections::HashMap<Vec<String>, &'a str> {
let mut out: std::collections::HashMap<Vec<String>, &'a str> =
std::collections::HashMap::with_capacity(files.len());
for (path, _) in files {
let segs = file_to_module_segments(path);
if segs.is_empty() {
continue;
}
match out.get(&segs) {
Some(existing) if existing.replace('\\', "/").ends_with("/mod.rs") => {
out.insert(segs, *path);
}
Some(_) => {
}
None => {
out.insert(segs, *path);
}
}
}
out
}
pub(crate) fn is_tie_break_winner(
path: &str,
segs: &[String],
segs_to_path: &std::collections::HashMap<Vec<String>, &str>,
) -> bool {
match segs_to_path.get(segs) {
Some(winner) => *winner == path,
None => true,
}
}
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
}