use serde::{Deserialize, Serialize};
use std::collections::HashSet;
fn round_sig3(x: f64) -> f64 {
if !x.is_finite() {
return 0.0; }
if x == 0.0 {
return 0.0;
}
let abs = x.abs();
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let truncated = if abs >= 1.0 {
(abs * 1000.0).floor() / 1000.0
} else {
let d = abs.log10().floor() as i32;
let factor = 10f64.powi(2 - d);
(abs * factor).floor() / factor
};
truncated * sign
}
fn sig3<S: serde::Serializer>(v: &f64, s: S) -> Result<S::Ok, S::Error> {
let x = round_sig3(*v);
if x.fract() == 0.0 && x.abs() < i64::MAX as f64 {
s.serialize_i64(x as i64)
} else {
s.serialize_f64(x)
}
}
fn is_zero_f64(v: &f64) -> bool {
*v == 0.0
}
fn is_zero_u32(v: &u32) -> bool {
*v == 0
}
pub type NodeId = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Crate,
Module,
File,
Fn,
Method,
Impl,
Trait,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CycleKind {
TestEmbed,
Mutual,
Chain,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CycleGroup {
pub kind: CycleKind,
pub nodes: Vec<NodeId>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AvgCoupling {
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub fan_in: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub fan_out: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub hk: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GraphStats {
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub cyclomatic: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub cognitive: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coupling: Option<AvgCoupling>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maintainability: Option<Maintainability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub loc: Option<Loc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub halstead: Option<Halstead>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EdgeKind {
Contains,
Uses,
Reexports,
Calls,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Visibility {
Public,
Crate,
Super,
Restricted { path: String },
Private,
}
impl Serialize for Visibility {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Visibility::Public => s.serialize_str("public"),
Visibility::Private => s.serialize_str("private"),
Visibility::Crate => s.serialize_str("crate"),
Visibility::Super => s.serialize_str("super"),
Visibility::Restricted { path } => {
let mut map = s.serialize_map(Some(1))?;
map.serialize_entry("restricted", path)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for Visibility {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct VisVisitor;
impl<'de> Visitor<'de> for VisVisitor {
type Value = Visibility;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
r#"a visibility string ("public", "private", "crate", "super") \
or an object {"restricted": "<path>"} / {"kind": "<kind>", ...}"#,
)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Visibility, E> {
match v {
"public" => Ok(Visibility::Public),
"private" => Ok(Visibility::Private),
"crate" => Ok(Visibility::Crate),
"super" => Ok(Visibility::Super),
other => Err(E::unknown_variant(
other,
&["public", "private", "crate", "super"],
)),
}
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Visibility, A::Error> {
let mut kind: Option<String> = None;
let mut path: Option<String> = None;
let mut restricted: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"restricted" => restricted = Some(map.next_value()?),
"kind" => kind = Some(map.next_value()?),
"path" => path = Some(map.next_value()?),
_ => {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
if let Some(r) = restricted {
return Ok(Visibility::Restricted { path: r });
}
match kind.as_deref() {
Some("public") => Ok(Visibility::Public),
Some("private") => Ok(Visibility::Private),
Some("crate") => Ok(Visibility::Crate),
Some("super") => Ok(Visibility::Super),
Some("restricted") => {
let p = path.ok_or_else(|| de::Error::missing_field("path"))?;
Ok(Visibility::Restricted { path: p })
}
Some(other) => Err(de::Error::unknown_variant(
other,
&["public", "private", "crate", "super", "restricted"],
)),
None => Err(de::Error::missing_field("kind")),
}
}
}
d.deserialize_any(VisVisitor)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Loc {
#[serde(serialize_with = "sig3")]
pub source: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub logical: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub comments: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub blank: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Halstead {
#[serde(serialize_with = "sig3")]
pub length: f64,
#[serde(serialize_with = "sig3")]
pub vocabulary: f64,
#[serde(serialize_with = "sig3")]
pub volume: f64,
#[serde(serialize_with = "sig3")]
pub effort: f64,
#[serde(serialize_with = "sig3")]
pub time: f64,
#[serde(serialize_with = "sig3")]
pub bugs: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Maintainability {
#[serde(serialize_with = "sig3")]
pub mi: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub mi_sei: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Coupling {
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub fan_in: u32,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub fan_out: u32,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub hk: f64,
}
fn coupling_is_trivial(c: &Option<Coupling>) -> bool {
c.as_ref().is_none_or(|c| c.fan_in == 0 && c.fan_out == 0)
}
fn complexity_is_empty(c: &Option<Complexity>) -> bool {
c.as_ref().is_none_or(|c| {
c.cyclomatic == 0.0
&& c.cognitive == 0.0
&& c.exits == 0.0
&& c.args == 0.0
&& c.functions == 0.0
&& c.closures == 0.0
&& coupling_is_trivial(&c.coupling)
&& c.maintainability.is_none()
&& c.loc.is_none()
&& c.halstead.is_none()
})
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Complexity {
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub cyclomatic: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub cognitive: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub exits: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub args: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub functions: f64,
#[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
pub closures: f64,
#[serde(default, skip_serializing_if = "coupling_is_trivial")]
pub coupling: Option<Coupling>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maintainability: Option<Maintainability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub loc: Option<Loc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub halstead: Option<Halstead>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub id: NodeId,
pub kind: NodeKind,
pub name: String,
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<NodeId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<Visibility>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub loc: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub item_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method_count: Option<u32>,
#[serde(default, skip_serializing_if = "complexity_is_empty")]
pub complexity: Option<Complexity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cycle_kind: Option<CycleKind>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub from: NodeId,
pub to: NodeId,
pub kind: EdgeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unresolved: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<Visibility>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cycles: Vec<CycleGroup>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stats: Option<GraphStats>,
}
impl Graph {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn project(&self, node_kinds: &[NodeKind], edge_kinds: &[EdgeKind]) -> Graph {
let kept_ids: HashSet<&NodeId> = self
.nodes
.iter()
.filter(|n| node_kinds.contains(&n.kind))
.map(|n| &n.id)
.collect();
let nodes = self
.nodes
.iter()
.filter(|n| node_kinds.contains(&n.kind))
.cloned()
.collect();
let edges = self
.edges
.iter()
.filter(|e| {
edge_kinds.contains(&e.kind)
&& kept_ids.contains(&e.from)
&& kept_ids.contains(&e.to)
})
.cloned()
.collect();
Graph {
nodes,
edges,
cycles: Vec::new(),
stats: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn n(id: &str, kind: NodeKind) -> Node {
Node {
id: id.into(),
kind,
name: id.into(),
path: String::new(),
parent: None,
external: None,
visibility: None,
loc: None,
line: None,
item_count: None,
method_count: None,
complexity: None,
cycle_kind: None,
}
}
fn e(from: &str, to: &str, kind: EdgeKind) -> Edge {
Edge {
from: from.into(),
to: to.into(),
kind,
unresolved: None,
external: None,
visibility: None,
}
}
#[test]
fn project_keeps_only_matching_node_kinds() {
let g = Graph {
nodes: vec![n("c", NodeKind::Crate), n("m", NodeKind::Module)],
edges: vec![],
cycles: vec![],
stats: None,
};
let p = g.project(&[NodeKind::Crate], &[]);
assert_eq!(p.nodes.len(), 1);
assert_eq!(p.nodes[0].id, "c");
}
#[test]
fn project_drops_edges_to_filtered_out_nodes() {
let g = Graph {
nodes: vec![n("a", NodeKind::Crate), n("b", NodeKind::Module)],
edges: vec![e("a", "b", EdgeKind::Contains)],
cycles: vec![],
stats: None,
};
let p = g.project(&[NodeKind::Crate], &[EdgeKind::Contains]);
assert!(p.edges.is_empty());
}
#[test]
fn project_keeps_edges_between_kept_nodes() {
let g = Graph {
nodes: vec![n("a", NodeKind::Crate), n("b", NodeKind::Crate)],
edges: vec![e("a", "b", EdgeKind::Uses)],
cycles: vec![],
stats: None,
};
let p = g.project(&[NodeKind::Crate], &[EdgeKind::Uses]);
assert_eq!(p.edges.len(), 1);
}
#[test]
fn visibility_roundtrip_new_format() {
let cases: &[(&str, Visibility)] = &[
("\"public\"", Visibility::Public),
("\"private\"", Visibility::Private),
("\"crate\"", Visibility::Crate),
("\"super\"", Visibility::Super),
(
r#"{"restricted":"some::path"}"#,
Visibility::Restricted {
path: "some::path".into(),
},
),
];
for (json, expected) in cases {
let got: Visibility = serde_json::from_str(json).unwrap();
assert_eq!(got, expected.clone());
let re = serde_json::to_string(&got).unwrap();
let back: Visibility = serde_json::from_str(&re).unwrap();
assert_eq!(back, expected.clone());
}
}
#[test]
fn visibility_deserialize_old_tagged_format() {
let cases: &[(&str, Visibility)] = &[
(r#"{"kind":"public"}"#, Visibility::Public),
(r#"{"kind":"private"}"#, Visibility::Private),
(r#"{"kind":"crate"}"#, Visibility::Crate),
(r#"{"kind":"super"}"#, Visibility::Super),
(
r#"{"kind":"restricted","path":"some::path"}"#,
Visibility::Restricted {
path: "some::path".into(),
},
),
];
for (json, expected) in cases {
let got: Visibility = serde_json::from_str(json).unwrap();
assert_eq!(got, expected.clone());
}
}
#[test]
fn complexity_coupling_roundtrip() {
let cx = Complexity {
cyclomatic: 3.0,
coupling: Some(Coupling {
fan_in: 2,
fan_out: 4,
hk: 576.0,
}),
..Default::default()
};
let json = serde_json::to_string(&cx).unwrap();
let back: Complexity = serde_json::from_str(&json).unwrap();
assert_eq!(back.cyclomatic, 3.0);
let c = back.coupling.unwrap();
assert_eq!(c.fan_in, 2);
assert_eq!(c.fan_out, 4);
}
}