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 {
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))
}
}
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(¯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",
Some(Macro {
source: ItemSource::File { .. },
..
}) => "file",
Some(Macro {
source: ItemSource::CommandLineOrMakeflags,
..
}) => "command line",
Some(Macro {
source: ItemSource::FunctionCall,
..
}) => "automatic",
}
}
}