build_alert/
lib.rs

1//! [![github]](https://github.com/dtolnay/build-alert) [![crates-io]](https://crates.io/crates/build-alert) [![docs-rs]](https://docs.rs/build-alert)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! Display a message in the Cargo build output during compilation.
10//!
11//! # Example
12//!
13//! ```
14//! #[cfg(debug_assertions)]
15//! build_alert::yellow! {"
16//! NOTE:  use --release
17//!   Syn's test suite has some tests that run on every source file
18//!   and test case in the rust-lang/rust repo, which can be pretty
19//!   slow in debug mode. Consider running cargo test with `--release`
20//!   to speed things up.
21//! "}
22//!
23//! #[cfg(not(feature = "all-features"))]
24//! build_alert::red! {"
25//! ERROR:  use --all-features
26//!   Syn's test suite normally only works with all-features enabled.
27//!   Run again with `--all-features`, or run with `--features test`
28//!   to bypass this check.
29//! "}
30//! ```
31//!
32//! ![screenshot](https://user-images.githubusercontent.com/1940490/227811885-3eca7b65-0425-4be5-aa1a-cf52d8014817.png)
33
34#![allow(clippy::toplevel_ref_arg, clippy::uninlined_format_args)]
35
36use proc_macro::TokenStream;
37use std::io::Write;
38use std::process;
39use syn::{parse_macro_input, LitStr};
40use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
41
42/// For <kbd>NOTE:</kbd> or <kbd>WARNING:</kbd> alerts.
43#[proc_macro]
44pub fn yellow(input: TokenStream) -> TokenStream {
45    do_alert(Color::Yellow, input)
46}
47
48/// For <kbd>ERROR:</kbd> alerts.
49#[proc_macro]
50pub fn red(input: TokenStream) -> TokenStream {
51    do_alert(Color::Red, input)
52}
53
54fn do_alert(color: Color, input: TokenStream) -> TokenStream {
55    let message = parse_macro_input!(input as LitStr).value();
56
57    let ref mut stderr = StandardStream::stderr(ColorChoice::Auto);
58    let color_spec = ColorSpec::new().set_fg(Some(color)).clone();
59    let mut has_nonspace = false;
60    let mut says_error = false;
61
62    for mut line in message.lines() {
63        if !has_nonspace {
64            let (indent, heading, rest) = split_heading(line);
65            if let Some(indent) = indent {
66                let _ = write!(stderr, "{}", indent);
67            }
68            if let Some(heading) = heading {
69                let _ = stderr.set_color(color_spec.clone().set_bold(true));
70                let _ = write!(stderr, "{}", heading);
71                has_nonspace = true;
72                says_error = heading == "ERROR";
73            }
74            line = rest;
75        }
76        if line.is_empty() {
77            let _ = writeln!(stderr);
78        } else {
79            let _ = stderr.set_color(&color_spec);
80            let _ = writeln!(stderr, "{}", line);
81            has_nonspace = has_nonspace || line.contains(|ch: char| ch != ' ');
82        }
83    }
84
85    let _ = stderr.reset();
86    let _ = writeln!(stderr);
87
88    if color == Color::Red && says_error {
89        process::exit(1);
90    } else {
91        TokenStream::new()
92    }
93}
94
95fn split_heading(s: &str) -> (Option<&str>, Option<&str>, &str) {
96    let mut start = 0;
97    while start < s.len() && s[start..].starts_with(' ') {
98        start += 1;
99    }
100
101    let mut end = start;
102    while end < s.len() && s[end..].starts_with(|ch: char| ch.is_ascii_uppercase()) {
103        end += 1;
104    }
105
106    if end - start >= 3 && (end == s.len() || s[end..].starts_with(':')) {
107        let indent = (start > 0).then_some(&s[..start]);
108        let heading = &s[start..end];
109        let rest = &s[end..];
110        (indent, Some(heading), rest)
111    } else {
112        (None, None, s)
113    }
114}