1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum TokenKind {
7 Keyword,
8 Identifier,
9 Literal,
10 Operator,
11 Punctuation,
12 Comment,
13 BlockComment,
14 Whitespace,
15 Ignore,
16 Other,
17}
18
19impl TokenKind {
20 pub fn discriminant(&self) -> u8 {
22 match self {
23 Self::Keyword => 1,
24 Self::Identifier => 2,
25 Self::Literal => 3,
26 Self::Operator => 4,
27 Self::Punctuation => 5,
28 Self::Comment => 6,
29 Self::BlockComment => 7,
30 Self::Whitespace => 8,
31 Self::Ignore => 9,
32 Self::Other => 10,
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Location {
39 pub line: u32,
40 pub column: u32,
41 pub offset: u32,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct Token {
46 pub kind: TokenKind,
47 pub value: String,
48 pub start: Location,
49 pub end: Location,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct BlameEntry {
54 pub commit_sha: String,
55 pub author: String,
56 pub timestamp: i64,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct Fragment {
61 pub source_id: String,
62 pub start: Location,
63 pub end: Location,
64 pub range: [u32; 2],
65 pub blame: Option<BlameEntry>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct CpdClone {
70 pub format: String,
71 pub fragment_a: Fragment,
72 pub fragment_b: Fragment,
73 pub token_count: u32,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct DetectionToken {
84 pub hash: u64,
86 pub start: Location,
87 pub end: Location,
88 pub range: [usize; 2],
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct SourceFile {
95 pub id: String,
96 pub format: String,
97 pub tokens: Vec<Token>,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct StatRow {
104 pub lines: u64,
105 pub tokens: u64,
106 pub sources: u64,
107 pub clones: u64,
108 pub duplicated_lines: u64,
109 pub duplicated_tokens: u64,
110 pub percentage: f64,
111 pub percentage_tokens: f64,
112 #[serde(default)]
113 pub new_duplicated_lines: u64,
114 #[serde(default)]
115 pub new_clones: u64,
116}
117
118impl Default for StatRow {
119 fn default() -> Self {
120 Self {
121 lines: 0,
122 tokens: 0,
123 sources: 0,
124 clones: 0,
125 duplicated_lines: 0,
126 duplicated_tokens: 0,
127 percentage: 0.0,
128 percentage_tokens: 0.0,
129 new_duplicated_lines: 0,
130 new_clones: 0,
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct Statistics {
139 pub total: StatRow,
140 pub formats: HashMap<String, StatRow>,
141 pub detection_date: String,
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use serde_json;
148
149 #[test]
150 fn statistics_default_total_is_zero() {
151 let stats = Statistics {
152 total: StatRow::default(),
153 formats: HashMap::new(),
154 detection_date: "2026-01-01T00:00:00Z".to_string(),
155 };
156 assert_eq!(stats.total.clones, 0);
157 }
158
159 #[test]
160 fn token_serializes_and_deserializes() {
161 let token = Token {
162 kind: TokenKind::Keyword,
163 value: "function".to_string(),
164 start: Location {
165 line: 1,
166 column: 0,
167 offset: 0,
168 },
169 end: Location {
170 line: 1,
171 column: 8,
172 offset: 8,
173 },
174 };
175 let json = serde_json::to_string(&token).unwrap();
176 let back: Token = serde_json::from_str(&json).unwrap();
177 assert_eq!(token, back);
178 }
179
180 #[test]
181 fn cpd_clone_serializes_with_blame() {
182 let loc = Location {
183 line: 1,
184 column: 0,
185 offset: 0,
186 };
187 let blame = BlameEntry {
188 commit_sha: "abc123".to_string(),
189 author: "Alice".to_string(),
190 timestamp: 1700000000,
191 };
192 let frag = Fragment {
193 source_id: "a.js".to_string(),
194 start: loc.clone(),
195 end: loc.clone(),
196 range: [0, 10],
197 blame: Some(blame),
198 };
199 let clone = CpdClone {
200 format: "javascript".to_string(),
201 fragment_a: frag.clone(),
202 fragment_b: frag,
203 token_count: 50,
204 };
205 let json = serde_json::to_string(&clone).unwrap();
206 assert!(json.contains("abc123"));
207 assert!(json.contains("fragment_a"));
208 }
209
210 #[test]
211 fn fragment_blame_none_serializes_as_null() {
212 let loc = Location {
213 line: 1,
214 column: 0,
215 offset: 0,
216 };
217 let frag = Fragment {
218 source_id: "b.js".to_string(),
219 start: loc.clone(),
220 end: loc.clone(),
221 range: [0, 5],
222 blame: None,
223 };
224 let json = serde_json::to_string(&frag).unwrap();
225 assert!(json.contains("\"blame\":null"));
226 }
227}