1use std::io;
7
8use crate::grouper::{DuplicateGroup, DuplicationStats};
9use crate::output::check_breach::CheckBreach;
10use crate::output::report::Report;
11use crate::output::reporter::Reporter;
12use crate::output::reporter::display_path;
13use serde_json::to_string_pretty;
14use std::path::PathBuf;
15
16pub struct JsonReporter {
17 pub base_path: Option<PathBuf>,
18}
19
20impl JsonReporter {
21 #[must_use]
22 pub const fn new(base_path: Option<PathBuf>) -> Self {
23 Self { base_path }
24 }
25}
26
27#[derive(serde::Serialize)]
28struct JsonStats {
29 total_code_units: usize,
30 total_lines: usize,
31 exact_duplicate_groups: usize,
32 exact_duplicate_units: usize,
33 near_duplicate_groups: usize,
34 near_duplicate_units: usize,
35 exact_duplicate_lines: usize,
36 near_duplicate_lines: usize,
37 exact_duplicate_percent: f64,
38 near_duplicate_percent: f64,
39 #[serde(skip_serializing_if = "is_zero")]
40 sub_exact_groups: usize,
41 #[serde(skip_serializing_if = "is_zero")]
42 sub_exact_units: usize,
43 #[serde(skip_serializing_if = "is_zero")]
44 sub_near_groups: usize,
45 #[serde(skip_serializing_if = "is_zero")]
46 sub_near_units: usize,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 baseline_suppressed: Option<usize>,
49}
50
51#[allow(clippy::trivially_copy_pass_by_ref)] const fn is_zero(v: &usize) -> bool {
53 *v == 0
54}
55
56#[derive(serde::Serialize)]
59struct JsonReport {
60 stats: JsonStats,
61 exact: Vec<JsonGroup>,
62 near: Vec<JsonGroup>,
63 #[serde(skip_serializing_if = "Vec::is_empty")]
64 sub_exact: Vec<JsonGroup>,
65 #[serde(skip_serializing_if = "Vec::is_empty")]
66 sub_near: Vec<JsonGroup>,
67}
68
69#[derive(serde::Serialize)]
73struct JsonCheck {
74 stats: JsonStats,
75 passed: bool,
76 breaches: Vec<String>,
77 #[serde(skip_serializing_if = "Vec::is_empty")]
78 exact: Vec<JsonGroup>,
79 #[serde(skip_serializing_if = "Vec::is_empty")]
80 near: Vec<JsonGroup>,
81}
82
83#[derive(serde::Serialize)]
84struct JsonGroup {
85 fingerprint: String,
86 similarity: f64,
87 members: Vec<JsonMember>,
88}
89
90#[derive(serde::Serialize)]
91struct JsonMember {
92 name: String,
93 kind: String,
94 file: String,
95 line_start: usize,
96 line_end: usize,
97}
98
99impl Reporter for JsonReporter {
100 fn report(&self, report: &Report<'_>, writer: &mut dyn io::Write) -> io::Result<()> {
101 let document = JsonReport {
102 stats: Self::to_json_stats(report.stats),
103 exact: self.to_json_groups(report.exact),
104 near: self.to_json_groups(report.near),
105 sub_exact: self.to_json_groups(report.sub_exact),
106 sub_near: self.to_json_groups(report.sub_near),
107 };
108 Self::write_document(&document, writer)
109 }
110
111 fn report_check(
112 &self,
113 stats: &DuplicationStats,
114 breaches: &[CheckBreach<'_>],
115 writer: &mut dyn io::Write,
116 ) -> io::Result<()> {
117 let groups_of = |exact: bool| {
118 breaches
119 .iter()
120 .find(|breach| breach.is_of_exact() == exact)
121 .map_or_else(Vec::new, |breach| self.to_json_groups(breach.groups()))
122 };
123 let document = JsonCheck {
124 stats: Self::to_json_stats(stats),
125 passed: breaches.is_empty(),
126 breaches: breaches
127 .iter()
128 .map(|breach| breach.message().to_owned())
129 .collect(),
130 exact: groups_of(true),
131 near: groups_of(false),
132 };
133 Self::write_document(&document, writer)
134 }
135
136 fn report_stats(&self, stats: &DuplicationStats, writer: &mut dyn io::Write) -> io::Result<()> {
137 Self::write_document(&Self::to_json_stats(stats), writer)
138 }
139
140 fn report_exact(
141 &self,
142 groups: &[DuplicateGroup],
143 writer: &mut dyn io::Write,
144 ) -> io::Result<()> {
145 self.write_groups(groups, writer)
146 }
147
148 fn report_near(&self, groups: &[DuplicateGroup], writer: &mut dyn io::Write) -> io::Result<()> {
149 self.write_groups(groups, writer)
150 }
151
152 fn report_sub_exact(
153 &self,
154 groups: &[DuplicateGroup],
155 writer: &mut dyn io::Write,
156 ) -> io::Result<()> {
157 self.write_groups(groups, writer)
158 }
159
160 fn report_sub_near(
161 &self,
162 groups: &[DuplicateGroup],
163 writer: &mut dyn io::Write,
164 ) -> io::Result<()> {
165 self.write_groups(groups, writer)
166 }
167}
168
169impl JsonReporter {
170 fn write_document<T: serde::Serialize>(
171 document: &T,
172 writer: &mut dyn io::Write,
173 ) -> io::Result<()> {
174 let json = to_string_pretty(document).map_err(io::Error::other)?;
175 writeln!(writer, "{json}")
176 }
177
178 fn write_groups(
179 &self,
180 groups: &[DuplicateGroup],
181 writer: &mut dyn io::Write,
182 ) -> io::Result<()> {
183 Self::write_document(&self.to_json_groups(groups), writer)
184 }
185
186 fn to_json_groups(&self, groups: &[DuplicateGroup]) -> Vec<JsonGroup> {
187 groups.iter().map(|g| self.to_json_group(g)).collect()
188 }
189
190 fn to_json_stats(stats: &DuplicationStats) -> JsonStats {
191 JsonStats {
192 total_code_units: stats.total_code_units,
193 total_lines: stats.total_lines,
194 exact_duplicate_groups: stats.exact_duplicate_groups,
195 exact_duplicate_units: stats.exact_duplicate_units,
196 near_duplicate_groups: stats.near_duplicate_groups,
197 near_duplicate_units: stats.near_duplicate_units,
198 exact_duplicate_lines: stats.exact_duplicate_lines,
199 near_duplicate_lines: stats.near_duplicate_lines,
200 exact_duplicate_percent: stats.exact_duplicate_percent(),
201 near_duplicate_percent: stats.near_duplicate_percent(),
202 sub_exact_groups: stats.sub_exact_groups,
203 sub_exact_units: stats.sub_exact_units,
204 sub_near_groups: stats.sub_near_groups,
205 sub_near_units: stats.sub_near_units,
206 baseline_suppressed: stats.baseline_suppressed,
207 }
208 }
209
210 fn to_json_group(&self, group: &DuplicateGroup) -> JsonGroup {
211 JsonGroup {
212 fingerprint: group.fingerprint.to_hex(),
213 similarity: group.similarity,
214 members: group
215 .members
216 .iter()
217 .map(|m| JsonMember {
218 name: m.name.clone(),
219 kind: m.kind.to_string(),
220 file: display_path(self.base_path.as_deref(), &m.file).into_owned(),
221 line_start: m.line_start,
222 line_end: m.line_end,
223 })
224 .collect(),
225 }
226 }
227}