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#[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#[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
60pub 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 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}