qlty_types/
lib.rs

1use pbjson_types::{value::Kind, Struct, Value};
2use std::cmp::Ordering;
3use std::{
4    collections::HashMap,
5    ops::{Add, AddAssign, RangeInclusive},
6    path::{Path, PathBuf},
7};
8
9pub mod analysis {
10    pub mod v1 {
11        include!("protos/qlty.analysis.v1.rs");
12    }
13}
14
15pub mod tests {
16    pub mod v1 {
17        include!("protos/qlty.tests.v1.rs");
18    }
19}
20
21impl From<tree_sitter::Range> for analysis::v1::Range {
22    fn from(range: tree_sitter::Range) -> Self {
23        analysis::v1::Range {
24            start_line: (range.start_point.row + 1) as u32,
25            start_column: (range.start_point.column + 1) as u32,
26            end_line: (range.end_point.row + 1) as u32,
27            end_column: (range.end_point.column + 1) as u32,
28            start_byte: Some(range.start_byte as u32),
29            end_byte: Some(range.end_byte as u32),
30        }
31    }
32}
33
34impl Eq for analysis::v1::Issue {}
35
36impl Ord for analysis::v1::Issue {
37    fn cmp(&self, other: &Self) -> Ordering {
38        let path_cmp = self.path().cmp(&other.path());
39        if path_cmp != Ordering::Equal {
40            return path_cmp;
41        }
42
43        let mut self_start_line = 0;
44
45        if let Some(self_location) = &self.location {
46            if let Some(self_start_range) = &self_location.range {
47                let self_start_line_range = self_start_range.line_range();
48                self_start_line = *self_start_line_range.start();
49            }
50        }
51
52        let mut other_start_line = 0;
53
54        if let Some(other_location) = &other.location {
55            if let Some(other_start_range) = &other_location.range {
56                let other_start_line_range = other_start_range.line_range();
57                other_start_line = *other_start_line_range.start();
58            }
59        }
60
61        let line_cmp = self_start_line.cmp(&other_start_line);
62        if line_cmp != Ordering::Equal {
63            return line_cmp;
64        }
65
66        let level_cmp = other.level.cmp(&self.level);
67        if level_cmp != Ordering::Equal {
68            return level_cmp;
69        }
70
71        self.message.cmp(&other.message)
72    }
73}
74
75impl PartialOrd for analysis::v1::Issue {
76    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77        Some(self.cmp(other))
78    }
79}
80
81impl analysis::v1::Issue {
82    pub fn rule_id(&self) -> String {
83        format!("{}:{}", self.tool, self.rule_key)
84    }
85
86    pub fn set_property_string(&mut self, key: &str, value: String) {
87        self.properties
88            .get_or_insert_with(Struct::default)
89            .fields
90            .insert(
91                key.to_string(),
92                Value {
93                    kind: Some(Kind::StringValue(value)),
94                },
95            );
96    }
97
98    pub fn set_property_number(&mut self, key: &str, value: f64) {
99        self.properties
100            .get_or_insert_with(Struct::default)
101            .fields
102            .insert(
103                key.to_string(),
104                Value {
105                    kind: Some(Kind::NumberValue(value)),
106                },
107            );
108    }
109
110    pub fn set_property_bool(&mut self, key: &str, value: bool) {
111        self.properties
112            .get_or_insert_with(Struct::default)
113            .fields
114            .insert(
115                key.to_string(),
116                Value {
117                    kind: Some(Kind::BoolValue(value)),
118                },
119            );
120    }
121
122    pub fn get_property_string(&self, key: &str) -> String {
123        match self.get_property_kind(key) {
124            Kind::StringValue(value) => value.clone(),
125            _ => panic!("Expected string value for property {}", key),
126        }
127    }
128
129    pub fn get_property_number(&self, key: &str) -> f64 {
130        match self.get_property_kind(key) {
131            Kind::NumberValue(value) => *value,
132            _ => panic!("Expected number value for property {}", key),
133        }
134    }
135
136    pub fn get_property_bool(&self, key: &str) -> bool {
137        match self.get_property_kind(key) {
138            Kind::BoolValue(value) => *value,
139            _ => panic!("Expected bool value for property {}", key),
140        }
141    }
142
143    fn get_property_kind(&self, key: &str) -> &Kind {
144        self.property_fields()
145            .get(key)
146            .unwrap()
147            .kind
148            .as_ref()
149            .unwrap()
150    }
151
152    fn property_fields(&self) -> &HashMap<String, pbjson_types::Value> {
153        &self.properties.as_ref().unwrap().fields
154    }
155
156    pub fn path(&self) -> Option<String> {
157        if let Some(location) = self.location() {
158            Some(location.path)
159        } else {
160            None
161        }
162    }
163
164    pub fn line_range(&self) -> Option<RangeInclusive<usize>> {
165        self.range().map(|range| range.line_range())
166    }
167
168    pub fn range(&self) -> Option<analysis::v1::Range> {
169        if let Some(location) = self.location() {
170            location.range()
171        } else {
172            None
173        }
174    }
175
176    pub fn location(&self) -> Option<analysis::v1::Location> {
177        self.location.clone()
178    }
179}
180
181impl analysis::v1::Replacement {
182    pub fn location(&self) -> analysis::v1::Location {
183        self.location.as_ref().unwrap().clone()
184    }
185
186    pub fn range(&self) -> analysis::v1::Range {
187        self.location().range().unwrap()
188    }
189}
190
191impl analysis::v1::Location {
192    pub fn line_range(&self) -> Option<RangeInclusive<usize>> {
193        self.range().map(|range| range.line_range())
194    }
195
196    pub fn range(&self) -> Option<analysis::v1::Range> {
197        self.range.clone()
198    }
199
200    pub fn relative_path(&self, base: &Path) -> String {
201        let path = PathBuf::from(&self.path);
202
203        // If it doesn't start with a slash, it is already
204        // relative so return it as-is
205        if path.is_relative() {
206            return self.path.clone();
207        }
208
209        path.strip_prefix(base)
210            .expect("Path not relative to base path")
211            .to_string_lossy()
212            .to_string()
213    }
214}
215
216impl std::hash::Hash for analysis::v1::Location {
217    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
218        self.path.hash(state);
219        self.range.hash(state);
220    }
221}
222
223impl Eq for analysis::v1::Location {}
224
225impl analysis::v1::Range {
226    pub fn line_range_u32(&self) -> RangeInclusive<u32> {
227        self.start_line..=self.end_line
228    }
229
230    pub fn line_range(&self) -> RangeInclusive<usize> {
231        (self.start_line as usize)..=(self.end_line as usize)
232    }
233}
234
235impl std::hash::Hash for analysis::v1::Range {
236    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
237        self.start_line.hash(state);
238        self.start_column.hash(state);
239        self.end_line.hash(state);
240        self.end_column.hash(state);
241        self.start_byte.hash(state);
242        self.end_byte.hash(state);
243    }
244}
245
246pub fn level_from_str(level: &str) -> analysis::v1::Level {
247    match level.to_lowercase().as_str() {
248        "high" => analysis::v1::Level::High,
249        "medium" => analysis::v1::Level::Medium,
250        "low" => analysis::v1::Level::Low,
251        "fmt" => analysis::v1::Level::Fmt,
252        _ => analysis::v1::Level::Unspecified,
253    }
254}
255
256pub fn category_from_str(category: &str) -> analysis::v1::Category {
257    match category.to_lowercase().as_str() {
258        "bug" => analysis::v1::Category::Bug,
259        "vulnerability" => analysis::v1::Category::Vulnerability,
260        "structure" => analysis::v1::Category::Structure,
261        "duplication" => analysis::v1::Category::Duplication,
262        "security_hotspot" => analysis::v1::Category::SecurityHotspot,
263        "performance" => analysis::v1::Category::Performance,
264        "documentation" => analysis::v1::Category::Documentation,
265        "type_check" => analysis::v1::Category::TypeCheck,
266        "style" => analysis::v1::Category::Style,
267        "anti_pattern" => analysis::v1::Category::AntiPattern,
268        "accessibility" => analysis::v1::Category::Accessibility,
269        _ => analysis::v1::Category::Unspecified,
270    }
271}
272
273impl analysis::v1::Level {
274    pub fn as_lower_str_name(&self) -> &'static str {
275        match self {
276            analysis::v1::Level::Unspecified => "unspecified",
277            analysis::v1::Level::Fmt => "fmt",
278            analysis::v1::Level::Low => "low",
279            analysis::v1::Level::Medium => "medium",
280            analysis::v1::Level::High => "high",
281        }
282    }
283}
284
285impl clap::ValueEnum for analysis::v1::Level {
286    fn value_variants<'a>() -> &'a [Self] {
287        &[
288            analysis::v1::Level::Unspecified,
289            analysis::v1::Level::Fmt,
290            analysis::v1::Level::Low,
291            analysis::v1::Level::Medium,
292            analysis::v1::Level::High,
293        ]
294    }
295
296    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
297        Some(clap::builder::PossibleValue::new(self.as_lower_str_name()))
298    }
299}
300
301pub fn language_enum_from_name(name: &str) -> analysis::v1::Language {
302    match name {
303        "php" => analysis::v1::Language::Php,
304        "kotlin" => analysis::v1::Language::Kotlin,
305        "go" => analysis::v1::Language::Go,
306        "java" => analysis::v1::Language::Java,
307        "javascript" => analysis::v1::Language::Javascript,
308        "jsx" => analysis::v1::Language::Jsx,
309        "python" => analysis::v1::Language::Python,
310        "ruby" => analysis::v1::Language::Ruby,
311        "rust" => analysis::v1::Language::Rust,
312        "tsx" => analysis::v1::Language::Tsx,
313        "typescript" => analysis::v1::Language::Typescript,
314        _ => panic!("Unrecognized language name: {}", name),
315    }
316}
317
318pub fn calculate_effort_minutes(
319    value_delta: u32,
320    base_minutes: u32,
321    minutes_per_delta: u32,
322) -> u32 {
323    base_minutes + (value_delta * minutes_per_delta)
324}
325
326impl Add for analysis::v1::Stats {
327    type Output = Self;
328
329    fn add(self, other: Self) -> Self {
330        Self {
331            name: self.name,
332            fully_qualified_name: self.fully_qualified_name,
333            workspace_id: self.workspace_id,
334            project_id: self.project_id,
335            kind: self.kind,
336            path: self.path,
337            language: self.language,
338            analyzed_at: self.analyzed_at,
339            files: Some(self.files.unwrap_or(0) + other.files.unwrap_or(0)),
340            functions: Some(self.functions.unwrap_or(0) + other.functions.unwrap_or(0)),
341            classes: Some(self.classes.unwrap_or(0) + other.classes.unwrap_or(0)),
342            fields: Some(self.fields.unwrap_or(0) + other.fields.unwrap_or(0)),
343            lines: Some(self.lines.unwrap_or(0) + other.lines.unwrap_or(0)),
344            code_lines: Some(self.code_lines.unwrap_or(0) + other.code_lines.unwrap_or(0)),
345            blank_lines: Some(self.blank_lines.unwrap_or(0) + other.blank_lines.unwrap_or(0)),
346            comment_lines: Some(self.comment_lines.unwrap_or(0) + other.comment_lines.unwrap_or(0)),
347            complexity: Some(self.complexity.unwrap_or(0) + other.complexity.unwrap_or(0)),
348            cyclomatic: Some(self.cyclomatic.unwrap_or(0) + other.cyclomatic.unwrap_or(0)),
349            lcom4: Some(self.lcom4.unwrap_or(0) + other.lcom4.unwrap_or(0)),
350            reference: self.reference,
351            build_id: self.build_id,
352            commit_sha: self.commit_sha,
353            pull_request_number: self.pull_request_number,
354            tracked_branch_id: self.tracked_branch_id,
355        }
356    }
357}
358
359impl tests::v1::CoverageSummary {
360    pub fn percent(&self) -> f64 {
361        self.covered as f64 / (self.covered + self.missed) as f64 * 100.0
362    }
363}
364
365impl Add for tests::v1::CoverageSummary {
366    type Output = Self;
367
368    fn add(self, other: Self) -> Self {
369        Self {
370            covered: self.covered + other.covered,
371            missed: self.missed + other.missed,
372            omit: self.omit + other.omit,
373            total: self.total + other.total,
374        }
375    }
376}
377
378impl Copy for tests::v1::CoverageSummary {}
379
380impl AddAssign for tests::v1::CoverageSummary {
381    fn add_assign(&mut self, other: Self) {
382        *self = *self + other;
383    }
384}