1#![cfg_attr(feature = "document-features", doc = "Feature flags")]
2#![cfg_attr(
3 feature = "document-features",
4 cfg_attr(doc, doc = ::document_features::document_features!())
5)]
6mod cargo;
7pub use crate::cargo::Lockfile;
8mod cli;
9pub use cli::Command;
10
11#[cfg(feature = "audit")]
13pub mod audit;
14#[cfg(feature = "clippy")]
15pub mod clippy;
16#[cfg(feature = "deny")]
17pub mod deny;
18#[cfg(feature = "outdated")]
19pub mod outdated;
20#[cfg(test)]
21mod test;
22#[cfg(feature = "typos")]
23pub mod typos;
24#[cfg(feature = "udeps")]
25pub mod udeps;
26
27#[cfg(feature = "codeclimate")]
29pub mod codeclimate;
30use cli::InputPath;
31#[cfg(feature = "codeclimate")]
32pub use codeclimate::CodeClimate;
33#[cfg(feature = "sonar")]
34pub mod sonar;
35#[cfg(feature = "sonar")]
36pub use sonar::Sonar;
37
38use eyre::{eyre, Context as _, Result};
39use md5::Digest;
40use std::{
41 fs::File,
42 io::{Read, Stdin},
43 marker::PhantomData,
44 path::PathBuf,
45};
46
47#[derive(Clone, Copy, Debug)]
48#[non_exhaustive]
49pub enum Severity {
50 Blocker,
51 Critical,
52 Major,
53 Minor,
54 Info,
55}
56
57#[derive(Clone, Copy, Debug)]
58#[non_exhaustive]
59pub enum Category {
60 Bug,
61 Complexity,
62 Duplication,
63 Performance,
64 Security,
65 Style,
66}
67
68#[derive(Clone, Copy, Debug)]
69struct Position {
70 line: usize,
71 column: usize,
72}
73
74#[derive(Clone, Copy, Debug)]
75pub struct TextRange {
76 start: Position,
77 end: Position,
78}
79
80impl TextRange {
81 #[inline]
82 #[must_use]
83 pub fn new(
84 (start_line, start_column): (usize, usize),
85 (end_line, end_column): (usize, usize),
86 ) -> Self {
87 Self {
88 start: Position {
89 line: start_line,
90 column: start_column,
91 },
92 end: Position {
93 line: end_line,
94 column: end_column,
95 },
96 }
97 }
98}
99
100impl Default for TextRange {
101 #[inline]
102 fn default() -> Self {
103 Self::new((1, 0), (1, 0))
104 }
105}
106
107#[derive(Clone, Debug)]
108pub struct Location {
109 pub path: PathBuf,
110 pub range: TextRange,
111 pub message: String,
112}
113
114pub trait Issue {
115 fn analyzer_id(&self) -> String;
117 fn issue_id(&self) -> String;
119 #[inline]
122 fn issue_uid(&self) -> String {
123 format!("{}::{}", self.analyzer_id(), self.issue_id())
124 }
125 fn fingerprint(&self) -> Digest;
129 fn category(&self) -> Category;
131 fn severity(&self) -> Severity;
134 fn location(&self) -> Option<Location>;
137 #[inline]
139 fn other_locations(&self) -> Vec<Location> {
140 Vec::new()
141 }
142}
143
144enum IssueType<'lock> {
145 #[cfg(feature = "audit")]
146 Audit(Box<audit::Issue<'lock>>),
147 #[cfg(feature = "clippy")]
148 Clippy(Box<clippy::Issue>),
149 #[cfg(feature = "deny")]
150 Deny(deny::Issue<'lock>),
151 #[cfg(feature = "outdated")]
152 Outdated(outdated::Issue<'lock>),
153 #[cfg(feature = "typos")]
154 Typos(typos::Issue),
155 #[cfg(feature = "udeps")]
156 Udeps(udeps::Issue<'lock>),
157 #[cfg(test)]
158 Test(test::Issue),
159 #[doc(hidden)]
163 #[allow(unused)]
164 Phantom(PhantomData<&'lock ()>),
165}
166
167impl crate::Issue for IssueType<'_> {
168 #[inline]
169 fn analyzer_id(&self) -> String {
170 match *self {
171 #[cfg(feature = "audit")]
172 IssueType::Audit(ref audit) => audit.analyzer_id(),
173 #[cfg(feature = "clippy")]
174 IssueType::Clippy(ref clippy) => clippy.analyzer_id(),
175 #[cfg(feature = "deny")]
176 IssueType::Deny(ref deny) => deny.analyzer_id(),
177 #[cfg(feature = "outdated")]
178 IssueType::Outdated(ref outdated) => outdated.analyzer_id(),
179 #[cfg(feature = "typos")]
180 IssueType::Typos(ref typos) => typos.analyzer_id(),
181 #[cfg(feature = "udeps")]
182 IssueType::Udeps(ref udeps) => udeps.analyzer_id(),
183 #[cfg(test)]
184 IssueType::Test(ref test) => test.analyzer_id(),
185 #[allow(clippy::unimplemented)]
187 _ => unimplemented!(),
188 }
189 }
190
191 #[inline]
192 fn issue_id(&self) -> String {
193 match *self {
194 #[cfg(feature = "audit")]
195 IssueType::Audit(ref audit) => audit.issue_id(),
196 #[cfg(feature = "clippy")]
197 IssueType::Clippy(ref clippy) => clippy.issue_id(),
198 #[cfg(feature = "deny")]
199 IssueType::Deny(ref deny) => deny.issue_id(),
200 #[cfg(feature = "outdated")]
201 IssueType::Outdated(ref outdated) => outdated.issue_id(),
202 #[cfg(feature = "typos")]
203 IssueType::Typos(ref typos) => typos.issue_id(),
204 #[cfg(feature = "udeps")]
205 IssueType::Udeps(ref udeps) => udeps.issue_id(),
206 #[cfg(test)]
207 IssueType::Test(ref test) => test.issue_id(),
208 #[allow(clippy::unimplemented)]
210 _ => unimplemented!(),
211 }
212 }
213
214 #[inline]
215 fn fingerprint(&self) -> Digest {
216 match *self {
217 #[cfg(feature = "audit")]
218 IssueType::Audit(ref audit) => audit.fingerprint(),
219 #[cfg(feature = "clippy")]
220 IssueType::Clippy(ref clippy) => clippy.fingerprint(),
221 #[cfg(feature = "deny")]
222 IssueType::Deny(ref deny) => deny.fingerprint(),
223 #[cfg(feature = "outdated")]
224 IssueType::Outdated(ref outdated) => outdated.fingerprint(),
225 #[cfg(feature = "typos")]
226 IssueType::Typos(ref typos) => typos.fingerprint(),
227 #[cfg(feature = "udeps")]
228 IssueType::Udeps(ref udeps) => udeps.fingerprint(),
229 #[cfg(test)]
230 IssueType::Test(ref test) => test.fingerprint(),
231 #[allow(clippy::unimplemented)]
233 _ => unimplemented!(),
234 }
235 }
236
237 #[inline]
238 fn category(&self) -> Category {
239 match *self {
240 #[cfg(feature = "audit")]
241 IssueType::Audit(ref audit) => audit.category(),
242 #[cfg(feature = "clippy")]
243 IssueType::Clippy(ref clippy) => clippy.category(),
244 #[cfg(feature = "deny")]
245 IssueType::Deny(ref deny) => deny.category(),
246 #[cfg(feature = "outdated")]
247 IssueType::Outdated(ref outdated) => outdated.category(),
248 #[cfg(feature = "typos")]
249 IssueType::Typos(ref typos) => typos.category(),
250 #[cfg(feature = "udeps")]
251 IssueType::Udeps(ref udeps) => udeps.category(),
252 #[cfg(test)]
253 IssueType::Test(ref test) => test.category(),
254 #[allow(clippy::unimplemented)]
256 _ => unimplemented!(),
257 }
258 }
259
260 #[inline]
261 fn severity(&self) -> Severity {
262 match *self {
263 #[cfg(feature = "audit")]
264 IssueType::Audit(ref audit) => audit.severity(),
265 #[cfg(feature = "clippy")]
266 IssueType::Clippy(ref clippy) => clippy.severity(),
267 #[cfg(feature = "deny")]
268 IssueType::Deny(ref deny) => deny.severity(),
269 #[cfg(feature = "outdated")]
270 IssueType::Outdated(ref outdated) => outdated.severity(),
271 #[cfg(feature = "typos")]
272 IssueType::Typos(ref typos) => typos.severity(),
273 #[cfg(feature = "udeps")]
274 IssueType::Udeps(ref udeps) => udeps.severity(),
275 #[cfg(test)]
276 IssueType::Test(ref test) => test.severity(),
277 #[allow(clippy::unimplemented)]
279 _ => unimplemented!(),
280 }
281 }
282
283 #[inline]
284 fn location(&self) -> Option<Location> {
285 match *self {
286 #[cfg(feature = "audit")]
287 IssueType::Audit(ref audit) => audit.location(),
288 #[cfg(feature = "clippy")]
289 IssueType::Clippy(ref clippy) => clippy.location(),
290 #[cfg(feature = "deny")]
291 IssueType::Deny(ref deny) => deny.location(),
292 #[cfg(feature = "outdated")]
293 IssueType::Outdated(ref outdated) => outdated.location(),
294 #[cfg(feature = "typos")]
295 IssueType::Typos(ref typos) => typos.location(),
296 #[cfg(feature = "udeps")]
297 IssueType::Udeps(ref udeps) => udeps.location(),
298 #[cfg(test)]
299 IssueType::Test(ref test) => test.location(),
300 #[allow(clippy::unimplemented)]
302 _ => unimplemented!(),
303 }
304 }
305}
306
307trait FromIssues<'lock> {
308 type Report;
309 fn from_issues(issues: impl IntoIterator<Item = IssueType<'lock>>) -> Self::Report;
310}
311
312fn read_from_input_path(input_path: &InputPath, binary_name: &str) -> Result<impl Read> {
313 enum InputPathRead {
314 Stdin(Stdin),
315 File(File),
316 }
317 impl Read for InputPathRead {
318 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
319 match self {
320 InputPathRead::Stdin(stdin) => stdin.read(buf),
321 InputPathRead::File(file) => file.read(buf),
322 }
323 }
324 }
325 match input_path {
326 cli::InputPath::Stdin => Ok(InputPathRead::Stdin(std::io::stdin())),
327 cli::InputPath::File(json_path) => File::open(json_path)
328 .map(InputPathRead::File)
329 .with_context(|| {
330 format!(
331 "failed to open '{}' report from '{}' file",
332 binary_name,
333 json_path.display(),
334 )
335 }),
336 }
337}
338
339pub struct Converter {
340 #[cfg_attr(
343 all(
344 feature = "clippy",
345 not(any(
346 feature = "audit",
347 feature = "deny",
348 feature = "outdated",
349 feature = "typos",
350 feature = "udeps"
351 ))
352 ),
353 allow(unused)
354 )]
355 lockfile: Lockfile,
356}
357
358impl Converter {
359 #[inline]
364 pub fn try_new() -> Result<Self> {
365 let path = PathBuf::from("Cargo.lock");
366 let lockfile = Lockfile::try_from(path.as_path())?;
367 Ok(Self { lockfile })
368 }
369
370 fn report<'c, F>(&'c self, options: &Command) -> Result<F::Report>
371 where
372 F: FromIssues<'c>,
373 {
374 if [
375 #[cfg(feature = "audit")]
376 matches!(options.audit_path, cli::InputPath::Stdin),
377 #[cfg(feature = "clippy")]
378 matches!(options.clippy_path, cli::InputPath::Stdin),
379 #[cfg(feature = "deny")]
380 matches!(options.deny_path, cli::InputPath::Stdin),
381 #[cfg(feature = "outdated")]
382 matches!(options.outdated_path, cli::InputPath::Stdin),
383 #[cfg(feature = "typos")]
384 matches!(options.typos_path, cli::InputPath::Stdin),
385 #[cfg(feature = "udeps")]
386 matches!(options.udeps_path, cli::InputPath::Stdin),
387 ]
388 .into_iter()
389 .map(usize::from)
390 .sum::<usize>()
391 > 1
392 {
393 return Err(eyre!(
394 "cannot have “stdin” (‘-’) be configured for two different reports at once"
395 ));
396 }
397 let mut issues: Box<dyn Iterator<Item = IssueType<'_>>> = Box::new(std::iter::empty());
398 #[cfg(feature = "audit")]
399 if options.audit {
400 let audit_read = read_from_input_path(&options.audit_path, "cargo-audit")?;
401 issues = Box::new(
402 issues.chain(
403 audit::Audit::try_new(audit_read, &self.lockfile)?
404 .map(Box::new)
405 .map(IssueType::Audit),
406 ),
407 );
408 }
409 #[cfg(feature = "clippy")]
410 if options.clippy {
411 let json_read = read_from_input_path(&options.clippy_path, "cargo-clippy")?;
412 issues = Box::new(
413 issues.chain(
414 clippy::Clippy::try_new(json_read)?
415 .map(Box::new)
416 .map(IssueType::Clippy),
417 ),
418 );
419 }
420 #[cfg(feature = "deny")]
421 if options.deny {
422 let json_read = read_from_input_path(&options.deny_path, "cargo-deny")?;
423 issues = Box::new(
424 issues.chain(deny::Deny::try_new(json_read, &self.lockfile)?.map(IssueType::Deny)),
425 );
426 }
427 #[cfg(feature = "outdated")]
428 if options.outdated {
429 let json_read = read_from_input_path(&options.outdated_path, "cargo-outdated")?;
430 issues = Box::new(issues.chain(
431 outdated::Outdated::try_new(json_read, &self.lockfile)?.map(IssueType::Outdated),
432 ));
433 }
434 #[cfg(feature = "typos")]
435 if options.typos {
436 let json_read = read_from_input_path(&options.typos_path, "typos")?;
437 issues =
438 Box::new(issues.chain(typos::Typos::try_new(json_read)?.map(IssueType::Typos)));
439 }
440 #[cfg(feature = "udeps")]
441 if options.udeps {
442 let json_read = read_from_input_path(&options.udeps_path, "cargo-udeps")?;
443 issues = Box::new(
444 issues
445 .chain(udeps::Udeps::try_new(json_read, &self.lockfile)?.map(IssueType::Udeps)),
446 );
447 }
448 Ok(F::from_issues(issues))
449 }
450
451 #[cfg(feature = "sonar")]
456 #[inline]
457 pub fn sonarize(&self, options: &Command) -> Result<sonar::Issues> {
458 self.report::<Sonar>(options)
459 }
460 #[cfg(feature = "codeclimate")]
465 #[inline]
466 pub fn codeclimatize(&self, options: &Command) -> Result<codeclimate::Issues> {
467 self.report::<CodeClimate>(options)
468 }
469}