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}