qlty_analysis/
report.rs

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    // TODO: Extract this into a Transformer pattern
52    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}