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 = "
77
78This may indicate a bug in Necessist. Consider opening an issue at: \
79https://github.com/trailofbits/necessist/issues
80";
81
82bitflags! {
83    struct State: u8 {
84        const ALLOW_MSG_EMITTED = 1 << 0;
85        const BUG_MSG_EMITTED = 1 << 1;
86        const WARNING_EMITTED = 1 << 2;
87    }
88}
89
90static WARNING_STATE_MAP: Mutex<BTreeMap<Warning, State>> = Mutex::new(BTreeMap::new());
91
92#[cfg_attr(dylint_lib = "general", allow(non_local_effect_before_error_return))]
93fn warn_internal(
94    context: &LightContext,
95    warning: Warning,
96    source: Option<&dyn ToConsoleString>,
97    msg: &str,
98    flags: Flags,
99) -> Result<()> {
100    assert_ne!(warning, Warning::All);
101
102    #[allow(clippy::unwrap_used)]
103    let mut warning_state_map = WARNING_STATE_MAP.lock().unwrap();
104
105    let state = warning_state_map
106        .entry(warning)
107        .or_insert_with(State::empty);
108
109    // smoelius: Append `BUG_MSG` to `msg` in case we have to `bail!`.
110    let msg = msg.to_owned()
111        + if may_be_bug(warning) && !state.contains(State::BUG_MSG_EMITTED) {
112            state.insert(State::BUG_MSG_EMITTED);
113            BUG_MSG
114        } else {
115            ""
116        };
117
118    if context.opts.deny.contains(&Warning::All) || context.opts.deny.contains(&warning) {
119        bail!(msg);
120    }
121
122    if context.opts.quiet
123        || context.opts.allow.contains(&Warning::All)
124        || context.opts.allow.contains(&warning)
125        || (flags.contains(Flags::ONCE) && state.contains(State::WARNING_EMITTED))
126    {
127        return Ok(());
128    }
129
130    let allow_msg = if state.contains(State::ALLOW_MSG_EMITTED) {
131        String::new()
132    } else {
133        state.insert(State::ALLOW_MSG_EMITTED);
134        format!(
135            "
136Silence this warning with: --allow {warning}"
137        )
138    };
139
140    (context.println)(&format!(
141        "{}{}: {}{}",
142        source.map_or(String::new(), |source| format!(
143            "{}: ",
144            source.to_console_string()
145        )),
146        if std::io::stdout().is_terminal() {
147            Yellow.bold()
148        } else {
149            Style::default()
150        }
151        .paint("Warning"),
152        msg,
153        allow_msg
154    ));
155
156    state.insert(State::WARNING_EMITTED);
157
158    Ok(())
159}
160
161pub(crate) fn note(context: &LightContext, msg: &str) {
162    if context.opts.quiet {
163        return;
164    }
165
166    (context.println)(&format!(
167        "{}: {}",
168        if std::io::stdout().is_terminal() {
169            Green.bold()
170        } else {
171            Style::default()
172        }
173        .paint("Note"),
174        msg
175    ));
176}
177
178fn may_be_bug(warning: Warning) -> bool {
179    match warning {
180        Warning::All => unreachable!(),
181        Warning::DatabaseDoesNotExist
182        | Warning::DryRunFailed
183        | Warning::FilesChanged
184        | Warning::IgnoredFunctionsUnsupported
185        | Warning::IgnoredMacrosUnsupported
186        | Warning::IgnoredMethodsUnsupported
187        | Warning::ItMessageNotFound
188        | Warning::LocalFunctionAmbiguous
189        | Warning::OptionDeprecated
190        | Warning::OutputInvalid
191        | Warning::ParsingFailed => false,
192        Warning::InstrumentationNonbuildable
193        | Warning::ModulePathUnknown
194        | Warning::RunTestFailed => true,
195    }
196}