Skip to main content

cargo_sonar/
clippy.rs

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    /// Create a Clippy parser for issues
21    ///
22    /// # Errors
23    /// May fail reading and parsing the file (IO errors).
24    #[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}