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 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}