makers 0.8.0

a POSIX-compatible make implemented in Rust
use std::collections::HashMap;
#[cfg(feature = "full")]
use std::collections::HashSet;
use std::env;
use std::fmt;
#[cfg(feature = "full")]
use std::io::BufRead;

#[cfg(feature = "full")]
use super::eval_context::DeferredEvalContext;
#[cfg(feature = "full")]
use super::MacroScopeStack;
use super::{ItemSource, TokenString};
#[cfg(feature = "full")]
use eyre::Result;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Macro {
    pub source: ItemSource,
    pub text: TokenString,
    #[cfg(feature = "full")]
    pub eagerly_expanded: bool,
}

#[cfg(feature = "full")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExportConfig {
    Only(HashSet<String>),
    AllBut(HashSet<String>),
}

#[cfg(feature = "full")]
impl ExportConfig {
    pub fn all_but() -> Self {
        Self::AllBut(HashSet::new())
    }

    pub fn only() -> Self {
        Self::Only(HashSet::new())
    }

    pub fn add_all<'a, I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
        match self {
            Self::Only(exported) => {
                exported.extend(iter.into_iter().map(|x| x.to_owned()));
            }
            Self::AllBut(not_exported) => {
                for added in iter {
                    not_exported.remove(added);
                }
            }
        }
    }

    pub fn remove_all<'a, I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
        match self {
            Self::Only(exported) => {
                for removed in iter {
                    exported.remove(removed);
                }
            }
            Self::AllBut(not_exported) => {
                not_exported.extend(iter.into_iter().map(|x| x.into()));
            }
        }
    }

    fn should_export(&self, x: &str) -> bool {
        match self {
            Self::Only(exported) => exported.contains(x),
            Self::AllBut(not_exported) => !not_exported.contains(x),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Set {
    pub data: HashMap<String, Macro>,
    #[cfg(feature = "full")]
    pub exported: ExportConfig,
}

impl Set {
    pub fn new() -> Self {
        Self {
            data: HashMap::new(),
            #[cfg(feature = "full")]
            exported: ExportConfig::only(),
        }
    }

    pub fn add_builtins(&mut self) {
        for (k, v) in builtins() {
            self.data.insert(
                k.into(),
                Macro {
                    source: ItemSource::Builtin,
                    text: v,
                    #[cfg(feature = "full")]
                    eagerly_expanded: false,
                },
            );
        }
    }

    pub fn add_env(&mut self) {
        for (k, v) in env::vars() {
            if k != "MAKEFLAGS" && k != "SHELL" {
                self.data.insert(
                    k,
                    Macro {
                        source: ItemSource::Environment,
                        text: TokenString::text(v),
                        #[cfg(feature = "full")]
                        eagerly_expanded: false,
                    },
                );
            }
        }
    }

    /// To properly process inherited macros, use [MacroScopeStack::get].
    pub fn get_non_recursive(&self, name: &str) -> Option<&Macro> {
        self.data.get(name)
    }

    pub fn set(&mut self, name: String, r#macro: Macro) {
        self.data.insert(name, r#macro);
    }

    pub fn extend(&mut self, other: Self) {
        #[cfg(feature = "full")]
        match (&mut self.exported, other.exported) {
            (ExportConfig::Only(se), ExportConfig::Only(oe)) => {
                se.extend(oe);
            }
            (ExportConfig::AllBut(sne), ExportConfig::AllBut(one)) => {
                sne.extend(one);
            }
            (ExportConfig::Only(se), ExportConfig::AllBut(one)) => {
                se.extend(
                    other
                        .data
                        .keys()
                        .filter(|name| !one.contains(*name))
                        .cloned(),
                );
            }
            (ExportConfig::AllBut(sne), ExportConfig::Only(oe)) => {
                sne.extend(
                    other
                        .data
                        .keys()
                        .filter(|name| !oe.contains(*name))
                        .cloned(),
                );
            }
        }
        self.data.extend(other.data);
    }

    #[cfg(feature = "full")]
    pub fn resolve_exports<R: BufRead>(
        &self,
        mut eval_context: Option<&mut DeferredEvalContext<R>>,
    ) -> Result<Vec<(&str, String)>> {
        let own_exports = self
            .data
            .iter()
            .filter(|(name, _)| self.exported.should_export(name))
            .map(|(name, value)| {
                MacroScopeStack::from_scope(self)
                    .expand(&value.text, eval_context.as_deref_mut())
                    .map(|text| (name.as_ref(), text))
            })
            .collect::<Result<Vec<_>>>()?;
        Ok(own_exports)
    }
}

impl fmt::Display for Set {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let pieces = self
            .data
            .iter()
            .map(|(k, x)| format!("{}={}", k, &x.text))
            .collect::<Vec<_>>();
        write!(f, "{}", pieces.join("\n"))
    }
}

impl Default for Set {
    fn default() -> Self {
        Self::new()
    }
}

fn builtins() -> Vec<(&'static str, TokenString)> {
    // Fuck it, might as well.
    macro_rules! handle {
        ($value:ident) => {
            stringify!($value).parse().unwrap()
        };
        ($value:literal) => {
            $value.parse().unwrap()
        };
        ($value:expr) => {
            $value
        };
    }
    macro_rules! make {
        ($($name:ident=$value:tt)+) => {vec![$(
            (stringify!($name), handle!($value))
        ),+]};
    }

    make![
        AR=ar
        YACC=yacc
        YFLAGS=""
        LEX=lex
        LFLAGS=""
        LDFLAGS=""

        AS=as
        CC=cc
        CXX="g++"
        CPP="$(CC) -E"
        FC=f77
        PC=pc
        CO=co
        GET=get
        LINT=lint
        MAKEINFO=makeinfo
        TEX=tex
        TEXI2DVI=texi2dvi
        WEAVE=weave
        CWEAVE=cweave
        TANGLE=tangle
        CTANGLE=ctangle
        RM="rm -f"

        ARFLAGS="rv"
        CFLAGS=""
        FFLAGS=""
    ]
}

#[cfg(test)]
mod test {
    use super::*;
    #[cfg(feature = "full")]
    use crate::makefile::functions::NO_EVAL;
    use crate::MacroScopeStack;
    use eyre::Result;

    type R = Result<()>;

    #[test]
    fn subst() -> R {
        let mut macros = Set::new();
        macros.set(
            "oof".to_owned(),
            Macro {
                source: ItemSource::Builtin,
                text: TokenString::text("bruh; swag; yeet;"),
                #[cfg(feature = "full")]
                eagerly_expanded: false,
            },
        );
        assert_eq!(
            MacroScopeStack::from_scope(&macros).expand(
                &"$(oof:;=?)".parse()?,
                #[cfg(feature = "full")]
                NO_EVAL
            )?,
            "bruh? swag? yeet?"
        );
        Ok(())
    }

    #[test]
    #[cfg(feature = "full")]
    fn hell_subst() -> R {
        let mut macros = Set::new();
        macros.set(
            "m".to_owned(),
            Macro {
                source: ItemSource::FunctionCall,
                text: TokenString::text("conf"),
                eagerly_expanded: false,
            },
        );
        assert_eq!(
            MacroScopeStack::from_scope(&macros).expand(&"$(m:%=%-objs)".parse()?, NO_EVAL)?,
            "conf-objs"
        );
        Ok(())
    }
}