use std::path::PathBuf;
use proc_macro2::TokenStream;
use proc_macro2::TokenTree;
use quote::ToTokens;
use rustc_hash::FxHashSet;
use rustc_middle::ty::TyCtxt;
use rustc_span::FileName;
use rustc_span::def_id::LOCAL_CRATE;
use syn::Attribute;
use syn::Expr;
use syn::ExprLit;
use syn::Field;
use syn::ForeignItem;
use syn::ImplItem;
use syn::Item;
use syn::Lit;
use syn::Meta;
use syn::Stmt;
use syn::Token;
use syn::TraitItem;
use syn::Variant;
use syn::parse::ParseStream;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::visit;
use syn::visit::Visit;
use super::source_cache::SourceCache;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CfgExcludedReference {
Present,
Absent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CfgExclusion {
Excluded,
Included,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TestCfg {
Enabled,
Disabled,
}
pub(super) struct CfgExcludedReferences {
names: FxHashSet<String>,
}
impl CfgExcludedReferences {
pub(super) fn collect(tcx: TyCtxt<'_>, source_cache: &SourceCache) -> Self {
let compiled_files = compiled_files(tcx);
let active_cfg = ActiveCfg::from(tcx);
let mut names = FxHashSet::default();
for source_file in source_cache.source_files() {
let Some(parsed_file) = source_cache.parsed_file(source_file) else {
continue;
};
if !compiled_files.contains(source_file) {
collect_identifiers(&parsed_file.to_token_stream(), &mut names);
continue;
}
if source_cache
.read_source(source_file)
.is_ok_and(|source| !source.contains("cfg"))
{
continue;
}
ExcludedNameCollector {
active_cfg: &active_cfg,
names: &mut names,
}
.visit_file(parsed_file);
}
Self { names }
}
pub(super) fn reference(&self, name: &str) -> CfgExcludedReference {
if self.names.contains(name) {
CfgExcludedReference::Present
} else {
CfgExcludedReference::Absent
}
}
}
struct ActiveCfg {
atoms: FxHashSet<(String, Option<String>)>,
}
impl ActiveCfg {
fn holds(&self, name: &str, value: Option<&str>) -> bool {
self.atoms
.iter()
.any(|(atom, atom_value)| atom == name && atom_value.as_deref() == value)
}
}
impl<'tcx> From<TyCtxt<'tcx>> for ActiveCfg {
fn from(tcx: TyCtxt<'tcx>) -> Self {
let atoms = tcx
.sess
.config
.iter()
.map(|&(name, value)| (name.to_string(), value.map(|value| value.to_string())))
.collect();
Self { atoms }
}
}
struct ExcludedNameCollector<'a> {
active_cfg: &'a ActiveCfg,
names: &'a mut FxHashSet<String>,
}
impl ExcludedNameCollector<'_> {
fn visit_node<Node: ToTokens>(&mut self, node: &Node, descend: impl FnOnce(&mut Self, &Node)) {
let tokens = node.to_token_stream();
if outer_attributes(tokens.clone()).iter().any(|attribute| {
attribute_exclusion(attribute, self.active_cfg) == CfgExclusion::Excluded
}) {
collect_identifiers(&tokens, self.names);
return;
}
descend(self, node);
}
}
impl<'ast> Visit<'ast> for ExcludedNameCollector<'_> {
fn visit_item(&mut self, node: &'ast Item) {
self.visit_node(node, |collector, node| visit::visit_item(collector, node));
}
fn visit_impl_item(&mut self, node: &'ast ImplItem) {
self.visit_node(node, |collector, node| {
visit::visit_impl_item(collector, node);
});
}
fn visit_trait_item(&mut self, node: &'ast TraitItem) {
self.visit_node(node, |collector, node| {
visit::visit_trait_item(collector, node);
});
}
fn visit_foreign_item(&mut self, node: &'ast ForeignItem) {
self.visit_node(node, |collector, node| {
visit::visit_foreign_item(collector, node);
});
}
fn visit_field(&mut self, node: &'ast Field) {
self.visit_node(node, |collector, node| visit::visit_field(collector, node));
}
fn visit_variant(&mut self, node: &'ast Variant) {
self.visit_node(node, |collector, node| {
visit::visit_variant(collector, node);
});
}
fn visit_stmt(&mut self, node: &'ast Stmt) {
self.visit_node(node, |collector, node| visit::visit_stmt(collector, node));
}
}
fn compiled_files(tcx: TyCtxt<'_>) -> FxHashSet<PathBuf> {
tcx.sess
.source_map()
.files()
.iter()
.filter(|source_file| source_file.cnum == LOCAL_CRATE)
.filter_map(|source_file| {
let FileName::Real(real_file_name) = &source_file.name else {
return None;
};
real_file_name
.local_path()
.map(|path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()))
})
.collect()
}
fn outer_attributes(tokens: TokenStream) -> Vec<Attribute> {
Parser::parse2(
|input: ParseStream<'_>| {
let attributes = input.call(Attribute::parse_outer)?;
input.parse::<TokenStream>()?;
Ok(attributes)
},
tokens,
)
.unwrap_or_default()
}
fn attribute_exclusion(attribute: &Attribute, active_cfg: &ActiveCfg) -> CfgExclusion {
if !attribute.path().is_ident("cfg") {
return CfgExclusion::Included;
}
let Meta::List(list) = &attribute.meta else {
return CfgExclusion::Included;
};
let Ok(predicates) = list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
else {
return CfgExclusion::Included;
};
let Some(predicate) = predicates.first() else {
return CfgExclusion::Included;
};
if predicate_names_an_atom(predicate)
&& [TestCfg::Enabled, TestCfg::Disabled]
.into_iter()
.all(|test_cfg| predicate_holds(predicate, active_cfg, test_cfg) == Some(false))
{
return CfgExclusion::Excluded;
}
CfgExclusion::Included
}
fn predicate_names_an_atom(predicate: &Meta) -> bool {
match predicate {
Meta::Path(_) | Meta::NameValue(_) => true,
Meta::List(list) => list
.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
.is_ok_and(|nested| nested.iter().any(predicate_names_an_atom)),
}
}
fn predicate_holds(predicate: &Meta, active_cfg: &ActiveCfg, test_cfg: TestCfg) -> Option<bool> {
match predicate {
Meta::Path(path) => {
let name = path.get_ident()?.to_string();
if name == "test" {
return Some(test_cfg == TestCfg::Enabled);
}
Some(active_cfg.holds(&name, None))
},
Meta::NameValue(name_value) => {
let name = name_value.path.get_ident()?.to_string();
let Expr::Lit(ExprLit {
lit: Lit::Str(value),
..
}) = &name_value.value
else {
return None;
};
Some(active_cfg.holds(&name, Some(&value.value())))
},
Meta::List(list) => {
let combinator = list.path.get_ident()?.to_string();
let nested = list
.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
.ok()?;
match combinator.as_str() {
"all" => nested.iter().try_fold(true, |held, nested_predicate| {
Some(held && predicate_holds(nested_predicate, active_cfg, test_cfg)?)
}),
"any" => nested.iter().try_fold(false, |held, nested_predicate| {
Some(held || predicate_holds(nested_predicate, active_cfg, test_cfg)?)
}),
"not" if nested.len() == 1 => {
Some(!predicate_holds(nested.first()?, active_cfg, test_cfg)?)
},
_ => None,
}
},
}
}
fn collect_identifiers(tokens: &TokenStream, names: &mut FxHashSet<String>) {
for token in tokens.clone() {
match token {
TokenTree::Ident(identifier) => {
names.insert(identifier.to_string());
},
TokenTree::Group(group) => collect_identifiers(&group.stream(), names),
TokenTree::Punct(_) | TokenTree::Literal(_) => {},
}
}
}