use std::borrow::Cow; use std::collections::HashSet; #[cfg(feature = "full")] use std::io::BufRead; use std::iter; use std::sync::RwLock; use eyre::Context; use lazy_static::lazy_static; #[cfg(not(feature = "full"))] use regex::Regex; #[cfg(feature = "full")] use super::eval_context::DeferredEvalContext; #[cfg(feature = "full")] use super::functions; use super::token::Token; use super::{ItemSource, LookupInternal, Macro, MacroSet, TokenString}; 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>; } impl MacroScope for MacroSet { fn get(&self, name: &str) -> Option> { self.get(name).map(Cow::Borrowed) } } impl<'a> MacroScope for LookupInternal<'a> { fn get(&self, name: &str) -> Option> { 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> = 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> { 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>, ) -> eyre::Result { 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", } } }