use crate::code_lint::ast::{FindingFact, OrderingReason, RelationKind, Visibility};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(super) struct NodeId(pub(super) String);
impl NodeId {
#[must_use]
pub(super) fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub(super) fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for NodeId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct SourceLocation {
pub(super) file: String,
pub(super) line: usize,
pub(super) column: usize,
pub(super) end_line: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum DeclarationKind {
Callable,
Method,
Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct DeclarationNode {
pub(super) id: NodeId,
pub(super) name: String,
pub(super) signature: Option<String>,
pub(super) scope_id: usize,
pub(super) kind: DeclarationKind,
pub(super) visibility: Visibility,
pub(super) is_constructor: bool,
pub(super) location: SourceLocation,
pub(super) owners: Vec<NodeId>,
pub(super) recursion_group: usize,
pub(super) level: usize,
pub(super) findings: Vec<FindingFact>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum GroupKind {
Component {
declared_dependencies: Option<Vec<String>>,
},
Module {
path: String,
},
Owner {
root: NodeId,
},
Shared {
owners: Vec<NodeId>,
},
Unowned,
TypeMembers {
type_id: NodeId,
},
RecursionGroup {
group_id: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct GroupNode {
pub(super) id: NodeId,
pub(super) label: String,
pub(super) kind: GroupKind,
pub(super) recursion_group: usize,
pub(super) level: usize,
pub(super) children: Vec<NodeId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(super) enum EdgeReason {
Call,
Containment,
ImplBehavior,
Encapsulation,
SignatureContract,
CrossModule,
}
impl From<OrderingReason> for EdgeReason {
fn from(reason: OrderingReason) -> Self {
match reason {
OrderingReason::Call => Self::Call,
OrderingReason::Containment => Self::Containment,
OrderingReason::ImplBehavior => Self::ImplBehavior,
OrderingReason::Encapsulation => Self::Encapsulation,
OrderingReason::SignatureContract => Self::SignatureContract,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(super) enum ReflexionStatus {
Convergent,
Absent,
Divergent,
Inferred,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct OrderingEdge {
pub(super) higher: NodeId,
pub(super) lower: NodeId,
pub(super) reason: EdgeReason,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct ReferenceEdge {
pub(super) source: NodeId,
pub(super) target: NodeId,
pub(super) kind: RelationKind,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct SummaryEdge {
pub(super) higher: NodeId,
pub(super) lower: NodeId,
pub(super) reason: EdgeReason,
pub(super) count: usize,
pub(super) reflexion: Option<ReflexionStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ModelError {
EmptyNodeId,
DuplicateNodeId(NodeId),
UnknownRelationEndpoint(NodeId),
UnknownChild {
group: NodeId,
child: NodeId,
},
NodeInMultipleGroups(NodeId),
ContainmentCycle(NodeId),
SharedItemInSingleOwner {
item: NodeId,
owner: NodeId,
},
PrivateItemInWrongOwner {
item: NodeId,
owner: NodeId,
},
InvalidSharedGroup(NodeId),
UpwardOrderingRelation {
higher: NodeId,
lower: NodeId,
higher_level: usize,
lower_level: usize,
},
EmptySummaryRelation {
higher: NodeId,
lower: NodeId,
},
}
impl fmt::Display for ModelError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyNodeId => formatter.write_str("node identifier must not be empty"),
Self::DuplicateNodeId(id) => write!(formatter, "duplicate node identifier: {id}"),
Self::UnknownRelationEndpoint(id) => {
write!(formatter, "relation references unknown node: {id}")
}
Self::UnknownChild { group, child } => {
write!(formatter, "group {group} references unknown child {child}")
}
Self::NodeInMultipleGroups(id) => {
write!(formatter, "node {id} is placed in multiple groups")
}
Self::ContainmentCycle(id) => {
write!(formatter, "containment cycle involving group {id}")
}
Self::SharedItemInSingleOwner { item, owner } => {
write!(
formatter,
"shared item {item} cannot be placed inside single owner {owner}"
)
}
Self::PrivateItemInWrongOwner { item, owner } => {
write!(
formatter,
"private item {item} does not belong to single owner {owner}"
)
}
Self::InvalidSharedGroup(id) => {
write!(
formatter,
"shared group {id} has invalid owners or member ownership"
)
}
Self::UpwardOrderingRelation {
higher,
lower,
higher_level,
lower_level,
} => write!(
formatter,
"ordering relation {higher} (level {higher_level}) -> {lower} (level {lower_level}) points upward outside a recursion group"
),
Self::EmptySummaryRelation { higher, lower } => {
write!(
formatter,
"summary relation {higher} -> {lower} has zero count"
)
}
}
}
}
impl std::error::Error for ModelError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct GraphParts {
pub(super) title: String,
pub(super) declarations: Vec<DeclarationNode>,
pub(super) groups: Vec<GroupNode>,
pub(super) ordering_edges: Vec<OrderingEdge>,
pub(super) reference_edges: Vec<ReferenceEdge>,
pub(super) summary_edges: Vec<SummaryEdge>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct AbstractionGraph {
title: String,
declarations: Vec<DeclarationNode>,
groups: Vec<GroupNode>,
ordering_edges: Vec<OrderingEdge>,
reference_edges: Vec<ReferenceEdge>,
summary_edges: Vec<SummaryEdge>,
parent_by_child: BTreeMap<NodeId, NodeId>,
}
impl AbstractionGraph {
pub(super) fn new(parts: GraphParts) -> Result<Self, ModelError> {
let decl_by_id = index_declarations(&parts.declarations)?;
let group_by_id = index_groups(&parts.groups, &decl_by_id)?;
let parent_by_child = validate_containment(&parts.groups, &decl_by_id, &group_by_id)?;
validate_ownership_groups(&parts.groups, &decl_by_id)?;
validate_relations(
&parts.ordering_edges,
&parts.reference_edges,
&parts.summary_edges,
&decl_by_id,
&group_by_id,
&parent_by_child,
)?;
Ok(Self {
title: parts.title,
declarations: parts.declarations,
groups: parts.groups,
ordering_edges: parts.ordering_edges,
reference_edges: parts.reference_edges,
summary_edges: parts.summary_edges,
parent_by_child,
})
}
#[must_use]
pub(super) fn title(&self) -> &str {
&self.title
}
#[must_use]
pub(super) fn declarations(&self) -> &[DeclarationNode] {
&self.declarations
}
#[must_use]
pub(super) fn groups(&self) -> &[GroupNode] {
&self.groups
}
#[must_use]
pub(super) fn ordering_edges(&self) -> &[OrderingEdge] {
&self.ordering_edges
}
#[must_use]
pub(super) fn reference_edges(&self) -> &[ReferenceEdge] {
&self.reference_edges
}
#[must_use]
pub(super) fn summary_edges(&self) -> &[SummaryEdge] {
&self.summary_edges
}
#[must_use]
pub(super) fn parent_of(&self, node_id: &NodeId) -> Option<&NodeId> {
self.parent_by_child.get(node_id)
}
}
fn index_declarations(
declarations: &[DeclarationNode],
) -> Result<BTreeMap<NodeId, &DeclarationNode>, ModelError> {
let mut by_id = BTreeMap::new();
for decl in declarations {
if decl.id.as_str().is_empty() {
return Err(ModelError::EmptyNodeId);
}
if by_id.insert(decl.id.clone(), decl).is_some() {
return Err(ModelError::DuplicateNodeId(decl.id.clone()));
}
}
Ok(by_id)
}
fn index_groups<'a>(
groups: &'a [GroupNode],
decl_by_id: &BTreeMap<NodeId, &DeclarationNode>,
) -> Result<BTreeMap<NodeId, &'a GroupNode>, ModelError> {
let mut by_id = BTreeMap::new();
for group in groups {
if group.id.as_str().is_empty() {
return Err(ModelError::EmptyNodeId);
}
if decl_by_id.contains_key(&group.id) || by_id.insert(group.id.clone(), group).is_some() {
return Err(ModelError::DuplicateNodeId(group.id.clone()));
}
}
Ok(by_id)
}
fn validate_containment(
groups: &[GroupNode],
decl_by_id: &BTreeMap<NodeId, &DeclarationNode>,
group_by_id: &BTreeMap<NodeId, &GroupNode>,
) -> Result<BTreeMap<NodeId, NodeId>, ModelError> {
let mut parent_by_child = BTreeMap::new();
for group in groups {
for child in &group.children {
if !decl_by_id.contains_key(child) && !group_by_id.contains_key(child) {
return Err(ModelError::UnknownChild {
group: group.id.clone(),
child: child.clone(),
});
}
if parent_by_child
.insert(child.clone(), group.id.clone())
.is_some()
{
return Err(ModelError::NodeInMultipleGroups(child.clone()));
}
}
}
for group in groups {
let mut visited = BTreeSet::new();
let mut cursor = Some(&group.id);
while let Some(current) = cursor {
if !visited.insert(current.clone()) {
return Err(ModelError::ContainmentCycle(group.id.clone()));
}
cursor = parent_by_child.get(current);
}
}
Ok(parent_by_child)
}
fn validate_ownership_groups(
groups: &[GroupNode],
decl_by_id: &BTreeMap<NodeId, &DeclarationNode>,
) -> Result<(), ModelError> {
for group in groups {
match &group.kind {
GroupKind::Owner { root } => {
if !decl_by_id.contains_key(root) {
return Err(ModelError::UnknownRelationEndpoint(root.clone()));
}
for child_id in &group.children {
if let Some(decl) = decl_by_id.get(child_id)
&& decl.visibility == Visibility::Private
{
if decl.owners.len() >= 2 {
return Err(ModelError::SharedItemInSingleOwner {
item: decl.id.clone(),
owner: root.clone(),
});
}
if decl.owners.as_slice() != [root.clone()] {
return Err(ModelError::PrivateItemInWrongOwner {
item: decl.id.clone(),
owner: root.clone(),
});
}
}
}
}
GroupKind::Shared { owners } => {
if owners.len() < 2 {
return Err(ModelError::InvalidSharedGroup(group.id.clone()));
}
for owner in owners {
if !decl_by_id.contains_key(owner) {
return Err(ModelError::UnknownRelationEndpoint(owner.clone()));
}
}
for child_id in &group.children {
if let Some(decl) = decl_by_id.get(child_id)
&& decl.owners != *owners
{
return Err(ModelError::InvalidSharedGroup(group.id.clone()));
}
}
}
GroupKind::TypeMembers { type_id } => {
if !decl_by_id.contains_key(type_id) {
return Err(ModelError::UnknownRelationEndpoint(type_id.clone()));
}
}
GroupKind::Unowned
| GroupKind::RecursionGroup { .. }
| GroupKind::Component { .. }
| GroupKind::Module { .. } => {}
}
}
Ok(())
}
fn validate_relations(
ordering_edges: &[OrderingEdge],
reference_edges: &[ReferenceEdge],
summary_edges: &[SummaryEdge],
decl_by_id: &BTreeMap<NodeId, &DeclarationNode>,
group_by_id: &BTreeMap<NodeId, &GroupNode>,
parent_by_child: &BTreeMap<NodeId, NodeId>,
) -> Result<(), ModelError> {
for edge in ordering_edges {
let higher = decl_by_id
.get(&edge.higher)
.ok_or_else(|| ModelError::UnknownRelationEndpoint(edge.higher.clone()))?;
let lower = decl_by_id
.get(&edge.lower)
.ok_or_else(|| ModelError::UnknownRelationEndpoint(edge.lower.clone()))?;
let same_level_domain = group_by_id.is_empty() || higher.owners == lower.owners;
if higher.scope_id == lower.scope_id
&& same_level_domain
&& higher.recursion_group != lower.recursion_group
&& higher.level < lower.level
{
return Err(ModelError::UpwardOrderingRelation {
higher: edge.higher.clone(),
lower: edge.lower.clone(),
higher_level: higher.level,
lower_level: lower.level,
});
}
}
for edge in reference_edges {
if !decl_by_id.contains_key(&edge.source) {
return Err(ModelError::UnknownRelationEndpoint(edge.source.clone()));
}
if !decl_by_id.contains_key(&edge.target) {
return Err(ModelError::UnknownRelationEndpoint(edge.target.clone()));
}
}
for edge in summary_edges {
let higher_group = group_by_id
.get(&edge.higher)
.ok_or_else(|| ModelError::UnknownRelationEndpoint(edge.higher.clone()))?;
let lower_group = group_by_id
.get(&edge.lower)
.ok_or_else(|| ModelError::UnknownRelationEndpoint(edge.lower.clone()))?;
if edge.count == 0 && edge.reflexion != Some(ReflexionStatus::Absent) {
return Err(ModelError::EmptySummaryRelation {
higher: edge.higher.clone(),
lower: edge.lower.clone(),
});
}
let same_parent = parent_by_child.get(&edge.higher) == parent_by_child.get(&edge.lower);
if same_parent
&& edge.reflexion != Some(ReflexionStatus::Divergent)
&& higher_group.recursion_group != lower_group.recursion_group
&& higher_group.level < lower_group.level
{
return Err(ModelError::UpwardOrderingRelation {
higher: edge.higher.clone(),
lower: edge.lower.clone(),
higher_level: higher_group.level,
lower_level: lower_group.level,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_declaration(
id: &str,
visibility: Visibility,
owners: &[&str],
level: usize,
recursion_group: usize,
) -> DeclarationNode {
DeclarationNode {
id: NodeId::new(id),
name: id.to_owned(),
signature: None,
scope_id: 0,
kind: DeclarationKind::Callable,
visibility,
is_constructor: false,
location: SourceLocation {
file: "src/sample.rs".to_owned(),
line: 1,
column: 1,
end_line: 1,
},
owners: owners.iter().map(|owner| NodeId::new(*owner)).collect(),
recursion_group,
level,
findings: Vec::new(),
}
}
#[test]
fn rejects_duplicate_node_identifier() {
let first = sample_declaration("dup", Visibility::Public, &["dup"], 0, 0);
let second = sample_declaration("dup", Visibility::Private, &["dup"], 0, 1);
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: vec![first, second],
groups: Vec::new(),
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(result, Err(ModelError::DuplicateNodeId(NodeId::new("dup"))));
}
#[test]
fn rejects_relation_with_missing_endpoint() {
let entry = sample_declaration("entry", Visibility::Public, &["entry"], 1, 0);
let edge = OrderingEdge {
higher: NodeId::new("entry"),
lower: NodeId::new("ghost"),
reason: EdgeReason::Call,
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: vec![entry],
groups: Vec::new(),
ordering_edges: vec![edge],
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(
result,
Err(ModelError::UnknownRelationEndpoint(NodeId::new("ghost")))
);
}
#[test]
fn rejects_item_placed_in_two_groups() {
let pub_a = sample_declaration("pub_a", Visibility::Public, &["pub_a"], 1, 0);
let helper = sample_declaration("helper", Visibility::Private, &["pub_a"], 0, 1);
let group_one = GroupNode {
id: NodeId::new("g1"),
label: "g1".to_owned(),
kind: GroupKind::RecursionGroup { group_id: 1 },
recursion_group: 0,
level: 1,
children: vec![NodeId::new("pub_a"), NodeId::new("helper")],
};
let group_two = GroupNode {
id: NodeId::new("g2"),
label: "g2".to_owned(),
kind: GroupKind::RecursionGroup { group_id: 0 },
recursion_group: 0,
level: 0,
children: vec![NodeId::new("helper")],
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: vec![pub_a, helper],
groups: vec![group_one, group_two],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(
result,
Err(ModelError::NodeInMultipleGroups(NodeId::new("helper")))
);
}
#[test]
fn rejects_containment_cycle_between_groups() {
let group_a = GroupNode {
id: NodeId::new("ga"),
label: "ga".to_owned(),
kind: GroupKind::RecursionGroup { group_id: 1 },
recursion_group: 0,
level: 1,
children: vec![NodeId::new("gb")],
};
let group_b = GroupNode {
id: NodeId::new("gb"),
label: "gb".to_owned(),
kind: GroupKind::RecursionGroup { group_id: 0 },
recursion_group: 0,
level: 0,
children: vec![NodeId::new("ga")],
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: Vec::new(),
groups: vec![group_a, group_b],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(result, Err(ModelError::ContainmentCycle(NodeId::new("ga"))));
}
#[test]
fn rejects_shared_item_inside_single_owner_group() {
let pub_a = sample_declaration("pub_a", Visibility::Public, &["pub_a"], 1, 0);
let pub_b = sample_declaration("pub_b", Visibility::Public, &["pub_b"], 1, 1);
let shared = sample_declaration("shared", Visibility::Private, &["pub_a", "pub_b"], 0, 2);
let owner_group = GroupNode {
id: NodeId::new("owner:pub_a"),
label: "pub_a".to_owned(),
kind: GroupKind::Owner {
root: NodeId::new("pub_a"),
},
recursion_group: 0,
level: 1,
children: vec![NodeId::new("pub_a"), NodeId::new("shared")],
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: vec![pub_a, pub_b, shared],
groups: vec![owner_group],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(
result,
Err(ModelError::SharedItemInSingleOwner {
item: NodeId::new("shared"),
owner: NodeId::new("pub_a"),
})
);
}
#[test]
fn rejects_private_item_inside_wrong_owner_group() {
let pub_a = sample_declaration("pub_a", Visibility::Public, &["pub_a"], 1, 0);
let pub_b = sample_declaration("pub_b", Visibility::Public, &["pub_b"], 1, 1);
let helper_b = sample_declaration("helper_b", Visibility::Private, &["pub_b"], 0, 2);
let owner_a = GroupNode {
id: NodeId::new("owner:pub_a"),
label: "pub_a".to_owned(),
kind: GroupKind::Owner {
root: NodeId::new("pub_a"),
},
recursion_group: 0,
level: 1,
children: vec![NodeId::new("pub_a"), NodeId::new("helper_b")],
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: vec![pub_a, pub_b, helper_b],
groups: vec![owner_a],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(
result,
Err(ModelError::PrivateItemInWrongOwner {
item: NodeId::new("helper_b"),
owner: NodeId::new("pub_a"),
})
);
}
#[test]
fn rejects_upward_ordering_relation_outside_recursion_group() {
let low = sample_declaration("low", Visibility::Public, &["low"], 0, 0);
let high = sample_declaration("high", Visibility::Private, &["low"], 2, 1);
let upward_edge = OrderingEdge {
higher: NodeId::new("low"),
lower: NodeId::new("high"),
reason: EdgeReason::Call,
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: vec![low, high],
groups: Vec::new(),
ordering_edges: vec![upward_edge],
reference_edges: Vec::new(),
summary_edges: Vec::new(),
});
assert_eq!(
result,
Err(ModelError::UpwardOrderingRelation {
higher: NodeId::new("low"),
lower: NodeId::new("high"),
higher_level: 0,
lower_level: 2,
})
);
}
#[test]
fn accepts_absent_reflexion_summary_edge_with_zero_count() {
let comp_high = GroupNode {
id: NodeId::new("comp:Runner"),
label: "Runner".to_owned(),
kind: GroupKind::Component {
declared_dependencies: None,
},
recursion_group: 1,
level: 1,
children: Vec::new(),
};
let comp_low = GroupNode {
id: NodeId::new("comp:Rules"),
label: "Rules".to_owned(),
kind: GroupKind::Component {
declared_dependencies: None,
},
recursion_group: 0,
level: 0,
children: Vec::new(),
};
let absent_edge = SummaryEdge {
higher: NodeId::new("comp:Runner"),
lower: NodeId::new("comp:Rules"),
reason: EdgeReason::CrossModule,
count: 0,
reflexion: Some(ReflexionStatus::Absent),
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: Vec::new(),
groups: vec![comp_high, comp_low],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: vec![absent_edge],
});
assert!(result.is_ok());
}
#[test]
fn rejects_non_absent_summary_edge_with_zero_count() {
let comp_high = GroupNode {
id: NodeId::new("comp:Runner"),
label: "Runner".to_owned(),
kind: GroupKind::Component {
declared_dependencies: None,
},
recursion_group: 1,
level: 1,
children: Vec::new(),
};
let comp_low = GroupNode {
id: NodeId::new("comp:Rules"),
label: "Rules".to_owned(),
kind: GroupKind::Component {
declared_dependencies: None,
},
recursion_group: 0,
level: 0,
children: Vec::new(),
};
let zero_edge = SummaryEdge {
higher: NodeId::new("comp:Runner"),
lower: NodeId::new("comp:Rules"),
reason: EdgeReason::CrossModule,
count: 0,
reflexion: Some(ReflexionStatus::Convergent),
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: Vec::new(),
groups: vec![comp_high, comp_low],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: vec![zero_edge],
});
assert_eq!(
result,
Err(ModelError::EmptySummaryRelation {
higher: NodeId::new("comp:Runner"),
lower: NodeId::new("comp:Rules"),
})
);
}
#[test]
fn accepts_divergent_reflexion_summary_edge_pointing_upward() {
let comp_high = GroupNode {
id: NodeId::new("comp:Runner"),
label: "Runner".to_owned(),
kind: GroupKind::Component {
declared_dependencies: None,
},
recursion_group: 1,
level: 1,
children: Vec::new(),
};
let comp_low = GroupNode {
id: NodeId::new("comp:Rules"),
label: "Rules".to_owned(),
kind: GroupKind::Component {
declared_dependencies: None,
},
recursion_group: 0,
level: 0,
children: Vec::new(),
};
let divergent_edge = SummaryEdge {
higher: NodeId::new("comp:Rules"),
lower: NodeId::new("comp:Runner"),
reason: EdgeReason::CrossModule,
count: 2,
reflexion: Some(ReflexionStatus::Divergent),
};
let result = AbstractionGraph::new(GraphParts {
title: "test".to_owned(),
declarations: Vec::new(),
groups: vec![comp_high, comp_low],
ordering_edges: Vec::new(),
reference_edges: Vec::new(),
summary_edges: vec![divergent_edge],
});
assert!(result.is_ok());
}
}