1#![deny(missing_docs)]
7
8use std::borrow::Cow;
9use std::ops::Index;
10
11pub mod apt;
13pub mod autopkgtest;
15pub mod brz;
17pub mod cudf;
19pub mod lines;
21pub mod problems;
23
24pub mod problem;
27
28pub use problem::{problem_from_json, ProblemDeserializer, ProblemFromJsonFn};
29
30#[cfg(any(feature = "chatgpt", feature = "claude"))]
31pub mod llm;
33
34#[cfg(feature = "chatgpt")]
35pub mod chatgpt;
37
38#[cfg(feature = "claude")]
39pub mod claude;
41
42pub mod common;
44
45pub mod r#match;
47
48pub mod sbuild;
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn test_singlelinematch_line() {
57 let m = SingleLineMatch {
58 origin: Origin("test".to_string()),
59 offset: 10,
60 line: "test line".to_string(),
61 };
62 assert_eq!(m.line(), "test line");
63 }
64
65 #[test]
66 fn test_singlelinematch_origin() {
67 let m = SingleLineMatch {
68 origin: Origin("test".to_string()),
69 offset: 10,
70 line: "test line".to_string(),
71 };
72 let origin = m.origin();
73 assert_eq!(origin.as_str(), "test");
74 }
75
76 #[test]
77 fn test_singlelinematch_offset() {
78 let m = SingleLineMatch {
79 origin: Origin("test".to_string()),
80 offset: 10,
81 line: "test line".to_string(),
82 };
83 assert_eq!(m.offset(), 10);
84 }
85
86 #[test]
87 fn test_singlelinematch_lineno() {
88 let m = SingleLineMatch {
89 origin: Origin("test".to_string()),
90 offset: 10,
91 line: "test line".to_string(),
92 };
93 assert_eq!(m.lineno(), 11);
94 }
95
96 #[test]
97 fn test_singlelinematch_linenos() {
98 let m = SingleLineMatch {
99 origin: Origin("test".to_string()),
100 offset: 10,
101 line: "test line".to_string(),
102 };
103 assert_eq!(m.linenos(), vec![11]);
104 }
105
106 #[test]
107 fn test_singlelinematch_offsets() {
108 let m = SingleLineMatch {
109 origin: Origin("test".to_string()),
110 offset: 10,
111 line: "test line".to_string(),
112 };
113 assert_eq!(m.offsets(), vec![10]);
114 }
115
116 #[test]
117 fn test_singlelinematch_lines() {
118 let m = SingleLineMatch {
119 origin: Origin("test".to_string()),
120 offset: 10,
121 line: "test line".to_string(),
122 };
123 assert_eq!(m.lines(), vec!["test line"]);
124 }
125
126 #[test]
127 fn test_singlelinematch_add_offset() {
128 let m = SingleLineMatch {
129 origin: Origin("test".to_string()),
130 offset: 10,
131 line: "test line".to_string(),
132 };
133 let new_m = m.add_offset(5);
134 assert_eq!(new_m.offset(), 15);
135 }
136
137 #[test]
138 fn test_multilinelmatch_line() {
139 let m = MultiLineMatch {
140 origin: Origin("test".to_string()),
141 offsets: vec![10, 11, 12],
142 lines: vec![
143 "line 1".to_string(),
144 "line 2".to_string(),
145 "line 3".to_string(),
146 ],
147 };
148 assert_eq!(m.line(), "line 3");
149 }
150
151 #[test]
152 fn test_multilinelmatch_origin() {
153 let m = MultiLineMatch {
154 origin: Origin("test".to_string()),
155 offsets: vec![10, 11, 12],
156 lines: vec![
157 "line 1".to_string(),
158 "line 2".to_string(),
159 "line 3".to_string(),
160 ],
161 };
162 let origin = m.origin();
163 assert_eq!(origin.as_str(), "test");
164 }
165
166 #[test]
167 fn test_multilinelmatch_offset() {
168 let m = MultiLineMatch {
169 origin: Origin("test".to_string()),
170 offsets: vec![10, 11, 12],
171 lines: vec![
172 "line 1".to_string(),
173 "line 2".to_string(),
174 "line 3".to_string(),
175 ],
176 };
177 assert_eq!(m.offset(), 12);
178 }
179
180 #[test]
181 fn test_multilinelmatch_lineno() {
182 let m = MultiLineMatch {
183 origin: Origin("test".to_string()),
184 offsets: vec![10, 11, 12],
185 lines: vec![
186 "line 1".to_string(),
187 "line 2".to_string(),
188 "line 3".to_string(),
189 ],
190 };
191 assert_eq!(m.lineno(), 13);
192 }
193
194 #[test]
195 fn test_multilinelmatch_offsets() {
196 let m = MultiLineMatch {
197 origin: Origin("test".to_string()),
198 offsets: vec![10, 11, 12],
199 lines: vec![
200 "line 1".to_string(),
201 "line 2".to_string(),
202 "line 3".to_string(),
203 ],
204 };
205 assert_eq!(m.offsets(), vec![10, 11, 12]);
206 }
207
208 #[test]
209 fn test_multilinelmatch_lines() {
210 let m = MultiLineMatch {
211 origin: Origin("test".to_string()),
212 offsets: vec![10, 11, 12],
213 lines: vec![
214 "line 1".to_string(),
215 "line 2".to_string(),
216 "line 3".to_string(),
217 ],
218 };
219 assert_eq!(m.lines(), vec!["line 1", "line 2", "line 3"]);
220 }
221
222 #[test]
223 fn test_multilinelmatch_add_offset() {
224 let m = MultiLineMatch {
225 origin: Origin("test".to_string()),
226 offsets: vec![10, 11, 12],
227 lines: vec![
228 "line 1".to_string(),
229 "line 2".to_string(),
230 "line 3".to_string(),
231 ],
232 };
233 let new_m = m.add_offset(5);
234 assert_eq!(new_m.offsets(), vec![15, 16, 17]);
235 }
236
237 #[test]
238 fn test_highlight_lines() {
239 let lines = vec!["line 1", "line 2", "line 3", "line 4", "line 5"];
240 let m = SingleLineMatch {
241 origin: Origin("test".to_string()),
242 offset: 2,
243 line: "line 3".to_string(),
244 };
245 highlight_lines(&lines, &m, 1);
247 }
248}
249
250pub trait Match: Send + Sync + std::fmt::Debug + std::fmt::Display {
255 fn line(&self) -> &str;
257
258 fn origin(&self) -> &Origin;
260
261 fn offset(&self) -> usize;
263
264 fn lineno(&self) -> usize {
266 self.offset() + 1
267 }
268
269 fn linenos(&self) -> Vec<usize> {
271 self.offsets().iter().map(|&x| x + 1).collect()
272 }
273
274 fn offsets(&self) -> Vec<usize>;
276
277 fn lines(&self) -> Vec<&str>;
279
280 fn add_offset(&self, offset: usize) -> Box<dyn Match>;
282}
283
284#[derive(Clone, Debug)]
288pub struct Origin(String);
289
290impl Origin {
291 pub fn as_str(&self) -> &str {
293 &self.0
294 }
295}
296
297impl std::fmt::Display for Origin {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 f.write_str(&self.0)
300 }
301}
302
303#[derive(Clone, Debug)]
307pub struct SingleLineMatch {
308 pub origin: Origin,
310 pub offset: usize,
312 pub line: String,
314}
315
316impl Match for SingleLineMatch {
317 fn line(&self) -> &str {
318 &self.line
319 }
320
321 fn origin(&self) -> &Origin {
322 &self.origin
323 }
324
325 fn offset(&self) -> usize {
326 self.offset
327 }
328
329 fn offsets(&self) -> Vec<usize> {
330 vec![self.offset]
331 }
332
333 fn lines(&self) -> Vec<&str> {
334 vec![&self.line]
335 }
336
337 fn add_offset(&self, offset: usize) -> Box<dyn Match> {
338 Box::new(Self {
339 origin: self.origin.clone(),
340 offset: self.offset + offset,
341 line: self.line.clone(),
342 })
343 }
344}
345
346impl std::fmt::Display for SingleLineMatch {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 write!(f, "{}:{}: {}", self.origin.0, self.lineno(), self.line)
349 }
350}
351
352impl SingleLineMatch {
353 pub fn from_lines<'a>(
363 lines: &impl Index<usize, Output = &'a str>,
364 offset: usize,
365 origin: Option<&str>,
366 ) -> Self {
367 let line = &lines[offset];
368 let origin = origin
369 .map(|s| Origin(s.to_string()))
370 .unwrap_or_else(|| Origin("".to_string()));
371 Self {
372 origin,
373 offset,
374 line: line.to_string(),
375 }
376 }
377}
378
379#[derive(Clone, Debug)]
383pub struct MultiLineMatch {
384 pub origin: Origin,
386 pub offsets: Vec<usize>,
388 pub lines: Vec<String>,
390}
391
392impl MultiLineMatch {
393 pub fn new(origin: Origin, offsets: Vec<usize>, lines: Vec<String>) -> Self {
403 assert!(!offsets.is_empty());
404 assert!(offsets.len() == lines.len());
405 Self {
406 origin,
407 offsets,
408 lines,
409 }
410 }
411
412 pub fn from_lines<'a>(
422 lines: &impl Index<usize, Output = &'a str>,
423 offsets: Vec<usize>,
424 origin: Option<&str>,
425 ) -> Self {
426 let lines = offsets
427 .iter()
428 .map(|&offset| lines[offset].to_string())
429 .collect();
430 let origin = origin
431 .map(|s| Origin(s.to_string()))
432 .unwrap_or_else(|| Origin("".to_string()));
433 Self::new(origin, offsets, lines)
434 }
435}
436
437impl Match for MultiLineMatch {
438 fn line(&self) -> &str {
439 self.lines
440 .last()
441 .expect("MultiLineMatch should have at least one line")
442 }
443
444 fn origin(&self) -> &Origin {
445 &self.origin
446 }
447
448 fn offset(&self) -> usize {
449 *self
450 .offsets
451 .last()
452 .expect("MultiLineMatch should have at least one offset")
453 }
454
455 fn lineno(&self) -> usize {
456 self.offset() + 1
457 }
458
459 fn offsets(&self) -> Vec<usize> {
460 self.offsets.clone()
461 }
462
463 fn lines(&self) -> Vec<&str> {
464 self.lines.iter().map(|s| s.as_str()).collect()
465 }
466
467 fn add_offset(&self, extra: usize) -> Box<dyn Match> {
468 let offsets = self.offsets.iter().map(|&offset| offset + extra).collect();
469 Box::new(Self {
470 origin: self.origin.clone(),
471 offsets,
472 lines: self.lines.clone(),
473 })
474 }
475}
476
477impl std::fmt::Display for MultiLineMatch {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 write!(f, "{}:{}: {}", self.origin.0, self.lineno(), self.line())
480 }
481}
482
483pub trait Problem: std::fmt::Display + Send + Sync + std::fmt::Debug {
488 fn kind(&self) -> Cow<'_, str>;
490
491 fn json(&self) -> serde_json::Value;
493
494 fn as_any(&self) -> &dyn std::any::Any;
496
497 fn is_universal(&self) -> bool {
501 false
502 }
503}
504
505pub struct ProblemKindInfo {
509 pub kind: &'static str,
511 pub detail_fields: &'static [&'static str],
513}
514
515inventory::collect!(ProblemKindInfo);
516
517impl PartialEq for dyn Problem {
518 fn eq(&self, other: &Self) -> bool {
519 self.kind() == other.kind() && self.json() == other.json()
520 }
521}
522
523impl Eq for dyn Problem {}
524
525impl serde::Serialize for dyn Problem {
526 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
527 let mut map = serde_json::Map::new();
528 map.insert(
529 "kind".to_string(),
530 serde_json::Value::String(self.kind().to_string()),
531 );
532 map.insert("details".to_string(), self.json());
533 map.serialize(serializer)
534 }
535}
536
537impl std::hash::Hash for dyn Problem {
538 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
539 self.kind().hash(state);
540 self.json().hash(state);
541 }
542}
543
544pub fn highlight_lines(lines: &[&str], m: &dyn Match, context: usize) {
551 use std::cmp::{max, min};
552 if m.linenos().len() == 1 {
553 println!("Issue found at line {}:", m.lineno());
554 } else {
555 println!(
556 "Issue found at lines {}-{}:",
557 m.linenos().first().unwrap(),
558 m.linenos().last().unwrap()
559 );
560 }
561 let start = max(0, m.offsets()[0].saturating_sub(context));
562 let end = min(lines.len(), m.offsets().last().unwrap() + context + 1);
563
564 for (i, line) in lines.iter().enumerate().take(end).skip(start) {
565 println!(
566 " {} {}",
567 if m.offsets().contains(&i) { ">" } else { " " },
568 line.trim_end_matches('\n')
569 );
570 }
571}