Skip to main content

necessist_core/
warn.rs

1use crate::{__ToConsoleString as ToConsoleString, LightContext};
2use ansi_term::{
3    Color::{Green, Yellow},
4    Style,
5};
6use anyhow::{Result, bail};
7use bitflags::bitflags;
8use heck::ToKebabCase;
9use std::{collections::BTreeMap, io::IsTerminal, sync::Mutex};
10
11// smoelius: `Warning` is part of Necessist's public API. Please try to follow the naming convention
12// of `what` (e.g., `Output`) followed by `why` (e.g., `Invalid`).
13#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
14#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
15#[non_exhaustive]
16#[remain::sorted]
17pub enum Warning {
18    All,
19    DatabaseDoesNotExist,
20    DryRunFailed,
21    FilesChanged,
22    IgnoredFunctionsUnsupported,
23    IgnoredMacrosUnsupported,
24    IgnoredMethodsUnsupported,
25    InstrumentationNonbuildable,
26    ItMessageNotFound,
27    LocalFunctionAmbiguous,
28    ModulePathUnknown,
29    OptionDeprecated,
30    OutputInvalid,
31    ParsingFailed,
32    RunTestFailed,
33}
34
35impl std::fmt::Display for Warning {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}", format!("{self:?}").to_kebab_case())
38    }
39}
40
41bitflags! {
42    #[derive(Clone, Copy)]
43    pub struct Flags: u8 {
44        const ONCE = 1 << 0;
45    }
46}
47
48/// Like [`warn`], but prints the warning prefixed with its source.
49#[allow(clippy::module_name_repetitions)]
50pub fn source_warn(
51    context: &LightContext,
52    warning: Warning,
53    source: &dyn ToConsoleString,
54    msg: &str,
55    flags: Flags,
56) -> Result<()> {
57    warn_internal(context, warning, Some(source), msg, flags)
58}
59
60/// Prints a warning message to the console.
61///
62/// # Arguments
63///
64/// * `context` - The context to use for printing the warning.
65/// * `warning` - The type of the warning.
66/// * `msg` - The message to print.
67/// * `flags` - The flags to use for printing the warning.
68///
69/// # Errors
70///
71/// Returns an error if the message could not be printed.
72pub fn warn(context: &LightContext, warning: Warning, msg: &str, flags: Flags) -> Result<()> {
73    warn_internal(context, warning, None, msg, flags)
74}
75
76const BUG_MSG: &str = "\
77This may indicate a bug in Necessist. Consider opening an issue at: \
78https://github.com/trailofbits/necessist/issues";
79
80bitflags! {
81    struct State: u8 {
82        const ALLOW_MSG_EMITTED = 1 << 0;
83        const BUG_MSG_EMITTED = 1 << 1;
84        const WARNING_EMITTED = 1 << 2;
85    }
86}
87
88static WARNING_STATE_MAP: Mutex<BTreeMap<Warning, State>> = Mutex::new(BTreeMap::new());
89
90fn warn_internal(
91    context: &LightContext,
92    warning: Warning,
93    source: Option<&dyn ToConsoleString>,
94    msg: &str,
95    flags: Flags,
96) -> Result<()> {
97    assert_ne!(warning, Warning::All);
98
99    #[allow(clippy::unwrap_used)]
100    let mut warning_state_map = WARNING_STATE_MAP.lock().unwrap();
101
102    let state = warning_state_map
103        .entry(warning)
104        .or_insert_with(State::empty);
105
106    // smoelius: Append `BUG_MSG` to `msg` in case we have to `bail!`.
107    let mut msg = msg.to_owned();
108    if may_be_bug(warning) && !state.contains(State::BUG_MSG_EMITTED) {
109        state.insert(State::BUG_MSG_EMITTED);
110        append_paragraph(&mut msg, BUG_MSG);
111    }
112
113    if context.opts.deny.contains(&Warning::All) || context.opts.deny.contains(&warning) {
114        bail!(msg);
115    }
116
117    if context.opts.quiet
118        || context.opts.allow.contains(&Warning::All)
119        || context.opts.allow.contains(&warning)
120        || (flags.contains(Flags::ONCE) && state.contains(State::WARNING_EMITTED))
121    {
122        return Ok(());
123    }
124
125    let allow_msg = if state.contains(State::ALLOW_MSG_EMITTED) {
126        String::new()
127    } else {
128        state.insert(State::ALLOW_MSG_EMITTED);
129        format!(
130            "
131Silence this warning with: --allow {warning}"
132        )
133    };
134
135    (context.println)(&format!(
136        "{}{}: {}{}",
137        source.map_or(String::new(), |source| format!(
138            "{}: ",
139            source.to_console_string()
140        )),
141        if std::io::stdout().is_terminal() {
142            Yellow.bold()
143        } else {
144            Style::default()
145        }
146        .paint("Warning"),
147        msg,
148        allow_msg
149    ));
150
151    state.insert(State::WARNING_EMITTED);
152
153    Ok(())
154}
155
156fn append_paragraph(msg: &mut String, paragraph: &str) {
157    msg.truncate(msg.trim_end().len());
158    msg.push_str("\n\n");
159    msg.push_str(paragraph);
160    msg.push('\n');
161}
162
163pub(crate) fn note(context: &LightContext, msg: &str) {
164    if context.opts.quiet {
165        return;
166    }
167
168    (context.println)(&format!(
169        "{}: {}",
170        if std::io::stdout().is_terminal() {
171            Green.bold()
172        } else {
173            Style::default()
174        }
175        .paint("Note"),
176        msg
177    ));
178}
179
180fn may_be_bug(warning: Warning) -> bool {
181    match warning {
182        Warning::All => unreachable!(),
183        Warning::DatabaseDoesNotExist
184        | Warning::DryRunFailed
185        | Warning::FilesChanged
186        | Warning::IgnoredFunctionsUnsupported
187        | Warning::IgnoredMacrosUnsupported
188        | Warning::IgnoredMethodsUnsupported
189        | Warning::ItMessageNotFound
190        | Warning::LocalFunctionAmbiguous
191        | Warning::OptionDeprecated
192        | Warning::OutputInvalid
193        | Warning::ParsingFailed => false,
194        Warning::InstrumentationNonbuildable
195        | Warning::ModulePathUnknown
196        | Warning::RunTestFailed => true,
197    }
198}