Skip to main content

cargo_sonar/
lib.rs

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// Parsers
12#[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// Reporter
28#[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    /// Name of the analyzer which produce the issue
116    fn analyzer_id(&self) -> String;
117    /// Name of the issue
118    fn issue_id(&self) -> String;
119    /// Unique identifier of the issue. Default implementation concatenate
120    /// `analyzer_id()` and `issue_id()`.
121    #[inline]
122    fn issue_uid(&self) -> String {
123        format!("{}::{}", self.analyzer_id(), self.issue_id())
124    }
125    /// Fingerprint is used to track an issue over evolutions of the code.
126    /// Ideally, even if the line changed but the issue is the same, the
127    /// fingerprint should not change.
128    fn fingerprint(&self) -> Digest;
129    /// Type of the reported lint, issue or vulnerability
130    fn category(&self) -> Category;
131    /// Severity of the reported issue, can be used to prioritize the issues
132    /// to fix.
133    fn severity(&self) -> Severity;
134    /// Location of the issue in a file. This information is crucial for nice
135    /// display in UI.
136    fn location(&self) -> Option<Location>;
137    /// Optional other locations related to the same issue.
138    #[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    // When compiling only with the `clippy` feature,
160    // 'lock lifetime on IssueType is not used by any variant.
161    // Phantom variant is here for avoiding a compilation error in this case.
162    #[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: only for tests
186            #[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: only for tests
209            #[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: only for tests
232            #[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: only for tests
255            #[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: only for tests
278            #[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: only for tests
301            #[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    // When feture 'clippy' is used alone, 'lockfile' is not used at all
341    // Make it 'allow(unused)' in this case
342    #[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    /// Create a new converter
360    ///
361    /// # Errors
362    /// May fail when trying to read and parse the 'Cargo.lock' file.
363    #[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    /// Transform all the issues into Sonar compatible format
452    ///
453    /// # Errors
454    /// If any input file is having a problem to read or parse, this operation will result in an error.
455    #[cfg(feature = "sonar")]
456    #[inline]
457    pub fn sonarize(&self, options: &Command) -> Result<sonar::Issues> {
458        self.report::<Sonar>(options)
459    }
460    /// Transform all the issues into Codeclimate compatible format
461    ///
462    /// # Errors
463    /// If any input file is having a problem to read or parse, this operation will result in an error.
464    #[cfg(feature = "codeclimate")]
465    #[inline]
466    pub fn codeclimatize(&self, options: &Command) -> Result<codeclimate::Issues> {
467        self.report::<CodeClimate>(options)
468    }
469}