Skip to main content

bylaw_core/
graph.rs

1use crate::AnalysisDiagnostic;
2use camino::Utf8PathBuf;
3use indexmap::{IndexMap, IndexSet};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::hash::Hash;
7use thiserror::Error;
8
9macro_rules! string_id {
10    ($name:ident) => {
11        #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
12        #[serde(transparent)]
13        pub struct $name(String);
14
15        impl $name {
16            pub fn new(value: impl Into<String>) -> Self {
17                Self(value.into())
18            }
19
20            pub fn as_str(&self) -> &str {
21                &self.0
22            }
23        }
24
25        impl fmt::Display for $name {
26            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27                formatter.write_str(&self.0)
28            }
29        }
30
31        impl From<String> for $name {
32            fn from(value: String) -> Self {
33                Self(value)
34            }
35        }
36
37        impl From<&str> for $name {
38            fn from(value: &str) -> Self {
39                Self(value.to_owned())
40            }
41        }
42    };
43}
44
45string_id!(PackageId);
46string_id!(CrateId);
47string_id!(ModuleId);
48string_id!(ExternalCrateId);
49
50#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
51#[serde(tag = "kind", content = "id", rename_all = "kebab-case")]
52pub enum ComponentId {
53    Crate(CrateId),
54    Module(ModuleId),
55    ExternalCrate(ExternalCrateId),
56}
57
58impl fmt::Display for ComponentId {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Crate(id) => id.fmt(formatter),
62            Self::Module(id) => id.fmt(formatter),
63            Self::ExternalCrate(id) => id.fmt(formatter),
64        }
65    }
66}
67
68#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
69#[serde(rename_all = "kebab-case")]
70pub enum ComponentKind {
71    Crate,
72    Module,
73    ExternalCrate,
74}
75
76#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
77#[serde(rename_all = "kebab-case")]
78pub enum TargetKind {
79    Library,
80    Binary,
81    Test,
82    Example,
83    Bench,
84    BuildScript,
85    ProcMacro,
86    Other(String),
87}
88
89#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
90pub struct Package {
91    pub id: PackageId,
92    pub name: String,
93    pub version: Option<String>,
94    pub manifest_path: Utf8PathBuf,
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
98pub struct CrateNode {
99    pub id: CrateId,
100    pub package_id: PackageId,
101    pub package_name: String,
102    pub crate_name: String,
103    pub target_name: String,
104    pub target_kind: TargetKind,
105    pub source_root: Utf8PathBuf,
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
109pub struct ModuleNode {
110    pub id: ModuleId,
111    pub crate_id: CrateId,
112    pub package_id: PackageId,
113    pub package_name: String,
114    pub crate_name: String,
115    pub path: String,
116    pub parent: Option<ModuleId>,
117    pub source_file: Utf8PathBuf,
118}
119
120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
121pub struct ExternalCrateNode {
122    pub id: ExternalCrateId,
123    pub package_name: String,
124    pub crate_name: String,
125    pub version: Option<String>,
126    pub source: Option<String>,
127    pub toolchain: bool,
128}
129
130#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
131#[serde(tag = "kind", rename_all = "kebab-case")]
132pub enum Component {
133    Crate(CrateNode),
134    Module(ModuleNode),
135    ExternalCrate(ExternalCrateNode),
136}
137
138impl Component {
139    pub fn id(&self) -> ComponentId {
140        match self {
141            Self::Crate(node) => ComponentId::Crate(node.id.clone()),
142            Self::Module(node) => ComponentId::Module(node.id.clone()),
143            Self::ExternalCrate(node) => ComponentId::ExternalCrate(node.id.clone()),
144        }
145    }
146
147    pub fn kind(&self) -> ComponentKind {
148        match self {
149            Self::Crate(_) => ComponentKind::Crate,
150            Self::Module(_) => ComponentKind::Module,
151            Self::ExternalCrate(_) => ComponentKind::ExternalCrate,
152        }
153    }
154
155    pub fn canonical_name(&self) -> &str {
156        match self {
157            Self::Crate(node) => node.id.as_str(),
158            Self::Module(node) => &node.path,
159            Self::ExternalCrate(node) => node.id.as_str(),
160        }
161    }
162
163    pub fn package_name(&self) -> &str {
164        match self {
165            Self::Crate(node) => &node.package_name,
166            Self::Module(node) => &node.package_name,
167            Self::ExternalCrate(node) => &node.package_name,
168        }
169    }
170
171    pub fn crate_name(&self) -> &str {
172        match self {
173            Self::Crate(node) => &node.crate_name,
174            Self::Module(node) => &node.crate_name,
175            Self::ExternalCrate(node) => &node.crate_name,
176        }
177    }
178
179    pub fn target_kind(&self) -> Option<&TargetKind> {
180        match self {
181            Self::Crate(node) => Some(&node.target_kind),
182            Self::Module(_) | Self::ExternalCrate(_) => None,
183        }
184    }
185
186    pub fn containing_crate(&self) -> Option<CrateId> {
187        match self {
188            Self::Crate(node) => Some(node.id.clone()),
189            Self::Module(node) => Some(node.crate_id.clone()),
190            Self::ExternalCrate(_) => None,
191        }
192    }
193
194    pub fn source_file(&self) -> Option<&Utf8PathBuf> {
195        match self {
196            Self::Crate(node) => Some(&node.source_root),
197            Self::Module(node) => Some(&node.source_file),
198            Self::ExternalCrate(_) => None,
199        }
200    }
201
202    pub fn is_toolchain_crate(&self) -> bool {
203        matches!(self, Self::ExternalCrate(node) if node.toolchain)
204    }
205}
206
207#[derive(
208    Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
209)]
210pub struct SourcePosition {
211    pub line: u32,
212    pub column: u32,
213}
214
215#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
216pub struct SourceSpan {
217    pub path: Utf8PathBuf,
218    pub start: SourcePosition,
219    pub end: SourcePosition,
220}
221
222#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
223#[serde(rename_all = "kebab-case")]
224pub enum DependencyScope {
225    Actual,
226    Declared,
227}
228
229#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
230#[serde(rename_all = "kebab-case")]
231pub enum DependencyKind {
232    Use,
233    Path,
234    Type,
235    Call,
236    Macro,
237    ReExport,
238    CargoNormal,
239    CargoBuild,
240    CargoDev,
241    Unknown,
242}
243
244#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
245pub struct DependencyEvidence {
246    pub kind: DependencyKind,
247    pub span: Option<SourceSpan>,
248    pub description: Option<String>,
249}
250
251impl DependencyEvidence {
252    pub fn new(kind: DependencyKind) -> Self {
253        Self {
254            kind,
255            span: None,
256            description: None,
257        }
258    }
259
260    pub fn with_span(mut self, span: SourceSpan) -> Self {
261        self.span = Some(span);
262        self
263    }
264
265    pub fn with_description(mut self, description: impl Into<String>) -> Self {
266        self.description = Some(description.into());
267        self
268    }
269}
270
271#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
272pub struct DependencyEdge {
273    pub origin: ComponentId,
274    pub target: ComponentId,
275    pub scope: DependencyScope,
276    pub evidence: Vec<DependencyEvidence>,
277}
278
279#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
280pub struct AnalysisContext {
281    pub manifest_path: Option<Utf8PathBuf>,
282    pub target_triple: Option<String>,
283    pub features: Vec<String>,
284    pub target_kinds: IndexSet<TargetKind>,
285}
286
287#[derive(Clone, Debug, Default, Deserialize, Serialize)]
288pub struct ArchitectureGraph {
289    context: AnalysisContext,
290    packages: IndexMap<PackageId, Package>,
291    components: IndexMap<ComponentId, Component>,
292    edges: Vec<DependencyEdge>,
293    diagnostics: Vec<AnalysisDiagnostic>,
294}
295
296impl ArchitectureGraph {
297    pub fn context(&self) -> &AnalysisContext {
298        &self.context
299    }
300
301    pub fn packages(&self) -> impl Iterator<Item = &Package> {
302        self.packages.values()
303    }
304
305    pub fn components(&self) -> impl Iterator<Item = &Component> {
306        self.components.values()
307    }
308
309    pub fn edges(&self) -> impl Iterator<Item = &DependencyEdge> {
310        self.edges.iter()
311    }
312
313    pub fn diagnostics(&self) -> &[AnalysisDiagnostic] {
314        &self.diagnostics
315    }
316
317    pub fn package(&self, id: &PackageId) -> Option<&Package> {
318        self.packages.get(id)
319    }
320
321    pub fn component(&self, id: &ComponentId) -> Option<&Component> {
322        self.components.get(id)
323    }
324
325    pub fn outgoing(&self, id: &ComponentId) -> impl Iterator<Item = &DependencyEdge> {
326        self.edges.iter().filter(move |edge| &edge.origin == id)
327    }
328
329    pub fn incoming(&self, id: &ComponentId) -> impl Iterator<Item = &DependencyEdge> {
330        self.edges.iter().filter(move |edge| &edge.target == id)
331    }
332}
333
334#[derive(Debug, Error)]
335pub enum GraphBuildError {
336    #[error("package `{0}` is already present")]
337    DuplicatePackage(PackageId),
338    #[error("component `{0}` is already present")]
339    DuplicateComponent(ComponentId),
340    #[error("component `{component}` references unknown package `{package}`")]
341    UnknownPackage {
342        component: ComponentId,
343        package: PackageId,
344    },
345    #[error("module `{module}` references unknown crate `{crate_id}`")]
346    UnknownCrate { module: ModuleId, crate_id: CrateId },
347    #[error("dependency references unknown {role} component `{component}`")]
348    UnknownDependencyComponent {
349        role: &'static str,
350        component: ComponentId,
351    },
352}
353
354#[derive(Clone, Debug, Eq, Hash, PartialEq)]
355struct EdgeKey {
356    origin: ComponentId,
357    target: ComponentId,
358    scope: DependencyScope,
359}
360
361#[derive(Clone, Debug, Default)]
362pub struct GraphBuilder {
363    context: AnalysisContext,
364    packages: IndexMap<PackageId, Package>,
365    components: IndexMap<ComponentId, Component>,
366    edges: IndexMap<EdgeKey, DependencyEdge>,
367    diagnostics: Vec<AnalysisDiagnostic>,
368}
369
370impl GraphBuilder {
371    pub fn new(context: AnalysisContext) -> Self {
372        Self {
373            context,
374            ..Self::default()
375        }
376    }
377
378    pub fn add_package(&mut self, package: Package) -> Result<(), GraphBuildError> {
379        if self.packages.contains_key(&package.id) {
380            return Err(GraphBuildError::DuplicatePackage(package.id));
381        }
382        self.packages.insert(package.id.clone(), package);
383        Ok(())
384    }
385
386    pub fn add_component(&mut self, component: Component) -> Result<(), GraphBuildError> {
387        let id = component.id();
388        if self.components.contains_key(&id) {
389            return Err(GraphBuildError::DuplicateComponent(id));
390        }
391
392        match &component {
393            Component::Crate(node) => {
394                if !self.packages.contains_key(&node.package_id) {
395                    return Err(GraphBuildError::UnknownPackage {
396                        component: id,
397                        package: node.package_id.clone(),
398                    });
399                }
400            }
401            Component::Module(node) => {
402                if !self.packages.contains_key(&node.package_id) {
403                    return Err(GraphBuildError::UnknownPackage {
404                        component: id,
405                        package: node.package_id.clone(),
406                    });
407                }
408                let crate_id = ComponentId::Crate(node.crate_id.clone());
409                if !self.components.contains_key(&crate_id) {
410                    return Err(GraphBuildError::UnknownCrate {
411                        module: node.id.clone(),
412                        crate_id: node.crate_id.clone(),
413                    });
414                }
415            }
416            Component::ExternalCrate(_) => {}
417        }
418
419        self.components.insert(id, component);
420        Ok(())
421    }
422
423    pub fn add_dependency(
424        &mut self,
425        origin: ComponentId,
426        target: ComponentId,
427        scope: DependencyScope,
428        evidence: DependencyEvidence,
429    ) -> Result<(), GraphBuildError> {
430        if !self.components.contains_key(&origin) {
431            return Err(GraphBuildError::UnknownDependencyComponent {
432                role: "origin",
433                component: origin,
434            });
435        }
436        if !self.components.contains_key(&target) {
437            return Err(GraphBuildError::UnknownDependencyComponent {
438                role: "target",
439                component: target,
440            });
441        }
442
443        let key = EdgeKey {
444            origin: origin.clone(),
445            target: target.clone(),
446            scope,
447        };
448        let edge = self.edges.entry(key).or_insert_with(|| DependencyEdge {
449            origin,
450            target,
451            scope,
452            evidence: Vec::new(),
453        });
454        if !edge.evidence.contains(&evidence) {
455            edge.evidence.push(evidence);
456        }
457        Ok(())
458    }
459
460    pub fn add_diagnostic(&mut self, diagnostic: AnalysisDiagnostic) {
461        self.diagnostics.push(diagnostic);
462    }
463
464    pub fn finish(self) -> ArchitectureGraph {
465        let mut packages = self.packages;
466        packages.sort_keys();
467        let mut components = self.components;
468        components.sort_keys();
469        let mut edges = self.edges.into_values().collect::<Vec<_>>();
470        for edge in &mut edges {
471            edge.evidence.sort_by(|left, right| {
472                (
473                    &left.kind,
474                    left.span.as_ref().map(|span| {
475                        (
476                            span.path.as_str(),
477                            span.start.line,
478                            span.start.column,
479                            span.end.line,
480                            span.end.column,
481                        )
482                    }),
483                    &left.description,
484                )
485                    .cmp(&(
486                        &right.kind,
487                        right.span.as_ref().map(|span| {
488                            (
489                                span.path.as_str(),
490                                span.start.line,
491                                span.start.column,
492                                span.end.line,
493                                span.end.column,
494                            )
495                        }),
496                        &right.description,
497                    ))
498            });
499        }
500        edges.sort_by(|left, right| {
501            (&left.origin, &left.target, left.scope).cmp(&(
502                &right.origin,
503                &right.target,
504                right.scope,
505            ))
506        });
507        ArchitectureGraph {
508            context: self.context,
509            packages,
510            components,
511            edges,
512            diagnostics: self.diagnostics,
513        }
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    fn package() -> Package {
522        Package {
523            id: PackageId::new("shop 0.1.0"),
524            name: "shop".to_owned(),
525            version: Some("0.1.0".to_owned()),
526            manifest_path: Utf8PathBuf::from("Cargo.toml"),
527        }
528    }
529
530    fn crate_node(package_id: PackageId) -> CrateNode {
531        CrateNode {
532            id: CrateId::new("shop#lib"),
533            package_id,
534            package_name: "shop".to_owned(),
535            crate_name: "shop".to_owned(),
536            target_name: "shop".to_owned(),
537            target_kind: TargetKind::Library,
538            source_root: Utf8PathBuf::from("src/lib.rs"),
539        }
540    }
541
542    #[test]
543    fn aggregates_duplicate_edges_and_evidence() {
544        let package = package();
545        let crate_node = crate_node(package.id.clone());
546        let crate_id = ComponentId::Crate(crate_node.id.clone());
547        let external = ExternalCrateNode {
548            id: ExternalCrateId::new("serde@1"),
549            package_name: "serde".to_owned(),
550            crate_name: "serde".to_owned(),
551            version: Some("1".to_owned()),
552            source: None,
553            toolchain: false,
554        };
555        let external_id = ComponentId::ExternalCrate(external.id.clone());
556
557        let mut builder = GraphBuilder::default();
558        builder.add_package(package).unwrap();
559        builder.add_component(Component::Crate(crate_node)).unwrap();
560        builder
561            .add_component(Component::ExternalCrate(external))
562            .unwrap();
563
564        let evidence = DependencyEvidence::new(DependencyKind::CargoNormal);
565        builder
566            .add_dependency(
567                crate_id.clone(),
568                external_id.clone(),
569                DependencyScope::Declared,
570                evidence.clone(),
571            )
572            .unwrap();
573        builder
574            .add_dependency(crate_id, external_id, DependencyScope::Declared, evidence)
575            .unwrap();
576
577        let graph = builder.finish();
578        let edge = graph.edges().next().unwrap();
579        assert_eq!(edge.evidence.len(), 1);
580    }
581}