Skip to main content

codehelion_core/
clone_class.rs

1//! The clone classification vocabulary, shared by every analysis mode.
2//!
3//! Classification is a property of a finding, not of the mode that produced
4//! it: a verbatim copy is a Type-1 clone whether the Fast engine matched it
5//! token-by-token or the Structural verifier scored it across dimensions. One
6//! enum for all modes keeps reports, storage and lineage comparable across
7//! modes; the names here are the identifiers the store and the JSON reports
8//! use.
9//!
10//! Not every mode produces every class: the Fast engine reports no gapped
11//! (Type-3) clones, since it only matches identical normalized content.
12
13/// How closely a clone group's members match.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum CloneClass {
16    /// Verbatim copy (formatting and comments aside).
17    Type1,
18    /// Copy with consistent renames and/or changed literals.
19    Type2,
20    /// Similar but not identical: a gapped clone, with inserted, deleted or
21    /// modified statements.
22    Type3,
23    /// A correspondence justified only by one or more registered semantic
24    /// rules, never by a general equivalence claim.
25    RestrictedSemantic,
26}
27
28impl CloneClass {
29    /// Stable lowercase identifier used in reports and storage.
30    #[must_use]
31    pub const fn name(self) -> &'static str {
32        match self {
33            Self::Type1 => "type-1",
34            Self::Type2 => "type-2",
35            Self::Type3 => "type-3",
36            Self::RestrictedSemantic => "restricted-semantic",
37        }
38    }
39
40    /// Read back a classification written by [`Self::name`].
41    ///
42    /// `None` for anything else, including a name a newer release writes:
43    /// guessing which class an unknown one resembles would put a finding in a
44    /// category the tool that recorded it did not choose.
45    #[must_use]
46    pub fn from_name(name: &str) -> Option<Self> {
47        match name {
48            "type-1" => Some(Self::Type1),
49            "type-2" => Some(Self::Type2),
50            "type-3" => Some(Self::Type3),
51            "restricted-semantic" => Some(Self::RestrictedSemantic),
52            _ => None,
53        }
54    }
55
56    /// Whether the class asserts equality rather than resemblance.
57    ///
58    /// Type-1 and Type-2 both mean the copies agree statement for statement,
59    /// verbatim or up to renaming. Type-3 and restricted semantic findings
60    /// are explainable correspondences, not claims of textual equality.
61    #[must_use]
62    pub const fn is_exact(self) -> bool {
63        matches!(self, Self::Type1 | Self::Type2)
64    }
65}
66
67/// What the members of a clone group are.
68///
69/// Orthogonal to [`CloneClass`]: a run of statements duplicated verbatim is a
70/// Type-1 clone exactly as a duplicated function is, and the two say different
71/// things about the code. A reader has to be able to tell "these functions are
72/// copies" from "these functions share a copied stretch", so the distinction
73/// is recorded rather than inferred from how the line ranges compare.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
75pub enum CloneScope {
76    /// Each member is a whole unit: a function, method, impl block or record.
77    Unit,
78    /// Each member is a run of statements inside a unit. The enclosing units
79    /// need not be clones of each other, and usually are not.
80    Fragment,
81}
82
83impl CloneScope {
84    /// Stable lowercase identifier used in reports and storage.
85    #[must_use]
86    pub const fn name(self) -> &'static str {
87        match self {
88            Self::Unit => "unit",
89            Self::Fragment => "fragment",
90        }
91    }
92
93    /// Read back a scope written by [`Self::name`], or `None` for anything
94    /// else.
95    #[must_use]
96    pub fn from_name(name: &str) -> Option<Self> {
97        match name {
98            "unit" => Some(Self::Unit),
99            "fragment" => Some(Self::Fragment),
100            _ => None,
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::{CloneClass, CloneScope};
108
109    #[test]
110    fn names_are_the_stable_report_identifiers() {
111        assert_eq!(CloneClass::Type1.name(), "type-1");
112        assert_eq!(CloneClass::Type2.name(), "type-2");
113        assert_eq!(CloneClass::Type3.name(), "type-3");
114        assert_eq!(CloneClass::RestrictedSemantic.name(), "restricted-semantic");
115        assert_eq!(CloneScope::Unit.name(), "unit");
116        assert_eq!(CloneScope::Fragment.name(), "fragment");
117    }
118
119    #[test]
120    fn a_recorded_name_reads_back_as_what_wrote_it() {
121        for class in [
122            CloneClass::Type1,
123            CloneClass::Type2,
124            CloneClass::Type3,
125            CloneClass::RestrictedSemantic,
126        ] {
127            assert_eq!(CloneClass::from_name(class.name()), Some(class));
128        }
129        for scope in [CloneScope::Unit, CloneScope::Fragment] {
130            assert_eq!(CloneScope::from_name(scope.name()), Some(scope));
131        }
132        // A name this release does not know stays unknown rather than being
133        // rounded to the nearest one it does.
134        assert_eq!(CloneClass::from_name("type-4"), None);
135        assert_eq!(CloneScope::from_name("statement"), None);
136    }
137
138    #[test]
139    fn ordering_runs_from_exact_to_gapped() {
140        let mut classes = [
141            CloneClass::RestrictedSemantic,
142            CloneClass::Type3,
143            CloneClass::Type1,
144            CloneClass::Type2,
145        ];
146        classes.sort_unstable();
147        assert_eq!(
148            classes,
149            [
150                CloneClass::Type1,
151                CloneClass::Type2,
152                CloneClass::Type3,
153                CloneClass::RestrictedSemantic,
154            ]
155        );
156    }
157}