diff options
Diffstat (limited to 'src/makefile/macro_scope.rs')
-rw-r--r-- | src/makefile/macro_scope.rs | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/src/makefile/macro_scope.rs b/src/makefile/macro_scope.rs new file mode 100644 index 0000000..045d6f7 --- /dev/null +++ b/src/makefile/macro_scope.rs @@ -0,0 +1,211 @@ +use super::eval_context::DeferredEvalContext; +use super::functions; +use super::token::Token; +use super::{ItemSource, LookupInternal, Macro, MacroSet, TokenString}; +use eyre::Context; +use lazy_static::lazy_static; +use std::borrow::Cow; +use std::collections::HashSet; +use std::io::BufRead; +use std::iter; +use std::sync::RwLock; + +pub trait MacroScope { + /// Looks up the macro with the given name and returns it if it exists. + /// + /// Uses [Cow] to allow for lazy macro definitions. + fn get(&self, name: &str) -> Option<Cow<Macro>>; +} + +impl MacroScope for MacroSet { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.get(name).map(Cow::Borrowed) + } +} + +impl<'a> MacroScope for LookupInternal<'a> { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.lookup(name).ok().map(|value| { + Cow::Owned(Macro { + source: ItemSource::Builtin, + text: TokenString::text(value), + #[cfg(feature = "full")] + eagerly_expanded: false, + }) + }) + } +} + +// warning on undefined macros is useful but can get repetitive fast +lazy_static! { + static ref WARNINGS_EMITTED: RwLock<HashSet<String>> = Default::default(); +} + +fn warn(text: String) { + let already_warned = WARNINGS_EMITTED + .read() + .map_or(true, |warnings| warnings.contains(&text)); + if !already_warned { + log::warn!("{}", &text); + if let Ok(mut warnings) = WARNINGS_EMITTED.write() { + warnings.insert(text); + } + } +} + +#[derive(Default)] +pub struct MacroScopeStack<'a> { + scopes: Vec<&'a dyn MacroScope>, +} + +impl<'a> MacroScopeStack<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn from_scope(scope: &'a dyn MacroScope) -> Self { + Self { + scopes: vec![scope], + } + } + + pub fn with_scope(&self, new_scope: &'a dyn MacroScope) -> Self { + Self { + scopes: iter::once(new_scope).chain(self.scopes.clone()).collect(), + } + } + + fn get(&self, name: &str) -> Option<Cow<Macro>> { + for scope in &self.scopes { + if let Some(r#macro) = scope.get(name) { + return Some(r#macro); + } + } + None + } + + pub fn expand<#[cfg(feature = "full")] R: BufRead>( + &self, + text: &TokenString, + #[cfg(feature = "full")] mut eval_context: Option<&mut DeferredEvalContext<R>>, + ) -> eyre::Result<String> { + let mut result = String::new(); + for token in text.tokens() { + match token { + Token::Text(t) => result.push_str(t), + Token::MacroExpansion { name, replacement } => { + let name = self + .expand( + name, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + ) + .wrap_err_with(|| format!("while expanding \"{}\"", name))?; + let macro_value = self.get(&name).map_or_else( + || { + warn(format!("undefined macro {}", name)); + Ok(String::new()) + }, + |x| { + self.expand( + &x.text, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + ) + .wrap_err_with(|| format!("while expanding \"{}\"", &x.text)) + }, + )?; + let macro_value = match replacement { + Some((subst1, subst2)) => { + let subst1 = self.expand( + subst1, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + )?; + #[cfg(feature = "full")] + { + let (subst1, subst2) = if subst1.contains('%') { + (subst1, subst2.clone()) + } else { + let mut real_subst2 = TokenString::text("%"); + real_subst2.extend(subst2.clone()); + (format!("%{}", subst1), real_subst2) + }; + let args = [ + TokenString::text(subst1), + subst2, + TokenString::text(macro_value), + ]; + functions::expand_call( + "patsubst", + &args, + self, + eval_context.as_deref_mut(), + )? + } + #[cfg(not(feature = "full"))] + { + let subst1_suffix = regex::escape(&subst1); + let subst1_suffix = + Regex::new(&format!(r"{}(\s|$)", subst1_suffix)) + .context("formed invalid regex somehow")?; + let subst2 = self.expand(subst2)?; + subst1_suffix + .replace_all(¯o_value, |c: ®ex::Captures| { + format!("{}{}", subst2, c.get(1).unwrap().as_str()) + }) + .to_string() + } + } + None => macro_value, + }; + log::trace!( + "expanded {} (from {:?}) into \"{}\"", + token, + self.get(&name).map(|x| x.source.clone()), + ¯o_value + ); + result.push_str(¯o_value); + } + #[cfg(feature = "full")] + Token::FunctionCall { name, args } => { + let name = self.expand(name, eval_context.as_deref_mut())?; + let fn_result = + functions::expand_call(&name, args, self, eval_context.as_deref_mut())?; + log::trace!("expanded {} into \"{}\"", token, &fn_result); + result.push_str(&fn_result); + } + } + } + Ok(result) + } + + #[cfg(feature = "full")] + pub fn origin(&self, name: &str) -> &'static str { + match self.get(name).as_deref() { + None => "undefined", + Some(Macro { + source: ItemSource::Builtin, + .. + }) => "default", + Some(Macro { + source: ItemSource::Environment, + .. + }) => "environment", + // TODO figure out when to return "environment override" + Some(Macro { + source: ItemSource::File { .. }, + .. + }) => "file", + Some(Macro { + source: ItemSource::CommandLineOrMakeflags, + .. + }) => "command line", + // TODO handle override + Some(Macro { + source: ItemSource::FunctionCall, + .. + }) => "automatic", + } + } +} |