circomspect_program_structure/utils/
writers.rs

1use anyhow;
2use anyhow::Context;
3use log::{info, warn};
4use std::fmt::Display;
5use std::fs::File;
6use std::io::Write;
7use std::path::{PathBuf, Path};
8use codespan_reporting::term;
9use termcolor::{StandardStream, ColorChoice, WriteColor, ColorSpec, Color};
10
11use crate::sarif_conversion::ToSarif;
12use crate::{
13    program_library::report::{Report, ReportCollection},
14    file_definition::FileLibrary,
15};
16
17pub trait ReportFilter {
18    /// Returns true if the report should be included.
19    fn filter(&self, report: &Report) -> bool;
20}
21
22impl<F: Fn(&Report) -> bool> ReportFilter for F {
23    fn filter(&self, report: &Report) -> bool {
24        self(report)
25    }
26}
27
28pub trait ReportWriter {
29    /// Filter and write the given reports. Returns the number of reports written.
30    fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize;
31
32    /// Filter and write a single report. Returns the number of reports written (0 or 1).
33    fn write_report(&mut self, report: Report, file_library: &FileLibrary) -> usize {
34        self.write_reports(&[report], file_library)
35    }
36
37    /// Returns the number of reports written.
38    #[must_use]
39    fn reports_written(&self) -> usize;
40}
41
42pub trait LogWriter {
43    fn write_messages<D: Display>(&mut self, messages: &[D]);
44
45    fn write_message<D: Display>(&mut self, message: D) {
46        self.write_messages(&[message]);
47    }
48}
49
50pub struct StdoutWriter {
51    verbose: bool,
52    written: usize,
53    writer: StandardStream,
54    filters: Vec<Box<dyn ReportFilter>>,
55}
56
57impl StdoutWriter {
58    pub fn new(verbose: bool) -> StdoutWriter {
59        let writer = if atty::is(atty::Stream::Stdout) {
60            StandardStream::stdout(ColorChoice::Always)
61        } else {
62            StandardStream::stdout(ColorChoice::Never)
63        };
64        StdoutWriter { verbose, written: 0, writer, filters: Vec::new() }
65    }
66
67    pub fn add_filter(mut self, filter: impl ReportFilter + 'static) -> StdoutWriter {
68        self.filters.push(Box::new(filter));
69        self
70    }
71
72    fn filter(&self, reports: &[Report]) -> ReportCollection {
73        reports
74            .iter()
75            .filter(|report| self.filters.iter().all(|f| f.filter(report)))
76            .cloned()
77            .collect()
78    }
79}
80
81impl LogWriter for StdoutWriter {
82    fn write_messages<D: Display>(&mut self, messages: &[D]) {
83        let mut spec = ColorSpec::new();
84        spec.set_fg(Some(Color::Green));
85
86        let write_impl = |message: &D| {
87            let mut writer = self.writer.lock();
88            writer.set_color(&spec)?;
89            write!(&mut writer, "circomspect")?;
90            writer.reset()?;
91            writeln!(&mut writer, ": {message}")
92        };
93        for message in messages {
94            write_impl(message).expect("failed to write log messages")
95        }
96    }
97}
98
99impl ReportWriter for StdoutWriter {
100    fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize {
101        let reports = self.filter(reports);
102
103        let mut config = term::Config::default();
104        let mut diagnostics = Vec::new();
105        let files = file_library.to_storage();
106        for report in reports.iter() {
107            diagnostics.push(report.to_diagnostic(self.verbose));
108        }
109        config.styles.header_help.set_intense(false);
110        config.styles.header_error.set_intense(false);
111        config.styles.header_warning.set_intense(false);
112        for diagnostic in diagnostics.iter() {
113            term::emit(&mut self.writer.lock(), &config, files, diagnostic)
114                .expect("failed to write reports");
115        }
116
117        self.written += reports.len();
118        reports.len()
119    }
120
121    /// Returns the number of reports written.
122    fn reports_written(&self) -> usize {
123        self.written
124    }
125}
126
127/// A `StdoutWriter` that caches all reports.
128pub struct CachedStdoutWriter {
129    writer: StdoutWriter,
130    reports: ReportCollection,
131}
132
133impl CachedStdoutWriter {
134    pub fn new(verbose: bool) -> CachedStdoutWriter {
135        CachedStdoutWriter { writer: StdoutWriter::new(verbose), reports: ReportCollection::new() }
136    }
137
138    pub fn reports(&self) -> &ReportCollection {
139        &self.reports
140    }
141
142    pub fn add_filter(mut self, filter: impl ReportFilter + 'static) -> CachedStdoutWriter {
143        self.writer.filters.push(Box::new(filter));
144        self
145    }
146}
147
148impl LogWriter for CachedStdoutWriter {
149    fn write_messages<D: Display>(&mut self, messages: &[D]) {
150        self.writer.write_messages(messages)
151    }
152}
153
154impl ReportWriter for CachedStdoutWriter {
155    fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize {
156        self.reports.extend(reports.iter().cloned());
157        self.writer.write_reports(reports, file_library)
158    }
159
160    fn reports_written(&self) -> usize {
161        self.writer.reports_written()
162    }
163}
164
165#[derive(Default)]
166pub struct SarifWriter {
167    sarif_file: PathBuf,
168    written: usize,
169    filters: Vec<Box<dyn ReportFilter>>,
170}
171
172impl SarifWriter {
173    pub fn new(sarif_file: &Path) -> SarifWriter {
174        SarifWriter { sarif_file: sarif_file.to_owned(), ..Default::default() }
175    }
176
177    pub fn add_filter(mut self, filter: impl ReportFilter + 'static) -> SarifWriter {
178        self.filters.push(Box::new(filter));
179        self
180    }
181
182    fn filter(&self, reports: &[Report]) -> ReportCollection {
183        reports
184            .iter()
185            .filter(|report| self.filters.iter().all(|f| f.filter(report)))
186            .cloned()
187            .collect()
188    }
189
190    fn serialize_reports(
191        &self,
192        reports: &ReportCollection,
193        file_library: &FileLibrary,
194    ) -> anyhow::Result<()> {
195        let sarif =
196            reports.to_sarif(file_library).context("failed to convert reports to Sarif format")?;
197        let json = serde_json::to_string_pretty(&sarif)?;
198        let mut sarif_file = File::create(&self.sarif_file)?;
199        writeln!(sarif_file, "{}", &json)
200            .with_context(|| format!("could not write to {}", self.sarif_file.display()))?;
201        Ok(())
202    }
203}
204
205impl ReportWriter for SarifWriter {
206    fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize {
207        let reports = self.filter(reports);
208        match self.serialize_reports(&reports, file_library) {
209            Ok(()) => {
210                info!("reports written to `{}`", self.sarif_file.display());
211                self.written += reports.len();
212                reports.len()
213            }
214            Err(_) => {
215                warn!("failed to write reports to `{}`", self.sarif_file.display());
216                0
217            }
218        }
219    }
220
221    fn reports_written(&self) -> usize {
222        self.written
223    }
224}