1use crate::utils::fs::path_to_string;
2use pbjson_types::Timestamp;
3use qlty_config::issue_transformer::IssueTransformer;
4use qlty_types::analysis::v1::{
5 AnalysisResult, ComponentType, Invocation, Issue, Message, Metadata, Stats,
6};
7use rayon::prelude::*;
8use serde::Serialize;
9use std::{
10 collections::HashMap,
11 path::{Path, PathBuf},
12};
13use time::OffsetDateTime;
14use tracing::debug;
15
16#[derive(Clone, Debug, Serialize, Default)]
17pub struct Report {
18 pub metadata: Metadata,
19 pub messages: Vec<Message>,
20 pub invocations: Vec<Invocation>,
21 pub issues: Vec<Issue>,
22 pub stats: Vec<Stats>,
23}
24
25impl Report {
26 pub fn merge(&mut self, other: &Report) {
27 self.messages.extend(other.messages.clone());
28 self.invocations.extend(other.invocations.clone());
29 self.issues.extend(other.issues.clone());
30 self.stats.extend(other.stats.clone());
31
32 if other.metadata.result == AnalysisResult::Error as i32 {
33 self.metadata.result = AnalysisResult::Error.into();
34 }
35 }
36
37 pub fn transform_issues(&mut self, transformer: Box<dyn IssueTransformer>) {
38 let mut transformed_issues = vec![];
39
40 for issue in self.issues.iter() {
41 if let Some(issue) = transformer.transform(issue.clone()) {
42 transformed_issues.push(issue);
43 } else {
44 debug!("Skipping issue due to transformer: {:?}", issue);
45 }
46 }
47
48 self.issues = transformed_issues;
49 }
50
51 pub fn relativeize_paths(&mut self, base_path: &Path) {
53 let prefix = base_path.to_path_buf();
54
55 self.issues.iter_mut().for_each(|issue| {
56 if let Some(location) = &mut issue.location() {
57 location.path = location.relative_path(&prefix);
58 issue.location = Some(location.to_owned());
59 }
60
61 issue.other_locations.iter_mut().for_each(|other_location| {
62 other_location.path = other_location.relative_path(&prefix);
63 });
64
65 issue.suggestions.par_iter_mut().for_each(|suggestion| {
66 suggestion.replacements.iter_mut().for_each(|replacement| {
67 let location = replacement.location.as_mut().unwrap();
68 location.path = location.relative_path(&prefix);
69 replacement.location = Some(location.clone());
70 });
71 });
72 });
73
74 self.stats.par_iter_mut().for_each(|stats| {
75 stats.path = stats
76 .path
77 .strip_prefix(&path_to_string(&prefix))
78 .unwrap_or(&stats.path)
79 .to_owned();
80
81 stats.fully_qualified_name = stats
82 .fully_qualified_name
83 .strip_prefix(&path_to_string(&prefix))
84 .unwrap_or(&stats.fully_qualified_name)
85 .to_owned();
86 });
87 }
88
89 pub fn attach_metadata(&mut self) {
90 self.invocations.par_iter_mut().for_each(|invocation| {
91 invocation.workspace_id = self.metadata.workspace_id.clone();
92 invocation.project_id = self.metadata.project_id.clone();
93 invocation.reference = self.metadata.reference.clone();
94 invocation.build_id = self.metadata.build_id.clone();
95 invocation.build_timestamp = self.metadata.start_time.clone();
96 invocation.commit_sha = self.metadata.revision_oid.clone();
97 });
98
99 self.messages.par_iter_mut().for_each(|message| {
100 message.workspace_id = self.metadata.workspace_id.clone();
101 message.project_id = self.metadata.project_id.clone();
102 message.reference = self.metadata.reference.clone();
103 message.build_id = self.metadata.build_id.clone();
104 message.build_timestamp = self.metadata.start_time.clone();
105 message.commit_sha = self.metadata.revision_oid.clone();
106 });
107
108 self.issues.par_iter_mut().for_each(|issue| {
109 issue.workspace_id = self.metadata.workspace_id.clone();
110 issue.project_id = self.metadata.project_id.clone();
111 issue.analyzed_at = Some(self.metadata.start_time.clone().unwrap());
112 issue.pull_request_number = self.metadata.pull_request_number.clone();
113 issue.tracked_branch_id = self.metadata.tracked_branch_id.clone();
114
115 issue.reference = self.metadata.reference.clone();
116 issue.build_id = self.metadata.build_id.clone();
117 issue.commit_sha = self.metadata.revision_oid.clone();
118 });
119
120 self.stats.par_iter_mut().for_each(|stats| {
121 stats.workspace_id = self.metadata.workspace_id.clone();
122 stats.project_id = self.metadata.project_id.clone();
123 stats.analyzed_at = Some(self.metadata.start_time.clone().unwrap());
124 stats.pull_request_number = self.metadata.pull_request_number.clone();
125 stats.tracked_branch_id = self.metadata.tracked_branch_id.clone();
126
127 stats.reference = self.metadata.reference.clone();
128 stats.build_id = self.metadata.build_id.clone();
129 stats.commit_sha = self.metadata.revision_oid.clone();
130 });
131 }
132
133 pub fn duplication_issues_by_duplication(&self) -> HashMap<String, Vec<Issue>> {
134 self.issues
135 .iter()
136 .filter(|issue| issue.tool == "qlty" && issue.driver == "duplication")
137 .fold(HashMap::new(), |mut acc, issue| {
138 let structural_hash = issue.get_property_string("structural_hash");
139 let issues = acc.entry(structural_hash).or_insert(vec![]);
140 issues.push(issue.clone());
141 acc
142 })
143 }
144
145 pub fn function_stats_by_path(&self) -> HashMap<PathBuf, Vec<Stats>> {
146 let function_stats = self
147 .stats
148 .par_iter()
149 .filter(|stats| stats.kind.try_into() == Ok(ComponentType::Function))
150 .cloned()
151 .collect::<Vec<_>>();
152
153 let mut results = HashMap::new();
154
155 for stat in function_stats {
156 let path = PathBuf::from(&stat.path);
157 let stats = results.entry(path).or_insert(vec![]);
158 stats.push(stat);
159 }
160
161 results
162 }
163
164 pub fn file_stats(&self) -> Vec<Stats> {
165 self.stats
166 .par_iter()
167 .filter(|stats| stats.kind.try_into() == Ok(ComponentType::File))
168 .cloned()
169 .collect()
170 }
171
172 pub fn directory_stats(&self) -> Vec<Stats> {
173 self.stats
174 .par_iter()
175 .filter(|stats| stats.kind.try_into() == Ok(ComponentType::Directory))
176 .cloned()
177 .collect()
178 }
179
180 pub fn finish(&mut self) {
181 self.metadata.finish_time = Some(self.now_timestamp());
182 }
183
184 fn now_timestamp(&self) -> Timestamp {
185 let now = OffsetDateTime::now_utc();
186 Timestamp {
187 seconds: now.unix_timestamp(),
188 nanos: now.nanosecond() as i32,
189 }
190 }
191}