ferrous_actions/cargo_hooks/
annotation.rs

1use super::Hook;
2use crate::actions::core::AnnotationLevel;
3use crate::actions::exec::Command;
4use crate::warning;
5use async_trait::async_trait;
6use cargo_metadata::diagnostic::{DiagnosticLevel, DiagnosticSpan};
7use std::borrow::Cow;
8
9#[derive(Default)]
10pub struct Annotation {
11    subcommand: String,
12}
13
14impl Annotation {
15    pub fn new(subcommand: &str) -> Annotation {
16        Annotation {
17            subcommand: subcommand.to_string(),
18        }
19    }
20
21    fn process_json_record(cargo_subcommand: &str, line: &str) {
22        use crate::actions::core::Annotation;
23        use crate::node::path::Path;
24        use cargo_metadata::Message;
25
26        // Ignore blank lines
27        let line = line.trim();
28        if line.is_empty() {
29            return;
30        }
31
32        let metadata: Message = match serde_json::from_str(line) {
33            Ok(metadata) => metadata,
34            Err(e) => {
35                warning!("Unable to cargo output line as JSON metadata record: {}", e);
36                return;
37            }
38        };
39        if let Message::CompilerMessage(compiler_message) = metadata {
40            let diagnostic = &compiler_message.message;
41            let level = Self::annotation_level(diagnostic.level);
42            let mut annotation = if let Some(rendered) = &diagnostic.rendered {
43                let mut annotation = Annotation::from(rendered.as_str());
44                annotation.title(&format!("cargo-{}: {}", cargo_subcommand, diagnostic.message));
45                annotation
46            } else {
47                let mut annotation = Annotation::from(diagnostic.message.as_str());
48                annotation.title(&format!("cargo-{}", cargo_subcommand));
49                annotation
50            };
51            if let Some(span) = Self::get_primary_span(&diagnostic.spans) {
52                let file_name = Path::from(&span.file_name);
53                annotation
54                    .file(&file_name)
55                    .start_line(span.line_start)
56                    .end_line(span.line_end)
57                    .start_column(span.column_start)
58                    .end_column(span.column_end);
59            }
60            annotation.output(level);
61        }
62    }
63
64    fn annotation_level(level: DiagnosticLevel) -> AnnotationLevel {
65        #[allow(clippy::match_same_arms)]
66        match level {
67            DiagnosticLevel::Ice | DiagnosticLevel::Error => AnnotationLevel::Error,
68            DiagnosticLevel::Warning => AnnotationLevel::Warning,
69            DiagnosticLevel::FailureNote | DiagnosticLevel::Note | DiagnosticLevel::Help => AnnotationLevel::Notice,
70            _ => AnnotationLevel::Warning,
71        }
72    }
73
74    fn get_primary_span(spans: &[DiagnosticSpan]) -> Option<&DiagnosticSpan> {
75        spans.iter().find(|s| s.is_primary)
76    }
77}
78
79#[async_trait(?Send)]
80impl Hook for Annotation {
81    fn additional_cargo_options(&self) -> Vec<Cow<str>> {
82        vec!["--message-format=json".into()]
83    }
84
85    fn modify_command(&self, command: &mut Command) {
86        use crate::actions::exec::Stdio;
87
88        let subcommand = self.subcommand.clone();
89        command
90            .outline(move |line| Self::process_json_record(&subcommand, line))
91            .stdout(Stdio::null());
92    }
93}