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}