use crate::adapters::analyzers::architecture::call_parity_rule::build_handler_touchpoints;
use crate::adapters::analyzers::architecture::call_parity_rule::check_a::check_no_delegation;
use crate::adapters::analyzers::architecture::call_parity_rule::check_b::check_missing_adapter;
use crate::adapters::analyzers::architecture::call_parity_rule::check_c::check_multi_touchpoint;
use crate::adapters::analyzers::architecture::call_parity_rule::check_d::check_multiplicity_mismatch;
use crate::adapters::analyzers::architecture::call_parity_rule::pub_fns::collect_pub_fns_by_layer;
use crate::adapters::analyzers::architecture::call_parity_rule::touchpoints::compute_touchpoints;
use crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::{
build_call_graph, canonical_name_for_pub_fn,
};
use crate::adapters::analyzers::architecture::compiled::CompiledCallParity;
use crate::adapters::analyzers::architecture::layer_rule::LayerDefinitions;
use crate::adapters::analyzers::architecture::MatchLocation;
use crate::adapters::shared::use_tree::{gather_alias_map, AliasMap};
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::collections::{HashMap, HashSet};
pub(super) struct Workspace {
pub files: Vec<(String, String, syn::File)>,
pub aliases_per_file: HashMap<String, AliasMap>,
}
pub(super) fn parse(src: &str) -> syn::File {
syn::parse_str(src).expect("parse")
}
pub(super) fn globset(patterns: &[&str]) -> GlobSet {
let mut b = GlobSetBuilder::new();
for p in patterns {
b.add(Glob::new(p).unwrap());
}
b.build().unwrap()
}
pub(super) fn build_workspace(entries: &[(&str, &str)]) -> Workspace {
let mut files = Vec::new();
let mut aliases_per_file = HashMap::new();
for (path, src) in entries {
let ast = parse(src);
let alias_map = gather_alias_map(&ast);
aliases_per_file.insert(path.to_string(), alias_map);
files.push((path.to_string(), src.to_string(), ast));
}
Workspace {
files,
aliases_per_file,
}
}
pub(super) fn borrowed_files(ws: &Workspace) -> Vec<(&str, &syn::File)> {
ws.files.iter().map(|(p, _, f)| (p.as_str(), f)).collect()
}
pub(super) fn three_layer() -> LayerDefinitions {
LayerDefinitions::new(
vec![
"application".to_string(),
"cli".to_string(),
"mcp".to_string(),
],
vec![
("application".to_string(), globset(&["src/application/**"])),
("cli".to_string(), globset(&["src/cli/**"])),
("mcp".to_string(), globset(&["src/mcp/**"])),
],
)
}
pub(super) fn cli_mcp_config(call_depth: usize) -> CompiledCallParity {
CompiledCallParity {
adapters: vec!["cli".to_string(), "mcp".to_string()],
target: "application".to_string(),
call_depth,
exclude_targets: GlobSet::empty(),
transparent_wrappers: HashSet::new(),
transparent_macros: HashSet::new(),
promoted_attributes: HashSet::new(),
single_touchpoint: crate::config::architecture::SingleTouchpointMode::default(),
}
}
pub(super) fn ports_app_cli_mcp() -> LayerDefinitions {
LayerDefinitions::new(
vec![
"ports".to_string(),
"application".to_string(),
"cli".to_string(),
"mcp".to_string(),
],
vec![
("ports".to_string(), globset(&["src/ports/**"])),
("application".to_string(), globset(&["src/application/**"])),
("cli".to_string(), globset(&["src/cli/**"])),
("mcp".to_string(), globset(&["src/mcp/**"])),
],
)
}
pub(super) fn four_layer() -> LayerDefinitions {
LayerDefinitions::new(
vec![
"application".to_string(),
"cli".to_string(),
"mcp".to_string(),
"rest".to_string(),
],
vec![
("application".to_string(), globset(&["src/application/**"])),
("cli".to_string(), globset(&["src/cli/**"])),
("mcp".to_string(), globset(&["src/mcp/**"])),
("rest".to_string(), globset(&["src/rest/**"])),
],
)
}
pub(super) enum Check {
A,
B,
C,
D,
}
pub(super) fn build_graph_only(
ws: &Workspace,
layers: &LayerDefinitions,
cfg_test: &HashSet<String>,
transparent_wrappers: &HashSet<String>,
) -> crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::CallGraph {
let borrowed = borrowed_files(ws);
let crate_root_modules =
crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::collect_crate_root_modules(&borrowed);
let workspace_module_paths =
crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::collect_workspace_module_paths(&borrowed);
let workspace = crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::WorkspaceLookup {
cfg_test_files: cfg_test,
crate_root_modules: &crate_root_modules,
workspace_module_paths: &workspace_module_paths,
};
build_call_graph(
&borrowed,
&ws.aliases_per_file,
layers,
transparent_wrappers,
&workspace,
)
}
fn build_pub_fns_and_graph<'ws>(
ws: &'ws Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
cfg_test: &HashSet<String>,
) -> (
HashMap<
String,
Vec<crate::adapters::analyzers::architecture::call_parity_rule::pub_fns::PubFnInfo<'ws>>,
>,
crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::CallGraph,
) {
let borrowed = borrowed_files(ws);
let crate_root_modules =
crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::collect_crate_root_modules(&borrowed);
let workspace_module_paths =
crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::collect_workspace_module_paths(&borrowed);
let workspace = crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::WorkspaceLookup {
cfg_test_files: cfg_test,
crate_root_modules: &crate_root_modules,
workspace_module_paths: &workspace_module_paths,
};
use crate::adapters::analyzers::architecture::call_parity_rule::pub_fns::PubFnInputs;
let pub_fns = collect_pub_fns_by_layer(PubFnInputs {
files: &borrowed,
aliases_per_file: &ws.aliases_per_file,
layers,
transparent_wrappers: &cp.transparent_wrappers,
promoted_attributes: &cp.promoted_attributes,
workspace: &workspace,
});
let graph = build_call_graph(
&borrowed,
&ws.aliases_per_file,
layers,
&cp.transparent_wrappers,
&workspace,
);
(pub_fns, graph)
}
pub(super) fn run_check(
which: Check,
ws: &Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
cfg_test: &HashSet<String>,
) -> Vec<MatchLocation> {
let (pub_fns, graph) = build_pub_fns_and_graph(ws, layers, cp, cfg_test);
let touchpoints = build_handler_touchpoints(&pub_fns, &graph, cp);
match which {
Check::A => check_no_delegation(&pub_fns, &touchpoints, cp),
Check::B => {
let borrowed = borrowed_files(ws);
let crate_root_modules =
crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::collect_crate_root_modules(&borrowed);
let workspace_module_paths =
crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::collect_workspace_module_paths(&borrowed);
let workspace = crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::WorkspaceLookup {
cfg_test_files: cfg_test,
crate_root_modules: &crate_root_modules,
workspace_module_paths: &workspace_module_paths,
};
let candidates = crate::adapters::analyzers::architecture::call_parity_rule::hint::collect_private_candidates(
&borrowed, &ws.aliases_per_file, layers, &cp.transparent_wrappers, &workspace,
);
let mut hits = check_missing_adapter(&pub_fns, &graph, &touchpoints, cp);
crate::adapters::analyzers::architecture::call_parity_rule::hint::enrich_with_hints(
&mut hits,
&graph,
cp,
&candidates,
);
hits
}
Check::C => check_multi_touchpoint(&pub_fns, &touchpoints, cp),
Check::D => check_multiplicity_mismatch(&pub_fns, &graph, &touchpoints, cp),
}
}
pub(super) fn run_check_a(
ws: &Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
cfg_test: &HashSet<String>,
) -> Vec<MatchLocation> {
run_check(Check::A, ws, layers, cp, cfg_test)
}
pub(super) fn run_check_b(
ws: &Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
cfg_test: &HashSet<String>,
) -> Vec<MatchLocation> {
run_check(Check::B, ws, layers, cp, cfg_test)
}
pub(super) fn run_check_c(
ws: &Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
cfg_test: &HashSet<String>,
) -> Vec<MatchLocation> {
run_check(Check::C, ws, layers, cp, cfg_test)
}
pub(super) fn run_check_d(
ws: &Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
cfg_test: &HashSet<String>,
) -> Vec<MatchLocation> {
run_check(Check::D, ws, layers, cp, cfg_test)
}
pub(super) fn empty_cfg_test() -> HashSet<String> {
HashSet::new()
}
pub(super) fn graph_contains_edge(
graph: &crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::CallGraph,
from: &str,
to: &str,
) -> bool {
graph
.forward
.get(from)
.is_some_and(|callees| callees.contains(to))
}
pub(super) fn callees_of<'g>(
graph: &'g crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::CallGraph,
from: &str,
) -> Vec<&'g str> {
graph
.forward
.get(from)
.map(|set| {
let mut v: Vec<&str> = set.iter().map(String::as_str).collect();
v.sort();
v
})
.unwrap_or_default()
}
pub(super) fn compute_touchpoints_for(
ws: &Workspace,
layers: &LayerDefinitions,
cp: &CompiledCallParity,
handler_fn_name: &str,
cfg_test: &HashSet<String>,
) -> HashSet<String> {
let (pub_fns, graph) = build_pub_fns_and_graph(ws, layers, cp, cfg_test);
let matches: Vec<(&str, _)> = pub_fns
.iter()
.flat_map(|(layer, infos)| {
infos
.iter()
.filter(|i| i.fn_name == handler_fn_name)
.map(move |i| (layer.as_str(), i))
})
.collect();
let (origin_adapter, info) = match matches.as_slice() {
[] => panic!("handler `{handler_fn_name}` not found in pub_fns"),
[m] => *m,
_ => panic!(
"handler `{handler_fn_name}` is ambiguous in pub_fns ({} matches); pass a more specific name or extend this helper to take a layer hint",
matches.len()
),
};
let canonical = canonical_name_for_pub_fn(info);
let ctx = crate::adapters::analyzers::architecture::call_parity_rule::touchpoints::TouchpointContext {
graph: &graph,
target_layer: &cp.target,
call_depth: cp.call_depth,
origin_adapter,
adapter_layers: &cp.adapters,
};
compute_touchpoints(&canonical, &ctx)
}