1use serde::{Deserialize, Serialize};
2use std::collections::HashSet;
3
4fn round_sig3(x: f64) -> f64 {
5 if !x.is_finite() {
6 return 0.0; }
8 if x == 0.0 {
9 return 0.0;
10 }
11 let abs = x.abs();
12 let sign = if x < 0.0 { -1.0 } else { 1.0 };
13 let truncated = if abs >= 1.0 {
14 (abs * 1000.0).floor() / 1000.0
16 } else {
17 let d = abs.log10().floor() as i32;
19 let factor = 10f64.powi(2 - d);
20 (abs * factor).floor() / factor
21 };
22 truncated * sign
23}
24
25fn sig3<S: serde::Serializer>(v: &f64, s: S) -> Result<S::Ok, S::Error> {
26 let x = round_sig3(*v);
27 if x.fract() == 0.0 && x.abs() < i64::MAX as f64 {
28 s.serialize_i64(x as i64)
29 } else {
30 s.serialize_f64(x)
31 }
32}
33
34fn is_zero_f64(v: &f64) -> bool {
35 *v == 0.0
36}
37
38fn is_zero_u32(v: &u32) -> bool {
39 *v == 0
40}
41
42pub type NodeId = String;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum NodeKind {
47 File,
49 External,
52 Crate,
56 Module,
57 Trait,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum CycleKind {
65 TestEmbed,
69 Mutual,
71 Chain,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct CycleGroup {
79 pub kind: CycleKind,
80 pub nodes: Vec<NodeId>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, Default)]
85pub struct AvgCoupling {
86 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
87 pub fan_in: f64,
88 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
89 pub fan_out: f64,
90 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
91 pub hk: f64,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, Default)]
96pub struct GraphStats {
97 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
98 pub cyclomatic: f64,
99 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
100 pub cognitive: f64,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub coupling: Option<AvgCoupling>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub maintainability: Option<Maintainability>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub loc: Option<Loc>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub halstead: Option<Halstead>,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum EdgeKind {
114 Contains,
117 Uses,
118 Reexports,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum Visibility {
131 Public,
132 Crate,
133 Super,
134 Restricted { path: String },
135 Private,
136}
137
138impl Serialize for Visibility {
139 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
140 use serde::ser::SerializeMap;
141 match self {
142 Visibility::Public => s.serialize_str("public"),
143 Visibility::Private => s.serialize_str("private"),
144 Visibility::Crate => s.serialize_str("crate"),
145 Visibility::Super => s.serialize_str("super"),
146 Visibility::Restricted { path } => {
147 let mut map = s.serialize_map(Some(1))?;
148 map.serialize_entry("restricted", path)?;
149 map.end()
150 }
151 }
152 }
153}
154
155impl<'de> Deserialize<'de> for Visibility {
156 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
157 use serde::de::{self, MapAccess, Visitor};
158 use std::fmt;
159
160 struct VisVisitor;
161
162 impl<'de> Visitor<'de> for VisVisitor {
163 type Value = Visibility;
164
165 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(
167 r#"a visibility string ("public", "private", "crate", "super") \
168 or an object {"restricted": "<path>"} / {"kind": "<kind>", ...}"#,
169 )
170 }
171
172 fn visit_str<E: de::Error>(self, v: &str) -> Result<Visibility, E> {
174 match v {
175 "public" => Ok(Visibility::Public),
176 "private" => Ok(Visibility::Private),
177 "crate" => Ok(Visibility::Crate),
178 "super" => Ok(Visibility::Super),
179 other => Err(E::unknown_variant(
180 other,
181 &["public", "private", "crate", "super"],
182 )),
183 }
184 }
185
186 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Visibility, A::Error> {
189 let mut kind: Option<String> = None;
190 let mut path: Option<String> = None;
191 let mut restricted: Option<String> = None;
192
193 while let Some(key) = map.next_key::<String>()? {
194 match key.as_str() {
195 "restricted" => restricted = Some(map.next_value()?),
197 "kind" => kind = Some(map.next_value()?),
199 "path" => path = Some(map.next_value()?),
200 _ => {
201 let _: serde::de::IgnoredAny = map.next_value()?;
202 }
203 }
204 }
205
206 if let Some(r) = restricted {
207 return Ok(Visibility::Restricted { path: r });
208 }
209
210 match kind.as_deref() {
211 Some("public") => Ok(Visibility::Public),
212 Some("private") => Ok(Visibility::Private),
213 Some("crate") => Ok(Visibility::Crate),
214 Some("super") => Ok(Visibility::Super),
215 Some("restricted") => {
216 let p = path.ok_or_else(|| de::Error::missing_field("path"))?;
217 Ok(Visibility::Restricted { path: p })
218 }
219 Some(other) => Err(de::Error::unknown_variant(
220 other,
221 &["public", "private", "crate", "super", "restricted"],
222 )),
223 None => Err(de::Error::missing_field("kind")),
224 }
225 }
226 }
227
228 d.deserialize_any(VisVisitor)
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct Loc {
236 #[serde(serialize_with = "sig3")]
238 pub source: f64,
239 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
241 pub logical: f64,
242 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
244 pub comments: f64,
245 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
247 pub blank: f64,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct Halstead {
252 #[serde(serialize_with = "sig3")]
253 pub length: f64,
254 #[serde(serialize_with = "sig3")]
255 pub vocabulary: f64,
256 #[serde(serialize_with = "sig3")]
257 pub volume: f64,
258 #[serde(serialize_with = "sig3")]
259 pub effort: f64,
260 #[serde(serialize_with = "sig3")]
261 pub time: f64,
262 #[serde(serialize_with = "sig3")]
263 pub bugs: f64,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct Maintainability {
268 #[serde(serialize_with = "sig3")]
269 pub mi: f64,
270 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
271 pub mi_sei: f64,
272}
273
274#[derive(Debug, Clone, Default, Serialize, Deserialize)]
275pub struct Coupling {
276 #[serde(default, skip_serializing_if = "is_zero_u32")]
277 pub fan_in: u32,
278 #[serde(default, skip_serializing_if = "is_zero_u32")]
279 pub fan_out: u32,
280 #[serde(default, skip_serializing_if = "is_zero_u32")]
283 pub fan_out_external: u32,
284 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
285 pub hk: f64,
286}
287
288fn coupling_is_trivial(c: &Option<Coupling>) -> bool {
289 c.as_ref()
290 .is_none_or(|c| c.fan_in == 0 && c.fan_out == 0 && c.fan_out_external == 0)
291}
292
293fn complexity_is_empty(c: &Option<Complexity>) -> bool {
294 c.as_ref().is_none_or(|c| {
295 c.cyclomatic == 0.0
296 && c.cognitive == 0.0
297 && c.exits == 0.0
298 && c.args == 0.0
299 && c.functions == 0.0
300 && c.closures == 0.0
301 && coupling_is_trivial(&c.coupling)
302 && c.maintainability.is_none()
303 && c.loc.is_none()
304 && c.halstead.is_none()
305 })
306}
307
308#[derive(Debug, Clone, Default, Serialize, Deserialize)]
312pub struct Complexity {
313 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
314 pub cyclomatic: f64,
315 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
316 pub cognitive: f64,
317 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
318 pub exits: f64,
319 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
320 pub args: f64,
321 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
322 pub functions: f64,
323 #[serde(default, serialize_with = "sig3", skip_serializing_if = "is_zero_f64")]
324 pub closures: f64,
325 #[serde(default, skip_serializing_if = "coupling_is_trivial")]
326 pub coupling: Option<Coupling>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub maintainability: Option<Maintainability>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub loc: Option<Loc>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub halstead: Option<Halstead>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct Node {
337 pub id: NodeId,
338 pub kind: NodeKind,
339 pub name: String,
340 pub path: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub parent: Option<NodeId>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub external: Option<bool>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub version: Option<String>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub visibility: Option<Visibility>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub loc: Option<u32>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub line: Option<u32>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub item_count: Option<u32>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub method_count: Option<u32>,
362 #[serde(default, skip_serializing_if = "complexity_is_empty")]
363 pub complexity: Option<Complexity>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub cycle_kind: Option<CycleKind>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct Edge {
371 pub from: NodeId,
372 pub to: NodeId,
373 pub kind: EdgeKind,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub unresolved: Option<bool>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub external: Option<bool>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub visibility: Option<Visibility>,
380}
381
382#[derive(Debug, Clone, Default, Serialize, Deserialize)]
383pub struct Graph {
384 pub nodes: Vec<Node>,
385 pub edges: Vec<Edge>,
386 #[serde(default, skip_serializing_if = "Vec::is_empty")]
388 pub cycles: Vec<CycleGroup>,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub stats: Option<GraphStats>,
392}
393
394impl Graph {
395 pub fn new() -> Self {
396 Self::default()
397 }
398
399 pub fn is_empty(&self) -> bool {
400 self.nodes.is_empty()
401 }
402
403 pub fn project(&self, node_kinds: &[NodeKind], edge_kinds: &[EdgeKind]) -> Graph {
404 let kept_ids: HashSet<&NodeId> = self
405 .nodes
406 .iter()
407 .filter(|n| node_kinds.contains(&n.kind))
408 .map(|n| &n.id)
409 .collect();
410 let nodes = self
411 .nodes
412 .iter()
413 .filter(|n| node_kinds.contains(&n.kind))
414 .cloned()
415 .collect();
416 let edges = self
417 .edges
418 .iter()
419 .filter(|e| {
420 edge_kinds.contains(&e.kind)
421 && kept_ids.contains(&e.from)
422 && kept_ids.contains(&e.to)
423 })
424 .cloned()
425 .collect();
426 Graph {
427 nodes,
428 edges,
429 cycles: Vec::new(),
430 stats: None,
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 fn n(id: &str, kind: NodeKind) -> Node {
440 Node {
441 id: id.into(),
442 kind,
443 name: id.into(),
444 path: String::new(),
445 parent: None,
446 external: None,
447 version: None,
448 visibility: None,
449 loc: None,
450 line: None,
451 item_count: None,
452 method_count: None,
453 complexity: None,
454 cycle_kind: None,
455 }
456 }
457
458 fn e(from: &str, to: &str, kind: EdgeKind) -> Edge {
459 Edge {
460 from: from.into(),
461 to: to.into(),
462 kind,
463 unresolved: None,
464 external: None,
465 visibility: None,
466 }
467 }
468
469 #[test]
470 fn project_keeps_only_matching_node_kinds() {
471 let g = Graph {
472 nodes: vec![n("c", NodeKind::Crate), n("m", NodeKind::Module)],
473 edges: vec![],
474 cycles: vec![],
475 stats: None,
476 };
477 let p = g.project(&[NodeKind::Crate], &[]);
478 assert_eq!(p.nodes.len(), 1);
479 assert_eq!(p.nodes[0].id, "c");
480 }
481
482 #[test]
483 fn project_drops_edges_to_filtered_out_nodes() {
484 let g = Graph {
485 nodes: vec![n("a", NodeKind::Crate), n("b", NodeKind::Module)],
486 edges: vec![e("a", "b", EdgeKind::Contains)],
487 cycles: vec![],
488 stats: None,
489 };
490 let p = g.project(&[NodeKind::Crate], &[EdgeKind::Contains]);
491 assert!(p.edges.is_empty());
492 }
493
494 #[test]
495 fn project_keeps_edges_between_kept_nodes() {
496 let g = Graph {
497 nodes: vec![n("a", NodeKind::Crate), n("b", NodeKind::Crate)],
498 edges: vec![e("a", "b", EdgeKind::Uses)],
499 cycles: vec![],
500 stats: None,
501 };
502 let p = g.project(&[NodeKind::Crate], &[EdgeKind::Uses]);
503 assert_eq!(p.edges.len(), 1);
504 }
505
506 #[test]
507 fn visibility_roundtrip_new_format() {
508 let cases: &[(&str, Visibility)] = &[
509 ("\"public\"", Visibility::Public),
510 ("\"private\"", Visibility::Private),
511 ("\"crate\"", Visibility::Crate),
512 ("\"super\"", Visibility::Super),
513 (
514 r#"{"restricted":"some::path"}"#,
515 Visibility::Restricted {
516 path: "some::path".into(),
517 },
518 ),
519 ];
520 for (json, expected) in cases {
521 let got: Visibility = serde_json::from_str(json).unwrap();
522 assert_eq!(got, expected.clone());
523 let re = serde_json::to_string(&got).unwrap();
524 let back: Visibility = serde_json::from_str(&re).unwrap();
525 assert_eq!(back, expected.clone());
526 }
527 }
528
529 #[test]
530 fn visibility_deserialize_old_tagged_format() {
531 let cases: &[(&str, Visibility)] = &[
532 (r#"{"kind":"public"}"#, Visibility::Public),
533 (r#"{"kind":"private"}"#, Visibility::Private),
534 (r#"{"kind":"crate"}"#, Visibility::Crate),
535 (r#"{"kind":"super"}"#, Visibility::Super),
536 (
537 r#"{"kind":"restricted","path":"some::path"}"#,
538 Visibility::Restricted {
539 path: "some::path".into(),
540 },
541 ),
542 ];
543 for (json, expected) in cases {
544 let got: Visibility = serde_json::from_str(json).unwrap();
545 assert_eq!(got, expected.clone());
546 }
547 }
548
549 #[test]
550 fn complexity_coupling_roundtrip() {
551 let cx = Complexity {
552 cyclomatic: 3.0,
553 coupling: Some(Coupling {
554 fan_in: 2,
555 fan_out: 4,
556 hk: 576.0,
557 ..Default::default()
558 }),
559 ..Default::default()
560 };
561 let json = serde_json::to_string(&cx).unwrap();
562 let back: Complexity = serde_json::from_str(&json).unwrap();
563 assert_eq!(back.cyclomatic, 3.0);
564 let c = back.coupling.unwrap();
565 assert_eq!(c.fan_in, 2);
566 assert_eq!(c.fan_out, 4);
567 }
568}