use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::{Path, PathBuf},
};
use quote::ToTokens as _;
use syn::{
AngleBracketedGenericArguments, Attribute, GenericArgument, ImplItem, Item, ItemImpl, ItemMod,
ItemUse, PathArguments, Type, UseTree,
};
use crate::{ArchitectureError, CrateArchitecture, DiagramNode, StateMachineDiagram, StepDiagram};
#[derive(Clone)]
struct Module {
uses: BTreeMap<String, Vec<String>>,
globs: Vec<Vec<String>>,
}
#[derive(Clone)]
enum DeclarationKind {
Enum(Vec<String>),
Other,
}
#[derive(Clone)]
struct Declaration {
module: Vec<String>,
name: String,
kind: DeclarationKind,
}
struct RawImpl {
module: Vec<String>,
implementation: ItemImpl,
}
struct SourceModel {
modules: BTreeMap<Vec<String>, Module>,
declarations: BTreeMap<Vec<String>, Declaration>,
implementations: Vec<RawImpl>,
visited_files: BTreeSet<PathBuf>,
}
impl SourceModel {
fn new() -> Self {
Self {
modules: BTreeMap::new(),
declarations: BTreeMap::new(),
implementations: Vec::new(),
visited_files: BTreeSet::new(),
}
}
fn read_module_file(
&mut self,
file_path: &Path,
module_path: Vec<String>,
child_directory: &Path,
) -> Result<(), ArchitectureError> {
let canonical_path = file_path.canonicalize().map_err(|error| {
ArchitectureError::new(format!(
"could not resolve `{}`: {error}",
file_path.display()
))
})?;
if !self.visited_files.insert(canonical_path) {
return Ok(());
}
let source = fs::read_to_string(file_path).map_err(|error| {
ArchitectureError::new(format!("could not read `{}`: {error}", file_path.display()))
})?;
let syntax = syn::parse_file(&source).map_err(|error| {
ArchitectureError::new(format!(
"could not parse `{}`: {error}",
file_path.display()
))
})?;
self.read_items(&syntax.items, module_path, child_directory)
}
fn read_items(
&mut self,
items: &[Item],
module_path: Vec<String>,
child_directory: &Path,
) -> Result<(), ArchitectureError> {
let mut module = Module {
uses: BTreeMap::new(),
globs: Vec::new(),
};
for item in items {
if let Item::Use(item_use) = item
&& !test_only(&item_use.attrs)
{
collect_use(&mut module, item_use);
}
}
self.modules.insert(module_path.clone(), module);
for item in items {
match item {
Item::Enum(item) if !test_only(&item.attrs) => self.insert_declaration(
&module_path,
item.ident.to_string(),
DeclarationKind::Enum(
item.variants
.iter()
.map(|variant| variant.ident.to_string())
.collect(),
),
)?,
Item::Struct(item) if !test_only(&item.attrs) => self.insert_declaration(
&module_path,
item.ident.to_string(),
DeclarationKind::Other,
)?,
Item::Union(item) if !test_only(&item.attrs) => self.insert_declaration(
&module_path,
item.ident.to_string(),
DeclarationKind::Other,
)?,
Item::Type(item) if !test_only(&item.attrs) => self.insert_declaration(
&module_path,
item.ident.to_string(),
DeclarationKind::Other,
)?,
Item::Impl(implementation) if !test_only(&implementation.attrs) => {
self.implementations.push(RawImpl {
module: module_path.clone(),
implementation: implementation.clone(),
});
}
Item::Mod(item_mod) => {
self.read_child_module(item_mod, &module_path, child_directory)?;
}
Item::Const(_)
| Item::Enum(_)
| Item::ExternCrate(_)
| Item::Fn(_)
| Item::ForeignMod(_)
| Item::Impl(_)
| Item::Macro(_)
| Item::Static(_)
| Item::Struct(_)
| Item::Trait(_)
| Item::TraitAlias(_)
| Item::Type(_)
| Item::Union(_)
| Item::Use(_)
| Item::Verbatim(_)
| _ => {}
}
}
Ok(())
}
fn read_child_module(
&mut self,
item_mod: &ItemMod,
parent_module: &[String],
child_directory: &Path,
) -> Result<(), ArchitectureError> {
if test_only(&item_mod.attrs) {
return Ok(());
}
let mut module_path = parent_module.to_vec();
module_path.push(item_mod.ident.to_string());
if let Some((_, items)) = &item_mod.content {
return self.read_items(
items,
module_path,
&child_directory.join(item_mod.ident.to_string()),
);
}
let direct_file = child_directory.join(format!("{}.rs", item_mod.ident));
let module_file = child_directory
.join(item_mod.ident.to_string())
.join("mod.rs");
match (direct_file.is_file(), module_file.is_file()) {
(true, false) => self.read_module_file(
&direct_file,
module_path,
&child_directory.join(item_mod.ident.to_string()),
),
(false, true) => self.read_module_file(
&module_file,
module_path,
&child_directory.join(item_mod.ident.to_string()),
),
(false, false) => Err(ArchitectureError::new(format!(
"module `{}` had no source at `{}` or `{}`",
qualified(&module_path),
direct_file.display(),
module_file.display()
))),
(true, true) => Err(ArchitectureError::new(format!(
"module `{}` had two possible source files",
qualified(&module_path)
))),
}
}
fn insert_declaration(
&mut self,
module: &[String],
name: String,
kind: DeclarationKind,
) -> Result<(), ArchitectureError> {
let mut path = module.to_vec();
path.push(name.clone());
if self.declarations.contains_key(&path) {
return Err(ArchitectureError::new(format!(
"type `{}` was declared more than once",
qualified(&path)
)));
}
self.declarations.insert(
path,
Declaration {
module: module.to_vec(),
name,
kind,
},
);
Ok(())
}
fn resolve_type(
&self,
module_path: &[String],
syntax: &Type,
) -> Result<&Declaration, ArchitectureError> {
let Type::Path(type_path) = syntax else {
return Err(ArchitectureError::new(format!(
"type `{}` was not a named path",
syntax.to_token_stream()
)));
};
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect();
let resolved = self.resolve_path(module_path, &segments)?;
self.declarations.get(&resolved).ok_or_else(|| {
ArchitectureError::new(format!(
"type `{}` resolved to `{}`, which was not declared in this crate",
syntax.to_token_stream(),
qualified(&resolved)
))
})
}
fn resolve_path(
&self,
module_path: &[String],
segments: &[String],
) -> Result<Vec<String>, ArchitectureError> {
let Some(first) = segments.first() else {
return Err(ArchitectureError::new("an empty Rust path was encountered"));
};
if first == "crate" {
return Ok(segments.iter().skip(1).cloned().collect());
}
if first == "self" {
let mut resolved = module_path.to_vec();
resolved.extend(segments.iter().skip(1).cloned());
return Ok(resolved);
}
if first == "super" {
let mut resolved = module_path.to_vec();
let mut remaining = segments.iter();
for segment in remaining.by_ref() {
if segment != "super" {
resolved.push(segment.clone());
break;
}
if resolved.pop().is_none() {
return Err(ArchitectureError::new(format!(
"path `{}` escaped the crate root",
segments.join("::")
)));
}
}
resolved.extend(remaining.cloned());
return Ok(resolved);
}
let module = self.modules.get(module_path).ok_or_else(|| {
ArchitectureError::new(format!(
"module `{}` was absent from the source model",
qualified(module_path)
))
})?;
if let Some(imported) = module.uses.get(first) {
let mut resolved = imported.clone();
resolved.extend(segments.iter().skip(1).cloned());
return self.resolve_import_target(module_path, &resolved);
}
let mut local = module_path.to_vec();
local.extend(segments.iter().cloned());
if self.declarations.contains_key(&local) {
return Ok(local);
}
let mut matches = Vec::new();
for glob in &module.globs {
let mut candidate = self.resolve_import_target(module_path, glob)?;
candidate.extend(segments.iter().cloned());
if self.declarations.contains_key(&candidate) {
matches.push(candidate);
}
}
match matches.as_slice() {
[resolved] => Ok(resolved.clone()),
[] => Ok(local),
_ => Err(ArchitectureError::new(format!(
"path `{}` was ambiguous through glob imports in `{}`",
segments.join("::"),
qualified(module_path)
))),
}
}
fn resolve_import_target(
&self,
module_path: &[String],
target: &[String],
) -> Result<Vec<String>, ArchitectureError> {
let Some(first) = target.first() else {
return Err(ArchitectureError::new(
"an empty import path was encountered",
));
};
let resolved = if first == "crate" || first == "self" || first == "super" {
self.resolve_path(module_path, target)?
} else {
let mut local = module_path.to_vec();
local.extend(target.iter().cloned());
if self.declarations.contains_key(&local) {
local
} else {
target.to_vec()
}
};
if self.declarations.contains_key(&resolved) {
return Ok(resolved);
}
let Some((name, owner)) = resolved.split_last() else {
return Ok(resolved);
};
let Some(module) = self.modules.get(owner) else {
return Ok(resolved);
};
let Some(reexported) = module.uses.get(name) else {
return Ok(resolved);
};
self.resolve_import_target(owner, reexported)
}
fn outcome_node(
&self,
module_path: &[String],
syntax: &Type,
) -> Result<DiagramNode, ArchitectureError> {
let Type::Path(type_path) = syntax else {
return Ok(DiagramNode {
label: syntax.to_token_stream().to_string(),
children: Vec::new(),
});
};
let Some(segment) = type_path.path.segments.last() else {
return Err(ArchitectureError::new("an outcome path had no segments"));
};
let name = segment.ident.to_string();
if name == "Result" || name == "Option" {
return self.standard_outcome_node(module_path, &name, &segment.arguments);
}
let declaration = self.resolve_type(module_path, syntax)?;
Ok(declaration_node(declaration))
}
fn standard_outcome_node(
&self,
module_path: &[String],
name: &str,
arguments: &PathArguments,
) -> Result<DiagramNode, ArchitectureError> {
let PathArguments::AngleBracketed(arguments) = arguments else {
return Err(ArchitectureError::new(format!(
"outcome `{name}` had no type arguments"
)));
};
let types: Vec<&Type> = arguments
.args
.iter()
.filter_map(|argument| {
if let GenericArgument::Type(argument) = argument {
Some(argument)
} else {
None
}
})
.collect();
let branch_names: &[&str] = if name == "Result" {
&["Ok", "Err"]
} else {
&["Some"]
};
if types.len() != branch_names.len() {
return Err(ArchitectureError::new(format!(
"outcome `{name}` had {} type arguments instead of {}",
types.len(),
branch_names.len()
)));
}
let mut children = Vec::new();
for (branch_name, argument) in branch_names.iter().zip(types) {
children.push(DiagramNode {
label: (*branch_name).to_owned(),
children: vec![self.outcome_node(module_path, argument)?],
});
}
if name == "Option" {
children.push(DiagramNode {
label: "None".to_owned(),
children: Vec::new(),
});
}
Ok(DiagramNode {
label: name.to_owned(),
children,
})
}
}
pub(super) fn read_crate(crate_root: &Path) -> Result<CrateArchitecture, ArchitectureError> {
let source_root = crate_root.join("src");
let library_root = source_root.join("lib.rs");
let mut model = SourceModel::new();
model.read_module_file(&library_root, Vec::new(), &source_root)?;
let mut machines: BTreeMap<Vec<String>, StateMachineDiagram> = BTreeMap::new();
for raw in &model.implementations {
if trait_name(&raw.implementation).as_deref() == Some("StateMachine") {
let machine = model.resolve_type(&raw.module, &raw.implementation.self_ty)?;
let qualified_name = declaration_path(machine);
machines
.entry(qualified_name.clone())
.or_insert_with(|| StateMachineDiagram {
name: machine.name.clone(),
qualified_name: qualified(&qualified_name),
steps: Vec::new(),
});
}
}
for raw in &model.implementations {
if trait_name(&raw.implementation).as_deref() != Some("StepInputOf") {
continue;
}
let machine_type = step_machine_type(&raw.implementation)?;
let machine = model.resolve_type(&raw.module, machine_type)?;
let machine_path = declaration_path(machine);
let input = model.resolve_type(&raw.module, &raw.implementation.self_ty)?;
let outcome = step_outcome_type(&raw.implementation)?;
let outcome = model.outcome_node(&raw.module, outcome)?;
let diagram = machines.get_mut(&machine_path).ok_or_else(|| {
ArchitectureError::new(format!(
"step input `{}` targeted `{}`, but no StateMachine implementation was found",
input.name,
qualified(&machine_path)
))
})?;
if diagram.steps.iter().any(|step| step.input == input.name) {
return Err(ArchitectureError::new(format!(
"state machine `{}` had more than one step for input `{}`",
diagram.qualified_name, input.name
)));
}
diagram.steps.push(StepDiagram {
input: input.name.clone(),
outcome,
});
}
let mut state_machines: Vec<StateMachineDiagram> = machines.into_values().collect();
for state_machine in &mut state_machines {
state_machine
.steps
.sort_by(|left, right| left.input.cmp(&right.input));
}
Ok(CrateArchitecture { state_machines })
}
fn declaration_node(declaration: &Declaration) -> DiagramNode {
let children = match &declaration.kind {
DeclarationKind::Enum(variants) => variants
.iter()
.map(|variant| DiagramNode {
label: variant.clone(),
children: Vec::new(),
})
.collect(),
DeclarationKind::Other => Vec::new(),
};
DiagramNode {
label: declaration.name.clone(),
children,
}
}
fn declaration_path(declaration: &Declaration) -> Vec<String> {
let mut path = declaration.module.clone();
path.push(declaration.name.clone());
path
}
fn trait_name(implementation: &ItemImpl) -> Option<String> {
implementation
.trait_
.as_ref()
.and_then(|(_, path, _)| path.segments.last())
.map(|segment| segment.ident.to_string())
}
fn step_machine_type(implementation: &ItemImpl) -> Result<&Type, ArchitectureError> {
let (_, path, _) = implementation
.trait_
.as_ref()
.ok_or_else(|| ArchitectureError::new("a StepInputOf implementation had no trait path"))?;
let segment = path.segments.last().ok_or_else(|| {
ArchitectureError::new("a StepInputOf implementation had an empty trait path")
})?;
let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
&segment.arguments
else {
return Err(ArchitectureError::new(
"a StepInputOf implementation did not name its state machine",
));
};
args.iter()
.find_map(|argument| {
if let GenericArgument::Type(machine) = argument {
Some(machine)
} else {
None
}
})
.ok_or_else(|| {
ArchitectureError::new(
"a StepInputOf implementation did not contain a state-machine type",
)
})
}
fn step_outcome_type(implementation: &ItemImpl) -> Result<&Type, ArchitectureError> {
implementation
.items
.iter()
.find_map(|item| {
if let ImplItem::Type(item) = item
&& item.ident == "Outcome"
{
Some(&item.ty)
} else {
None
}
})
.ok_or_else(|| {
ArchitectureError::new("a StepInputOf implementation did not declare its Outcome")
})
}
fn collect_use(module: &mut Module, item_use: &ItemUse) {
flatten_use(module, Vec::new(), &item_use.tree);
}
fn flatten_use(module: &mut Module, prefix: Vec<String>, tree: &UseTree) {
match tree {
UseTree::Path(path) => {
let mut next = prefix;
next.push(path.ident.to_string());
flatten_use(module, next, &path.tree);
}
UseTree::Name(name) => {
let mut target = prefix;
target.push(name.ident.to_string());
module.uses.insert(name.ident.to_string(), target);
}
UseTree::Rename(rename) => {
let mut target = prefix;
target.push(rename.ident.to_string());
module.uses.insert(rename.rename.to_string(), target);
}
UseTree::Glob(_) => module.globs.push(prefix),
UseTree::Group(group) => {
for item in &group.items {
flatten_use(module, prefix.clone(), item);
}
}
}
}
fn test_only(attributes: &[Attribute]) -> bool {
attributes.iter().any(|attribute| {
if !attribute.path().is_ident("cfg") {
return false;
}
let syn::Meta::List(list) = &attribute.meta else {
return false;
};
let condition = list.tokens.to_string();
condition == "test" || condition == "kani"
})
}
#[cfg(test)]
mod tests;
fn qualified(path: &[String]) -> String {
if path.is_empty() {
"crate".to_owned()
} else {
format!("crate::{}", path.join("::"))
}
}