| //! Types and function used to emit pretty diagnostics for `bindgen`. | |
| //! | |
| //! The entry point of this module is the [`Diagnostic`] type. | |
| use std::fmt::Write; | |
| use std::io::{self, BufRead, BufReader}; | |
| use std::{borrow::Cow, fs::File}; | |
| use annotate_snippets::{Renderer, Snippet}; | |
| pub(crate) use annotate_snippets::Level; | |
| /// A `bindgen` diagnostic. | |
| #[derive(Default)] | |
| pub(crate) struct Diagnostic<'a> { | |
| title: Option<(Cow<'a, str>, Level)>, | |
| slices: Vec<Slice<'a>>, | |
| footer: Vec<(Cow<'a, str>, Level)>, | |
| } | |
| impl<'a> Diagnostic<'a> { | |
| /// Add a title to the diagnostic and set its type. | |
| pub(crate) fn with_title( | |
| &mut self, | |
| title: impl Into<Cow<'a, str>>, | |
| level: Level, | |
| ) -> &mut Self { | |
| self.title = Some((title.into(), level)); | |
| self | |
| } | |
| /// Add a slice of source code to the diagnostic. | |
| pub(crate) fn add_slice(&mut self, slice: Slice<'a>) -> &mut Self { | |
| self.slices.push(slice); | |
| self | |
| } | |
| /// Add a footer annotation to the diagnostic. This annotation will have its own type. | |
| pub(crate) fn add_annotation( | |
| &mut self, | |
| msg: impl Into<Cow<'a, str>>, | |
| level: Level, | |
| ) -> &mut Self { | |
| self.footer.push((msg.into(), level)); | |
| self | |
| } | |
| /// Print this diagnostic. | |
| /// | |
| /// The diagnostic is printed using `cargo:warning` if `bindgen` is being invoked by a build | |
| /// script or using `eprintln` otherwise. | |
| pub(crate) fn display(&self) { | |
| std::thread_local! { | |
| static INVOKED_BY_BUILD_SCRIPT: bool = std::env::var_os("CARGO_CFG_TARGET_ARCH").is_some(); | |
| } | |
| let mut footer = vec![]; | |
| let mut slices = vec![]; | |
| let snippet = if let Some((msg, level)) = &self.title { | |
| (*level).title(msg) | |
| } else { | |
| return; | |
| }; | |
| for (msg, level) in &self.footer { | |
| footer.push((*level).title(msg)); | |
| } | |
| // add additional info that this is generated by bindgen | |
| // so as to not confuse with rustc warnings | |
| footer.push( | |
| Level::Info.title("This diagnostic was generated by bindgen."), | |
| ); | |
| for slice in &self.slices { | |
| if let Some(source) = &slice.source { | |
| let mut snippet = Snippet::source(source) | |
| .line_start(slice.line.unwrap_or_default()); | |
| if let Some(origin) = &slice.filename { | |
| snippet = snippet.origin(origin); | |
| } | |
| slices.push(snippet); | |
| } | |
| } | |
| let renderer = Renderer::styled(); | |
| let dl = renderer.render(snippet.snippets(slices).footers(footer)); | |
| if INVOKED_BY_BUILD_SCRIPT.with(Clone::clone) { | |
| // This is just a hack which hides the `warning:` added by cargo at the beginning of | |
| // every line. This should be fine as our diagnostics already have a colorful title. | |
| // FIXME (pvdrz): Could it be that this doesn't work in other languages? | |
| let hide_warning = "\r \r"; | |
| let string = dl.to_string(); | |
| for line in string.lines() { | |
| println!("cargo:warning={hide_warning}{line}"); | |
| } | |
| } else { | |
| eprintln!("{dl}\n"); | |
| } | |
| } | |
| } | |
| /// A slice of source code. | |
| #[derive(Default)] | |
| pub(crate) struct Slice<'a> { | |
| source: Option<Cow<'a, str>>, | |
| filename: Option<String>, | |
| line: Option<usize>, | |
| } | |
| impl<'a> Slice<'a> { | |
| /// Set the source code. | |
| pub(crate) fn with_source( | |
| &mut self, | |
| source: impl Into<Cow<'a, str>>, | |
| ) -> &mut Self { | |
| self.source = Some(source.into()); | |
| self | |
| } | |
| /// Set the file, line and column. | |
| pub(crate) fn with_location( | |
| &mut self, | |
| mut name: String, | |
| line: usize, | |
| col: usize, | |
| ) -> &mut Self { | |
| write!(name, ":{line}:{col}").expect("Writing to a string cannot fail"); | |
| self.filename = Some(name); | |
| self.line = Some(line); | |
| self | |
| } | |
| } | |
| pub(crate) fn get_line( | |
| filename: &str, | |
| line: usize, | |
| ) -> io::Result<Option<String>> { | |
| let file = BufReader::new(File::open(filename)?); | |
| if let Some(line) = file.lines().nth(line.wrapping_sub(1)) { | |
| return line.map(Some); | |
| } | |
| Ok(None) | |
| } |