1use crate::{Category, Location, Severity, TextRange};
2use cargo_metadata::{
3 diagnostic::{DiagnosticLevel, DiagnosticSpan},
4 CompilerMessage, Message,
5};
6use dyn_iter::IntoDynIterator as _;
7use std::{
8 io::{BufRead as _, BufReader},
9 path::PathBuf,
10};
11
12const CLIPPY_ENGINE: &str = "clippy";
13
14#[derive(Debug)]
15pub struct Clippy {
16 compiler_messages: dyn_iter::DynIter<'static, CompilerMessage>,
17}
18
19impl Clippy {
20 #[inline]
25 pub fn try_new<R>(json_read: R) -> eyre::Result<Self>
26 where
27 R: std::io::Read + 'static,
28 {
29 let reader = BufReader::new(json_read);
30 let compiler_messages = reader
31 .lines()
32 .map_while(Result::ok)
33 .flat_map(|line| serde_json::from_str::<cargo_metadata::Message>(&line))
34 .filter_map(|message| {
35 if let Message::CompilerMessage(compiler_message) = message {
36 Some(compiler_message)
37 } else {
38 None
39 }
40 })
41 .into_dyn_iter();
42 let clippy = Self { compiler_messages };
43 Ok(clippy)
44 }
45}
46
47impl Iterator for Clippy {
48 type Item = CompilerMessage;
49
50 #[inline]
51 fn next(&mut self) -> Option<Self::Item> {
52 self.compiler_messages.next()
53 }
54}
55
56impl From<&DiagnosticLevel> for Severity {
57 #[inline]
58 fn from(level: &DiagnosticLevel) -> Self {
59 match *level {
60 DiagnosticLevel::Ice => Severity::Blocker,
61 DiagnosticLevel::Error => Severity::Critical,
62 DiagnosticLevel::Warning => Severity::Major,
63 DiagnosticLevel::FailureNote => Severity::Minor,
64 _ => Severity::Info,
65 }
66 }
67}
68
69impl From<&DiagnosticSpan> for TextRange {
70 #[inline]
71 fn from(span: &DiagnosticSpan) -> Self {
72 TextRange::new(
73 (span.line_start, span.column_start.saturating_sub(1)),
74 (span.line_end, span.column_end.saturating_sub(1)),
75 )
76 }
77}
78
79pub type Issue = CompilerMessage;
80impl crate::Issue for Issue {
81 #[inline]
82 fn analyzer_id(&self) -> String {
83 CLIPPY_ENGINE.to_owned()
84 }
85
86 #[inline]
87 fn issue_id(&self) -> String {
88 self.message.code.as_ref().map_or_else(
89 || String::from("unknown"),
90 |diagnostic_code| {
91 diagnostic_code
92 .code
93 .trim_start_matches("clippy::")
94 .to_owned()
95 },
96 )
97 }
98
99 #[inline]
100 fn fingerprint(&self) -> md5::Digest {
101 md5::compute(&self.message.message)
102 }
103
104 #[inline]
105 fn category(&self) -> Category {
106 Category::Style
107 }
108
109 #[inline]
110 fn severity(&self) -> Severity {
111 Severity::from(&self.message.level)
112 }
113
114 #[inline]
115 fn location(&self) -> Option<Location> {
116 self.message.spans.first().map(|span| Location {
117 path: PathBuf::from(&span.file_name),
118 range: TextRange::from(span),
119 message: self.message.message.clone(),
120 })
121 }
122
123 #[inline]
124 fn other_locations(&self) -> Vec<Location> {
125 self.message
126 .spans
127 .iter()
128 .skip(1)
129 .map(|span| Location {
130 path: PathBuf::from(&span.file_name),
131 range: TextRange::from(span),
132 message: self.message.message.clone(),
133 })
134 .collect()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use crate::{clippy::Clippy, Category, Issue as _, Severity};
141 use std::{io::Write as _, path::PathBuf};
142 use test_log::test;
143
144 #[test]
145 fn single_issue() {
146 let json = r##"{
147 "reason": "compiler-message",
148 "package_id": "cargo-sonar 0.15.0 (path+file:///home/woshilapin/projects/woshilapin/cargo-sonar)",
149 "manifest_path": "/home/woshilapin/projects/woshilapin/cargo-sonar/Cargo.toml",
150 "target": {
151 "kind": [
152 "bin"
153 ],
154 "crate_types": [
155 "bin"
156 ],
157 "name": "cargo-sonar",
158 "src_path": "/home/woshilapin/projects/woshilapin/cargo-sonar/src/main.rs",
159 "edition": "2021",
160 "doc": true,
161 "doctest": false,
162 "test": true
163 },
164 "message": {
165 "rendered": "warning: used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`\n --> src/clippy.rs:156:5\n |\n156 | #[test]\n | ^^^^^^^\n |\n = help: `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertions should not be used in a function that returns `Result` as `Result` is expected to return an error instead of crashing\nnote: return Err() instead of panicking\n --> src/clippy.rs:206:9\n |\n206 | assert_eq!(sonar_issues.len(), 1);\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#panic_in_result_fn\nnote: the lint level is defined here\n --> src/main.rs:26:5\n |\n26 | clippy::panic_in_result_fn,\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n = note: this warning originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)\n\n",
166 "children": [
167 {
168 "children": [],
169 "code": null,
170 "level": "help",
171 "message": "`unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertions should not be used in a function that returns `Result` as `Result` is expected to return an error instead of crashing",
172 "rendered": null,
173 "spans": []
174 },
175 {
176 "children": [],
177 "code": null,
178 "level": "note",
179 "message": "return Err() instead of panicking",
180 "rendered": null,
181 "spans": [
182 {
183 "byte_end": 6887,
184 "byte_start": 6854,
185 "column_end": 42,
186 "column_start": 9,
187 "expansion": null,
188 "file_name": "src/clippy.rs",
189 "is_primary": true,
190 "label": null,
191 "line_end": 206,
192 "line_start": 206,
193 "suggested_replacement": null,
194 "suggestion_applicability": null,
195 "text": [
196 {
197 "highlight_end": 42,
198 "highlight_start": 9,
199 "text": " assert_eq!(sonar_issues.len(), 1);"
200 }
201 ]
202 }
203 ]
204 },
205 {
206 "children": [],
207 "code": null,
208 "level": "help",
209 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#panic_in_result_fn",
210 "rendered": null,
211 "spans": []
212 },
213 {
214 "children": [],
215 "code": null,
216 "level": "note",
217 "message": "the lint level is defined here",
218 "rendered": null,
219 "spans": [
220 {
221 "byte_end": 757,
222 "byte_start": 731,
223 "column_end": 31,
224 "column_start": 5,
225 "expansion": null,
226 "file_name": "src/main.rs",
227 "is_primary": true,
228 "label": null,
229 "line_end": 26,
230 "line_start": 26,
231 "suggested_replacement": null,
232 "suggestion_applicability": null,
233 "text": [
234 {
235 "highlight_end": 31,
236 "highlight_start": 5,
237 "text": " clippy::panic_in_result_fn,"
238 }
239 ]
240 }
241 ]
242 }
243 ],
244 "code": {
245 "code": "clippy::panic_in_result_fn",
246 "explanation": null
247 },
248 "level": "warning",
249 "message": "used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`",
250 "spans": [
251 {
252 "byte_end": 5238,
253 "byte_start": 5231,
254 "column_end": 12,
255 "column_start": 5,
256 "expansion": {
257 "def_site_span": {
258 "byte_end": 2153,
259 "byte_start": 2089,
260 "column_end": 65,
261 "column_start": 1,
262 "expansion": null,
263 "file_name": "/home/woshilapin/.cargo/registry/src/index.crates.io-6f17d22bba15001f/test-log-0.2.12/src/lib.rs",
264 "is_primary": false,
265 "label": null,
266 "line_end": 82,
267 "line_start": 82,
268 "suggested_replacement": null,
269 "suggestion_applicability": null,
270 "text": [
271 {
272 "highlight_end": 65,
273 "highlight_start": 1,
274 "text": "pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {"
275 }
276 ]
277 },
278 "macro_decl_name": "#[test]",
279 "span": {
280 "byte_end": 5238,
281 "byte_start": 5231,
282 "column_end": 12,
283 "column_start": 5,
284 "expansion": null,
285 "file_name": "src/clippy.rs",
286 "is_primary": false,
287 "label": null,
288 "line_end": 156,
289 "line_start": 156,
290 "suggested_replacement": null,
291 "suggestion_applicability": null,
292 "text": [
293 {
294 "highlight_end": 12,
295 "highlight_start": 5,
296 "text": " #[test]"
297 }
298 ]
299 }
300 },
301 "file_name": "src/clippy.rs",
302 "is_primary": true,
303 "label": null,
304 "line_end": 156,
305 "line_start": 156,
306 "suggested_replacement": null,
307 "suggestion_applicability": null,
308 "text": [
309 {
310 "highlight_end": 12,
311 "highlight_start": 5,
312 "text": " #[test]"
313 }
314 ]
315 }
316 ]
317 }
318 }"##;
319 let json = json.to_owned().replace('\n', "");
320 let mut clippy_json = tempfile::NamedTempFile::new().unwrap();
321 write!(clippy_json, "{}", json).unwrap();
322 let clippy_json = clippy_json.reopen().unwrap();
323
324 let mut clippy = Clippy::try_new(clippy_json).unwrap();
325 let issue = clippy.next().unwrap();
326 assert_eq!(issue.analyzer_id(), "clippy");
327 assert_eq!(issue.issue_uid(), "clippy::panic_in_result_fn");
328 assert!(matches!(issue.severity(), Severity::Major));
329 assert!(matches!(issue.category(), Category::Style));
330 let location = issue.location().unwrap();
331 assert_eq!(location.path, PathBuf::from("src/clippy.rs"));
332 assert_eq!(location.message, "used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`");
333 assert_eq!(location.range.start.line, 156);
334 assert_eq!(location.range.end.line, 156);
335 assert_eq!(location.range.start.column, 4);
336 assert_eq!(location.range.end.column, 11);
337 }
338}