aboutsummaryrefslogtreecommitdiff
path: root/src/makefile/macro_scope.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/makefile/macro_scope.rs')
-rw-r--r--src/makefile/macro_scope.rs211
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(&macro_value, |c: &regex::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()),
+ &macro_value
+ );
+ result.push_str(&macro_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",
+ }
+ }
+}