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<'_>) -> io::Result<String> {
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)
109 }
110
111 fn report_check(
112 &self,
113 stats: &DuplicationStats,
114 breaches: &[CheckBreach<'_>],
115 ) -> io::Result<String> {
116 let groups_of = |exact: bool| {
117 breaches
118 .iter()
119 .find(|breach| breach.is_of_exact() == exact)
120 .map_or_else(Vec::new, |breach| self.to_json_groups(breach.groups()))
121 };
122 let document = JsonCheck {
123 stats: Self::to_json_stats(stats),
124 passed: breaches.is_empty(),
125 breaches: breaches
126 .iter()
127 .map(|breach| breach.message().to_owned())
128 .collect(),
129 exact: groups_of(true),
130 near: groups_of(false),
131 };
132 Self::write_document(&document)
133 }
134
135 fn report_stats(&self, stats: &DuplicationStats) -> io::Result<String> {
136 Self::write_document(&Self::to_json_stats(stats))
137 }
138
139 fn report_exact(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
140 self.write_groups(groups)
141 }
142
143 fn report_near(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
144 self.write_groups(groups)
145 }
146
147 fn report_sub_exact(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
148 self.write_groups(groups)
149 }
150
151 fn report_sub_near(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
152 self.write_groups(groups)
153 }
154}
155
156impl JsonReporter {
157 fn write_document<T: serde::Serialize>(document: &T) -> io::Result<String> {
158 let json = to_string_pretty(document).map_err(io::Error::other)?;
159 Ok(format!("{json}\n"))
160 }
161
162 fn write_groups(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
163 Self::write_document(&self.to_json_groups(groups))
164 }
165
166 fn to_json_groups(&self, groups: &[DuplicateGroup]) -> Vec<JsonGroup> {
167 groups.iter().map(|g| self.to_json_group(g)).collect()
168 }
169
170 fn to_json_stats(stats: &DuplicationStats) -> JsonStats {
171 JsonStats {
172 total_code_units: stats.total_code_units,
173 total_lines: stats.total_lines,
174 exact_duplicate_groups: stats.exact_duplicate_groups,
175 exact_duplicate_units: stats.exact_duplicate_units,
176 near_duplicate_groups: stats.near_duplicate_groups,
177 near_duplicate_units: stats.near_duplicate_units,
178 exact_duplicate_lines: stats.exact_duplicate_lines,
179 near_duplicate_lines: stats.near_duplicate_lines,
180 exact_duplicate_percent: stats.exact_duplicate_percent(),
181 near_duplicate_percent: stats.near_duplicate_percent(),
182 sub_exact_groups: stats.sub_exact_groups,
183 sub_exact_units: stats.sub_exact_units,
184 sub_near_groups: stats.sub_near_groups,
185 sub_near_units: stats.sub_near_units,
186 baseline_suppressed: stats.baseline_suppressed,
187 }
188 }
189
190 fn to_json_group(&self, group: &DuplicateGroup) -> JsonGroup {
191 JsonGroup {
192 fingerprint: group.fingerprint.to_hex(),
193 similarity: group.similarity,
194 members: group
195 .members
196 .iter()
197 .map(|m| JsonMember {
198 name: m.name.clone(),
199 kind: m.kind.to_string(),
200 file: display_path(self.base_path.as_deref(), &m.file).into_owned(),
201 line_start: m.line_start,
202 line_end: m.line_end,
203 })
204 .collect(),
205 }
206 }
207}