archunit/common/extraction/
edge.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use std::fmt;

use super::identifier::normalize_identifier;
use super::{ImportKind, ImportKindSet};

/// One directed dependency in an extracted Rust project.
///
/// Internal endpoints are normalized workspace-relative file identifiers. When [`Self::external`]
/// is true, `target` is instead the Cargo-visible crate name used in source.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Edge {
    /// The internal file containing the dependency syntax.
    pub source: String,
    /// The internal target file or external Cargo-visible crate name.
    pub target: String,
    /// Whether the target is outside the analyzed workspace.
    pub external: bool,
    /// Every Rust syntax form that produced this source-target pair.
    pub import_kinds: ImportKindSet,
}

impl Edge {
    /// Builds an edge and lexically normalizes both identifiers.
    ///
    /// An edge whose normalized endpoints are equal is canonicalized to [`Self::self_edge`].
    #[must_use]
    pub fn new(
        source: impl AsRef<str>,
        target: impl AsRef<str>,
        external: bool,
        import_kinds: impl IntoIterator<Item = ImportKind>,
    ) -> Self {
        let source = normalize_identifier(source.as_ref());
        let target = normalize_identifier(target.as_ref());

        if source == target {
            return Self::self_edge(source);
        }

        Self {
            source,
            target,
            external,
            import_kinds: import_kinds.into_iter().collect(),
        }
    }

    /// Builds the marker edge that keeps a dependency-free file in the graph.
    #[must_use]
    pub fn self_edge(identifier: impl AsRef<str>) -> Self {
        let identifier = normalize_identifier(identifier.as_ref());
        Self {
            source: identifier.clone(),
            target: identifier,
            external: false,
            import_kinds: ImportKindSet::new(),
        }
    }

    /// Returns whether this edge is the marker from a file to itself.
    #[must_use]
    pub fn is_self_edge(&self) -> bool {
        self.source == self.target
    }
}

impl fmt::Display for Edge {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.is_self_edge() {
            return write!(formatter, "{} -> itself", self.source);
        }

        write!(formatter, "{} -> {}", self.source, self.target)?;
        if self.external {
            formatter.write_str(" (external)")?;
        }
        write!(formatter, " {}", self.import_kinds)
    }
}

#[cfg(test)]
mod tests {
    use super::Edge;
    use crate::common::ImportKind;

    #[test]
    fn normalizes_internal_file_identifiers() {
        let edge = Edge::new(
            r".\crates\api\src\handler.rs",
            "crates/api/src/../db/repository.rs",
            false,
            [ImportKind::Use],
        );

        assert_eq!(edge.source, "crates/api/src/handler.rs");
        assert_eq!(edge.target, "crates/api/db/repository.rs");
        assert!(!edge.external);
        assert!(edge.import_kinds.contains(ImportKind::Use));
    }

    #[test]
    fn keeps_external_crate_names() {
        let edge = Edge::new(
            "crates/api/src/lib.rs",
            "serde_json",
            true,
            [ImportKind::PathReference, ImportKind::Use],
        );

        assert_eq!(edge.target, "serde_json");
        assert!(edge.external);
        assert_eq!(edge.import_kinds.len(), 2);
    }

    #[test]
    fn creates_one_canonical_self_edge_shape() {
        let edge = Edge::new(
            "crates/api/src/lib.rs",
            r"crates\api\src\.\lib.rs",
            true,
            [ImportKind::Use],
        );

        assert!(edge.is_self_edge());
        assert!(!edge.external);
        assert!(edge.import_kinds.is_empty());
        assert_eq!(edge, Edge::self_edge("crates/api/src/lib.rs"));
    }

    #[test]
    fn renders_diagnostic_text_deterministically() {
        let edge = Edge::new(
            "src/api.rs",
            "src/db.rs",
            false,
            [ImportKind::PubUse, ImportKind::Use],
        );
        let external = Edge::new("src/api.rs", "tokio", true, [ImportKind::MacroReference]);

        assert_eq!(edge.to_string(), "src/api.rs -> src/db.rs [use, pub_use]");
        assert_eq!(
            external.to_string(),
            "src/api.rs -> tokio (external) [macro_reference]"
        );
        assert_eq!(
            Edge::self_edge("src/api.rs").to_string(),
            "src/api.rs -> itself"
        );
    }
}