1 //! Types and function used to emit pretty diagnostics for `bindgen`.
2 //!
3 //! The entry point of this module is the [`Diagnostic`] type.
4 
5 use std::fmt::Write;
6 use std::io::{self, BufRead, BufReader};
7 use std::{borrow::Cow, fs::File};
8 
9 use annotate_snippets::{
10     display_list::{DisplayList, FormatOptions},
11     snippet::{Annotation, Slice as ExtSlice, Snippet},
12 };
13 
14 use annotate_snippets::snippet::AnnotationType;
15 
16 #[derive(Clone, Copy, Debug)]
17 pub(crate) enum Level {
18     Error,
19     Warn,
20     Info,
21     Note,
22     Help,
23 }
24 
25 impl From<Level> for AnnotationType {
from(level: Level) -> Self26     fn from(level: Level) -> Self {
27         match level {
28             Level::Error => Self::Error,
29             Level::Warn => Self::Warning,
30             Level::Info => Self::Info,
31             Level::Note => Self::Note,
32             Level::Help => Self::Help,
33         }
34     }
35 }
36 
37 /// A `bindgen` diagnostic.
38 #[derive(Default)]
39 pub(crate) struct Diagnostic<'a> {
40     title: Option<(Cow<'a, str>, Level)>,
41     slices: Vec<Slice<'a>>,
42     footer: Vec<(Cow<'a, str>, Level)>,
43 }
44 
45 impl<'a> Diagnostic<'a> {
46     /// Add a title to the diagnostic and set its type.
with_title( &mut self, title: impl Into<Cow<'a, str>>, level: Level, ) -> &mut Self47     pub(crate) fn with_title(
48         &mut self,
49         title: impl Into<Cow<'a, str>>,
50         level: Level,
51     ) -> &mut Self {
52         self.title = Some((title.into(), level));
53         self
54     }
55 
56     /// Add a slice of source code to the diagnostic.
add_slice(&mut self, slice: Slice<'a>) -> &mut Self57     pub(crate) fn add_slice(&mut self, slice: Slice<'a>) -> &mut Self {
58         self.slices.push(slice);
59         self
60     }
61 
62     /// Add a footer annotation to the diagnostic. This annotation will have its own type.
add_annotation( &mut self, msg: impl Into<Cow<'a, str>>, level: Level, ) -> &mut Self63     pub(crate) fn add_annotation(
64         &mut self,
65         msg: impl Into<Cow<'a, str>>,
66         level: Level,
67     ) -> &mut Self {
68         self.footer.push((msg.into(), level));
69         self
70     }
71 
72     /// Print this diagnostic.
73     ///
74     /// The diagnostic is printed using `cargo:warning` if `bindgen` is being invoked by a build
75     /// script or using `eprintln` otherwise.
display(&self)76     pub(crate) fn display(&self) {
77         std::thread_local! {
78             static INVOKED_BY_BUILD_SCRIPT: bool =  std::env::var_os("CARGO_CFG_TARGET_ARCH").is_some();
79         }
80 
81         let mut title = None;
82         let mut footer = vec![];
83         let mut slices = vec![];
84         if let Some((msg, level)) = &self.title {
85             title = Some(Annotation {
86                 id: Some("bindgen"),
87                 label: Some(msg.as_ref()),
88                 annotation_type: (*level).into(),
89             })
90         }
91 
92         for (msg, level) in &self.footer {
93             footer.push(Annotation {
94                 id: None,
95                 label: Some(msg.as_ref()),
96                 annotation_type: (*level).into(),
97             });
98         }
99 
100         // add additional info that this is generated by bindgen
101         // so as to not confuse with rustc warnings
102         footer.push(Annotation {
103             id: None,
104             label: Some("This diagnostic was generated by bindgen."),
105             annotation_type: AnnotationType::Info,
106         });
107 
108         for slice in &self.slices {
109             if let Some(source) = &slice.source {
110                 slices.push(ExtSlice {
111                     source: source.as_ref(),
112                     line_start: slice.line.unwrap_or_default(),
113                     origin: slice.filename.as_deref(),
114                     annotations: vec![],
115                     fold: false,
116                 })
117             }
118         }
119 
120         let snippet = Snippet {
121             title,
122             footer,
123             slices,
124             opt: FormatOptions {
125                 color: true,
126                 ..Default::default()
127             },
128         };
129         let dl = DisplayList::from(snippet);
130 
131         if INVOKED_BY_BUILD_SCRIPT.with(Clone::clone) {
132             // This is just a hack which hides the `warning:` added by cargo at the beginning of
133             // every line. This should be fine as our diagnostics already have a colorful title.
134             // FIXME (pvdrz): Could it be that this doesn't work in other languages?
135             let hide_warning = "\r        \r";
136             let string = dl.to_string();
137             for line in string.lines() {
138                 println!("cargo:warning={}{}", hide_warning, line);
139             }
140         } else {
141             eprintln!("{}\n", dl);
142         }
143     }
144 }
145 
146 /// A slice of source code.
147 #[derive(Default)]
148 pub(crate) struct Slice<'a> {
149     source: Option<Cow<'a, str>>,
150     filename: Option<String>,
151     line: Option<usize>,
152 }
153 
154 impl<'a> Slice<'a> {
155     /// Set the source code.
with_source( &mut self, source: impl Into<Cow<'a, str>>, ) -> &mut Self156     pub(crate) fn with_source(
157         &mut self,
158         source: impl Into<Cow<'a, str>>,
159     ) -> &mut Self {
160         self.source = Some(source.into());
161         self
162     }
163 
164     /// Set the file, line and column.
with_location( &mut self, mut name: String, line: usize, col: usize, ) -> &mut Self165     pub(crate) fn with_location(
166         &mut self,
167         mut name: String,
168         line: usize,
169         col: usize,
170     ) -> &mut Self {
171         write!(name, ":{}:{}", line, col)
172             .expect("Writing to a string cannot fail");
173         self.filename = Some(name);
174         self.line = Some(line);
175         self
176     }
177 }
178 
get_line( filename: &str, line: usize, ) -> io::Result<Option<String>>179 pub(crate) fn get_line(
180     filename: &str,
181     line: usize,
182 ) -> io::Result<Option<String>> {
183     let file = BufReader::new(File::open(filename)?);
184     if let Some(line) = file.lines().nth(line.wrapping_sub(1)) {
185         return line.map(Some);
186     }
187 
188     Ok(None)
189 }
190