makers 0.8.0

a POSIX-compatible make implemented in Rust
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<Cow<Macro>>;
}

impl MacroScope for MacroSet {
    fn get(&self, name: &str) -> Option<Cow<Macro>> {
        self.get_non_recursive(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,
            })
        })
    }
}

impl<T: MacroScope> MacroScope for Option<&T> {
    fn get(&self, name: &str) -> Option<Cow<Macro>> {
        self.as_ref().and_then(|value| value.get(name))
    }
}

// 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(),
        }
    }

    pub 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
    }

    #[cfg(feature = "full")]
    pub fn is_defined(&self, name: &str) -> bool {
        self.get(name).map_or(false, |x| !x.text.is_empty())
    }

    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",
        }
    }
}