#![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};
use std::collections::HashMap;
use syn::spanned::Spanned;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnmatchedBehavior {
CompositionRoot,
StrictError,
}
#[derive(Debug)]
pub struct LayerDefinitions {
ranks: HashMap<String, usize>,
definitions: Vec<(String, GlobSet)>,
}
impl LayerDefinitions {
pub fn new(order: Vec<String>, definitions: Vec<(String, GlobSet)>) -> Self {
let ranks = order
.iter()
.enumerate()
.map(|(i, n)| (n.clone(), i))
.collect();
Self { ranks, definitions }
}
pub fn rank_of(&self, layer: &str) -> Option<usize> {
self.ranks.get(layer).copied()
}
pub fn layer_for_file(&self, path: &str) -> Option<&str> {
self.definitions
.iter()
.find(|(_, gs)| gs.is_match(path))
.map(|(name, _)| name.as_str())
}
pub fn layer_and_rank_for_file(&self, path: &str) -> Option<(&str, usize)> {
let layer = self.layer_for_file(path)?;
let rank = self.rank_of(layer)?;
Some((layer, rank))
}
pub fn layer_for_crate_segment(&self, seg: &str) -> Option<&str> {
[format!("src/{seg}.rs"), format!("src/{seg}/mod.rs")]
.iter()
.find_map(|c| self.layer_for_file(c))
}
}
pub struct LayerRuleInput<'a> {
pub layers: &'a LayerDefinitions,
pub reexport_points: &'a GlobSet,
pub unmatched_behavior: UnmatchedBehavior,
pub external_exact: &'a HashMap<String, String>,
pub external_glob: &'a [(GlobMatcher, String)],
}
enum FileClass<'a> {
Skip,
Unmatched,
Matched { layer: &'a str, rank: usize },
}
struct FileInfo<'a> {
path: &'a str,
ast: &'a syn::File,
layer: &'a str,
rank: usize,
}
enum ImportTarget<'a> {
Layer {
layer: &'a str,
display_path: String,
},
Ignore,
}
pub fn check_layer_rule(
files: &[(String, &syn::File)],
input: &LayerRuleInput<'_>,
) -> Vec<MatchLocation> {
files
.iter()
.flat_map(|(path, ast)| file_violations(path, ast, input))
.collect()
}
fn file_violations(path: &str, ast: &syn::File, input: &LayerRuleInput<'_>) -> Vec<MatchLocation> {
match classify_file(path, input) {
FileClass::Skip => Vec::new(),
FileClass::Unmatched => vec![make_unmatched(path)],
FileClass::Matched { layer, rank } => collect_file_violations(
&FileInfo {
path,
ast,
layer,
rank,
},
input,
),
}
}
fn classify_file<'a>(path: &str, input: &'a LayerRuleInput<'_>) -> FileClass<'a> {
if input.reexport_points.is_match(path) {
return FileClass::Skip;
}
let Some((layer, rank)) = input.layers.layer_and_rank_for_file(path) else {
return match input.unmatched_behavior {
UnmatchedBehavior::CompositionRoot => FileClass::Skip,
UnmatchedBehavior::StrictError => FileClass::Unmatched,
};
};
FileClass::Matched { layer, rank }
}
fn make_unmatched(path: &str) -> MatchLocation {
MatchLocation {
file: path.to_string(),
line: 1,
column: 0,
kind: ViolationKind::UnmatchedLayer {
file: path.to_string(),
},
}
}
fn collect_file_violations(file: &FileInfo<'_>, input: &LayerRuleInput<'_>) -> Vec<MatchLocation> {
gather_imports(file.ast)
.into_iter()
.filter_map(|(segments, span)| evaluate_import(file, &segments, span, input))
.collect()
}
fn evaluate_import(
file: &FileInfo<'_>,
segments: &[String],
span: proc_macro2::Span,
input: &LayerRuleInput<'_>,
) -> Option<MatchLocation> {
let ImportTarget::Layer {
layer,
display_path,
} = resolve_target(segments, input)
else {
return None;
};
let to_rank = input.layers.rank_of(layer)?;
if to_rank <= file.rank {
return None;
}
let start = span.start();
Some(MatchLocation {
file: file.path.to_string(),
line: start.line,
column: start.column,
kind: ViolationKind::LayerViolation {
from_layer: file.layer.to_string(),
to_layer: layer.to_string(),
imported_path: display_path,
},
})
}
fn resolve_target<'a>(segments: &[String], input: &'a LayerRuleInput<'_>) -> ImportTarget<'a> {
let Some(first) = segments.first() else {
return ImportTarget::Ignore;
};
match first.as_str() {
"self" | "super" | "std" | "core" | "alloc" => ImportTarget::Ignore,
"crate" => resolve_crate_target(segments, input),
ext => resolve_external_target(ext, segments, input),
}
}
fn resolve_crate_target<'a>(
segments: &[String],
input: &'a LayerRuleInput<'_>,
) -> ImportTarget<'a> {
let Some(seg) = segments.get(1) else {
return ImportTarget::Ignore;
};
match input.layers.layer_for_crate_segment(seg) {
Some(layer) => ImportTarget::Layer {
layer,
display_path: segments.join("::"),
},
None => ImportTarget::Ignore,
}
}
fn resolve_external_target<'a>(
crate_name: &str,
segments: &[String],
input: &'a LayerRuleInput<'_>,
) -> ImportTarget<'a> {
if let Some(layer) = input.external_exact.get(crate_name) {
return ImportTarget::Layer {
layer: layer.as_str(),
display_path: segments.join("::"),
};
}
input
.external_glob
.iter()
.find(|(m, _)| m.is_match(crate_name))
.map(|(_, layer)| ImportTarget::Layer {
layer: layer.as_str(),
display_path: segments.join("::"),
})
.unwrap_or(ImportTarget::Ignore)
}