use crate::adapters::analyzers::architecture::compiled::{
compile_architecture, CompiledArchitecture,
};
use crate::adapters::analyzers::architecture::forbidden_rule::{
check_forbidden_rules, CompiledForbiddenRule,
};
use crate::adapters::analyzers::architecture::layer_rule::{check_layer_rule, LayerRuleInput};
use crate::adapters::analyzers::architecture::matcher::{
find_derive_matches, find_function_call_matches, find_glob_imports, find_item_kind_matches,
find_macro_calls, find_method_call_matches, find_path_prefix_matches,
};
use crate::adapters::analyzers::architecture::rendering::{
build_file_refs, format_match_message, match_to_finding,
};
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use crate::config::architecture::SymbolPattern;
use crate::domain::{Dimension, Finding, Severity};
use crate::ports::{AnalysisContext, DimensionAnalyzer};
use globset::{Glob, GlobSet, GlobSetBuilder};
pub struct ArchitectureAnalyzer;
impl DimensionAnalyzer for ArchitectureAnalyzer {
fn dimension_name(&self) -> &'static str {
"architecture"
}
fn analyze(&self, ctx: &AnalysisContext<'_>) -> Vec<Finding> {
let arch = &ctx.config.architecture;
if !arch.enabled {
return Vec::new();
}
let compiled = match compile_architecture(arch) {
Ok(c) => c,
Err(e) => {
eprintln!("Error compiling [architecture] config: {e}");
return Vec::new();
}
};
collect_all_findings(ctx, arch, &compiled)
}
}
fn collect_all_findings(
ctx: &AnalysisContext<'_>,
arch: &crate::config::ArchitectureConfig,
compiled: &CompiledArchitecture,
) -> Vec<Finding> {
let mut findings = Vec::new();
findings.extend(collect_symbol_findings(ctx, &arch.patterns));
findings.extend(collect_layer_findings(ctx, compiled));
findings.extend(collect_forbidden_findings(ctx, &compiled.forbidden));
findings.extend(
crate::adapters::analyzers::architecture::trait_contract_rule::collect_findings(
ctx,
&compiled.trait_contracts,
),
);
findings.extend(
crate::adapters::analyzers::architecture::call_parity_rule::collect_findings(ctx, compiled),
);
findings
}
fn collect_symbol_findings(ctx: &AnalysisContext<'_>, patterns: &[SymbolPattern]) -> Vec<Finding> {
patterns
.iter()
.flat_map(|p| collect_pattern_findings(ctx, p))
.collect()
}
fn collect_pattern_findings(ctx: &AnalysisContext<'_>, pattern: &SymbolPattern) -> Vec<Finding> {
let Some(scope) = compile_pattern_scope(pattern) else {
return Vec::new();
};
ctx.files
.iter()
.filter(|f| scope.accepts(&f.path))
.flat_map(|f| run_pattern_matchers(f, pattern))
.collect()
}
struct PatternScope {
kind: ScopeKind,
paths: GlobSet,
except: GlobSet,
}
enum ScopeKind {
AllowedIn,
ForbiddenIn,
}
impl PatternScope {
fn accepts(&self, path: &str) -> bool {
if self.except.is_match(path) {
return false;
}
match self.kind {
ScopeKind::AllowedIn => !self.paths.is_match(path),
ScopeKind::ForbiddenIn => self.paths.is_match(path),
}
}
}
fn compile_pattern_scope(pattern: &SymbolPattern) -> Option<PatternScope> {
let (kind, raw_paths) = match (&pattern.allowed_in, &pattern.forbidden_in) {
(Some(p), None) => (ScopeKind::AllowedIn, p.as_slice()),
(None, Some(p)) => (ScopeKind::ForbiddenIn, p.as_slice()),
_ => return None,
};
let paths = build_globset(raw_paths)?;
let except = build_globset(&pattern.except).unwrap_or_else(GlobSet::empty);
Some(PatternScope {
kind,
paths,
except,
})
}
fn build_globset(patterns: &[String]) -> Option<GlobSet> {
let mut builder = GlobSetBuilder::new();
for p in patterns {
match Glob::new(p) {
Ok(g) => {
builder.add(g);
}
Err(e) => {
eprintln!("architecture: invalid glob \"{p}\": {e}");
return None;
}
}
}
builder.build().ok()
}
fn run_pattern_matchers(file: &crate::ports::ParsedFile, pattern: &SymbolPattern) -> Vec<Finding> {
let rule_id = format!("architecture/pattern/{}", pattern.name);
let mut out = Vec::new();
if let Some(prefixes) = &pattern.forbid_path_prefix {
let hits = find_path_prefix_matches(&file.path, &file.ast, prefixes);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
if let Some(names) = &pattern.forbid_method_call {
let hits = find_method_call_matches(&file.path, &file.ast, names);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
if let Some(paths) = &pattern.forbid_function_call {
let hits = find_function_call_matches(&file.path, &file.ast, paths);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
if let Some(names) = &pattern.forbid_macro_call {
let hits = find_macro_calls(&file.path, &file.ast, names);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
if matches!(pattern.forbid_glob_import, Some(true)) {
let hits = find_glob_imports(&file.path, &file.ast);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
if let Some(kinds) = &pattern.forbid_item_kind {
let hits = find_item_kind_matches(&file.path, &file.ast, kinds);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
if let Some(names) = &pattern.forbid_derive {
let hits = find_derive_matches(&file.path, &file.ast, names);
out.extend(
hits.into_iter()
.map(|h| match_to_finding(h, &rule_id, pattern)),
);
}
out
}
fn collect_layer_findings(
ctx: &AnalysisContext<'_>,
compiled: &CompiledArchitecture,
) -> Vec<Finding> {
let refs: Vec<(String, &syn::File)> =
ctx.files.iter().map(|f| (f.path.clone(), &f.ast)).collect();
let input = LayerRuleInput {
layers: &compiled.layers,
reexport_points: &compiled.reexport_points,
unmatched_behavior: compiled.unmatched_behavior,
external_exact: &compiled.external_exact,
external_glob: &compiled.external_glob,
};
check_layer_rule(&refs, &input)
.into_iter()
.map(layer_hit_to_finding)
.collect()
}
fn layer_hit_to_finding(hit: MatchLocation) -> Finding {
let rule_id = match &hit.kind {
ViolationKind::UnmatchedLayer { .. } => "architecture/layer/unmatched",
_ => "architecture/layer",
};
Finding {
file: hit.file.clone(),
line: hit.line,
column: hit.column,
dimension: Dimension::Architecture,
rule_id: rule_id.to_string(),
message: format_match_message(&hit.kind, "layer import rule"),
severity: Severity::High,
..Finding::default()
}
}
fn collect_forbidden_findings(
ctx: &AnalysisContext<'_>,
rules: &[CompiledForbiddenRule],
) -> Vec<Finding> {
if rules.is_empty() {
return Vec::new();
}
check_forbidden_rules(&build_file_refs(ctx), rules)
.into_iter()
.map(forbidden_hit_to_finding)
.collect()
}
fn forbidden_hit_to_finding(hit: MatchLocation) -> Finding {
let reason = if let ViolationKind::ForbiddenEdge { reason, .. } = &hit.kind {
reason.clone()
} else {
String::new()
};
Finding {
file: hit.file.clone(),
line: hit.line,
column: hit.column,
dimension: Dimension::Architecture,
rule_id: "architecture/forbidden".to_string(),
message: format_match_message(&hit.kind, &reason),
severity: Severity::High,
..Finding::default()
}
}