use std::collections::HashSet;
use anyhow::Result;
use syn::{punctuated::Punctuated, Attribute, Meta, MetaList, MetaNameValue, Token};
#[derive(Debug, Clone)]
pub enum FeatureSet {
Unconstrained,
Specific(HashSet<String>),
}
impl FeatureSet {
pub fn unconstrained() -> Self {
Self::Unconstrained
}
pub fn from_features<I, S>(features: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self::Specific(features.into_iter().map(Into::into).collect())
}
fn feature_enabled(&self, name: &str) -> bool {
match self {
Self::Unconstrained => true,
Self::Specific(set) => set.contains(name),
}
}
}
pub fn item_is_active(attrs: &[Attribute], features: &FeatureSet) -> Result<bool> {
if matches!(features, FeatureSet::Unconstrained) {
return Ok(true);
}
for attr in attrs {
if !attr.path().is_ident("cfg") {
continue;
}
let inner: Meta = attr.parse_args()?;
if !eval(&inner, features) {
return Ok(false);
}
}
Ok(true)
}
fn eval(meta: &Meta, features: &FeatureSet) -> bool {
match meta {
Meta::NameValue(nv) => eval_name_value(nv, features),
Meta::List(list) => eval_list(list, features),
Meta::Path(_) => true,
}
}
fn eval_name_value(nv: &MetaNameValue, features: &FeatureSet) -> bool {
let key = nv
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if key != "feature" {
return true;
}
let syn::Expr::Lit(lit) = &nv.value else {
return true;
};
let syn::Lit::Str(s) = &lit.lit else {
return true;
};
features.feature_enabled(&s.value())
}
fn eval_list(list: &MetaList, features: &FeatureSet) -> bool {
let name = list
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
let inner: Punctuated<Meta, Token![,]> = match list.parse_args_with(Punctuated::parse_terminated) {
Ok(p) => p,
Err(_) => return true, };
match name.as_str() {
"not" => {
let Some(first) = inner.first() else {
return true;
};
!eval(first, features)
}
"all" => inner.iter().all(|m| eval(m, features)),
"any" => inner.iter().any(|m| eval(m, features)),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
fn fs(features: &[&str]) -> FeatureSet {
FeatureSet::from_features(features.iter().copied())
}
fn active(attrs: Vec<Attribute>, features: &FeatureSet) -> bool {
item_is_active(&attrs, features).unwrap()
}
#[test]
fn no_cfg_always_active() {
assert!(active(vec![], &fs(&[])));
assert!(active(vec![], &fs(&["server"])));
}
#[test]
fn unconstrained_includes_everything() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(feature = "cloud")])];
assert!(active(attrs, &FeatureSet::Unconstrained));
}
#[test]
fn simple_feature() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(feature = "cloud")])];
assert!(active(attrs.clone(), &fs(&["cloud"])));
assert!(!active(attrs, &fs(&["server"])));
}
#[test]
fn not_feature() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(not(feature = "cloud"))])];
assert!(!active(attrs.clone(), &fs(&["cloud"])));
assert!(active(attrs, &fs(&["server"])));
}
#[test]
fn all_features() {
let attrs: Vec<Attribute> =
vec![parse_quote!(#[cfg(all(feature = "server", feature = "cloud"))])];
assert!(active(attrs.clone(), &fs(&["server", "cloud"])));
assert!(!active(attrs.clone(), &fs(&["server"])));
assert!(!active(attrs, &fs(&["cloud"])));
}
#[test]
fn any_features() {
let attrs: Vec<Attribute> =
vec![parse_quote!(#[cfg(any(feature = "desktop", feature = "cloud"))])];
assert!(active(attrs.clone(), &fs(&["desktop"])));
assert!(active(attrs.clone(), &fs(&["cloud"])));
assert!(active(attrs.clone(), &fs(&["desktop", "cloud"])));
assert!(!active(attrs, &fs(&["server"])));
}
#[test]
fn nested_predicates() {
let attrs: Vec<Attribute> = vec![parse_quote!(
#[cfg(all(feature = "server", not(feature = "desktop")))]
)];
assert!(active(attrs.clone(), &fs(&["server"])));
assert!(!active(attrs.clone(), &fs(&["server", "desktop"])));
assert!(!active(attrs, &fs(&["desktop"])));
}
#[test]
fn multiple_cfg_attrs_anded() {
let attrs: Vec<Attribute> = vec![
parse_quote!(#[cfg(feature = "server")]),
parse_quote!(#[cfg(feature = "cloud")]),
];
assert!(active(attrs.clone(), &fs(&["server", "cloud"])));
assert!(!active(attrs, &fs(&["server"])));
}
#[test]
fn unknown_predicate_lenient() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(target_os = "linux")])];
assert!(active(attrs, &fs(&["server"])));
}
}