hir_expand/
cfg_process.rs

1//! Processes out #[cfg] and #[cfg_attr] attributes from the input for the derive macro
2use std::iter::Peekable;
3
4use base_db::Crate;
5use cfg::{CfgAtom, CfgExpr};
6use intern::{Symbol, sym};
7use rustc_hash::FxHashSet;
8use syntax::{
9    AstNode, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, T,
10    ast::{self, Attr, HasAttrs, Meta, TokenTree, VariantList},
11};
12use tracing::{debug, warn};
13
14use crate::{MacroCallLoc, MacroDefKind, db::ExpandDatabase, proc_macro::ProcMacroKind};
15
16fn check_cfg(db: &dyn ExpandDatabase, attr: &Attr, krate: Crate) -> Option<bool> {
17    if !attr.simple_name().as_deref().map(|v| v == "cfg")? {
18        return None;
19    }
20    let cfg = parse_from_attr_token_tree(&attr.meta()?.token_tree()?)?;
21    let enabled = krate.cfg_options(db).check(&cfg) != Some(false);
22    Some(enabled)
23}
24
25fn check_cfg_attr(db: &dyn ExpandDatabase, attr: &Attr, krate: Crate) -> Option<bool> {
26    if !attr.simple_name().as_deref().map(|v| v == "cfg_attr")? {
27        return None;
28    }
29    check_cfg_attr_value(db, &attr.token_tree()?, krate)
30}
31
32pub fn check_cfg_attr_value(
33    db: &dyn ExpandDatabase,
34    attr: &TokenTree,
35    krate: Crate,
36) -> Option<bool> {
37    let cfg_expr = parse_from_attr_token_tree(attr)?;
38    let enabled = krate.cfg_options(db).check(&cfg_expr) != Some(false);
39    Some(enabled)
40}
41
42fn process_has_attrs_with_possible_comma<I: HasAttrs>(
43    db: &dyn ExpandDatabase,
44    items: impl Iterator<Item = I>,
45    krate: Crate,
46    remove: &mut FxHashSet<SyntaxElement>,
47) -> Option<()> {
48    for item in items {
49        let field_attrs = item.attrs();
50        'attrs: for attr in field_attrs {
51            if let Some(enabled) = check_cfg(db, &attr, krate) {
52                if enabled {
53                    debug!("censoring {:?}", attr.syntax());
54                    remove.insert(attr.syntax().clone().into());
55                } else {
56                    debug!("censoring {:?}", item.syntax());
57                    remove.insert(item.syntax().clone().into());
58                    // We need to remove the , as well
59                    remove_possible_comma(&item, remove);
60                    break 'attrs;
61                }
62            }
63
64            if let Some(enabled) = check_cfg_attr(db, &attr, krate) {
65                if enabled {
66                    debug!("Removing cfg_attr tokens {:?}", attr);
67                    let meta = attr.meta()?;
68                    let removes_from_cfg_attr = remove_tokens_within_cfg_attr(meta)?;
69                    remove.extend(removes_from_cfg_attr);
70                } else {
71                    debug!("censoring type cfg_attr {:?}", item.syntax());
72                    remove.insert(attr.syntax().clone().into());
73                }
74            }
75        }
76    }
77    Some(())
78}
79
80#[derive(Debug, PartialEq, Eq, Clone, Copy)]
81enum CfgExprStage {
82    /// Stripping the CFGExpr part of the attribute
83    StrippigCfgExpr,
84    /// Found the comma after the CFGExpr. Will keep all tokens until the next comma or the end of the attribute
85    FoundComma,
86    /// Everything following the attribute. This could be another attribute or the end of the attribute.
87    // FIXME: cfg_attr with multiple attributes will not be handled correctly. We will only keep the first attribute
88    // Related Issue: https://github.com/rust-lang/rust-analyzer/issues/10110
89    EverythingElse,
90}
91
92/// This function creates its own set of tokens to remove. To help prevent malformed syntax as input.
93fn remove_tokens_within_cfg_attr(meta: Meta) -> Option<FxHashSet<SyntaxElement>> {
94    let mut remove: FxHashSet<SyntaxElement> = FxHashSet::default();
95    debug!("Enabling attribute {}", meta);
96    let meta_path = meta.path()?;
97    debug!("Removing {:?}", meta_path.syntax());
98    remove.insert(meta_path.syntax().clone().into());
99
100    let meta_tt = meta.token_tree()?;
101    debug!("meta_tt {}", meta_tt);
102    let mut stage = CfgExprStage::StrippigCfgExpr;
103    for tt in meta_tt.token_trees_and_tokens() {
104        debug!("Checking {:?}. Stage: {:?}", tt, stage);
105        match (stage, tt) {
106            (CfgExprStage::StrippigCfgExpr, syntax::NodeOrToken::Node(node)) => {
107                remove.insert(node.syntax().clone().into());
108            }
109            (CfgExprStage::StrippigCfgExpr, syntax::NodeOrToken::Token(token)) => {
110                if token.kind() == T![,] {
111                    stage = CfgExprStage::FoundComma;
112                }
113                remove.insert(token.into());
114            }
115            (CfgExprStage::FoundComma, syntax::NodeOrToken::Token(token))
116                if (token.kind() == T![,] || token.kind() == T![')']) =>
117            {
118                // The end of the attribute or separator for the next attribute
119                stage = CfgExprStage::EverythingElse;
120                remove.insert(token.into());
121            }
122            (CfgExprStage::EverythingElse, syntax::NodeOrToken::Node(node)) => {
123                remove.insert(node.syntax().clone().into());
124            }
125            (CfgExprStage::EverythingElse, syntax::NodeOrToken::Token(token)) => {
126                remove.insert(token.into());
127            }
128            // This is an actual attribute
129            _ => {}
130        }
131    }
132    if stage != CfgExprStage::EverythingElse {
133        warn!("Invalid cfg_attr attribute. {:?}", meta_tt);
134        return None;
135    }
136    Some(remove)
137}
138/// Removes a possible comma after the [AstNode]
139fn remove_possible_comma(item: &impl AstNode, res: &mut FxHashSet<SyntaxElement>) {
140    if let Some(comma) = item.syntax().next_sibling_or_token().filter(|it| it.kind() == T![,]) {
141        res.insert(comma);
142    }
143}
144fn process_enum(
145    db: &dyn ExpandDatabase,
146    variants: VariantList,
147    krate: Crate,
148    remove: &mut FxHashSet<SyntaxElement>,
149) -> Option<()> {
150    'variant: for variant in variants.variants() {
151        for attr in variant.attrs() {
152            if let Some(enabled) = check_cfg(db, &attr, krate) {
153                if enabled {
154                    debug!("censoring {:?}", attr.syntax());
155                    remove.insert(attr.syntax().clone().into());
156                } else {
157                    // Rustc does not strip the attribute if it is enabled. So we will leave it
158                    debug!("censoring type {:?}", variant.syntax());
159                    remove.insert(variant.syntax().clone().into());
160                    // We need to remove the , as well
161                    remove_possible_comma(&variant, remove);
162                    continue 'variant;
163                }
164            }
165
166            if let Some(enabled) = check_cfg_attr(db, &attr, krate) {
167                if enabled {
168                    debug!("Removing cfg_attr tokens {:?}", attr);
169                    let meta = attr.meta()?;
170                    let removes_from_cfg_attr = remove_tokens_within_cfg_attr(meta)?;
171                    remove.extend(removes_from_cfg_attr);
172                } else {
173                    debug!("censoring type cfg_attr {:?}", variant.syntax());
174                    remove.insert(attr.syntax().clone().into());
175                }
176            }
177        }
178        if let Some(fields) = variant.field_list() {
179            match fields {
180                ast::FieldList::RecordFieldList(fields) => {
181                    process_has_attrs_with_possible_comma(db, fields.fields(), krate, remove)?;
182                }
183                ast::FieldList::TupleFieldList(fields) => {
184                    process_has_attrs_with_possible_comma(db, fields.fields(), krate, remove)?;
185                }
186            }
187        }
188    }
189    Some(())
190}
191
192pub(crate) fn process_cfg_attrs(
193    db: &dyn ExpandDatabase,
194    node: &SyntaxNode,
195    loc: &MacroCallLoc,
196) -> Option<FxHashSet<SyntaxElement>> {
197    // FIXME: #[cfg_eval] is not implemented. But it is not stable yet
198    let is_derive = match loc.def.kind {
199        MacroDefKind::BuiltInDerive(..)
200        | MacroDefKind::ProcMacro(_, _, ProcMacroKind::CustomDerive) => true,
201        MacroDefKind::BuiltInAttr(_, expander) => expander.is_derive(),
202        _ => false,
203    };
204    let mut remove = FxHashSet::default();
205
206    let item = ast::Item::cast(node.clone())?;
207    for attr in item.attrs() {
208        if let Some(enabled) = check_cfg_attr(db, &attr, loc.krate) {
209            if enabled {
210                debug!("Removing cfg_attr tokens {:?}", attr);
211                let meta = attr.meta()?;
212                let removes_from_cfg_attr = remove_tokens_within_cfg_attr(meta)?;
213                remove.extend(removes_from_cfg_attr);
214            } else {
215                debug!("Removing type cfg_attr {:?}", item.syntax());
216                remove.insert(attr.syntax().clone().into());
217            }
218        }
219    }
220
221    if is_derive {
222        // Only derives get their code cfg-clean, normal attribute macros process only the cfg at their level
223        // (cfg_attr is handled above, cfg is handled in the def map).
224        match item {
225            ast::Item::Struct(it) => match it.field_list()? {
226                ast::FieldList::RecordFieldList(fields) => {
227                    process_has_attrs_with_possible_comma(
228                        db,
229                        fields.fields(),
230                        loc.krate,
231                        &mut remove,
232                    )?;
233                }
234                ast::FieldList::TupleFieldList(fields) => {
235                    process_has_attrs_with_possible_comma(
236                        db,
237                        fields.fields(),
238                        loc.krate,
239                        &mut remove,
240                    )?;
241                }
242            },
243            ast::Item::Enum(it) => {
244                process_enum(db, it.variant_list()?, loc.krate, &mut remove)?;
245            }
246            ast::Item::Union(it) => {
247                process_has_attrs_with_possible_comma(
248                    db,
249                    it.record_field_list()?.fields(),
250                    loc.krate,
251                    &mut remove,
252                )?;
253            }
254            // FIXME: Implement for other items if necessary. As we do not support #[cfg_eval] yet, we do not need to implement it for now
255            _ => {}
256        }
257    }
258    Some(remove)
259}
260/// Parses a `cfg` attribute from the meta
261fn parse_from_attr_token_tree(tt: &TokenTree) -> Option<CfgExpr> {
262    let mut iter = tt
263        .token_trees_and_tokens()
264        .filter(is_not_whitespace)
265        .skip(1)
266        .take_while(is_not_closing_paren)
267        .peekable();
268    next_cfg_expr_from_syntax(&mut iter)
269}
270
271fn is_not_closing_paren(element: &NodeOrToken<ast::TokenTree, syntax::SyntaxToken>) -> bool {
272    !matches!(element, NodeOrToken::Token(token) if (token.kind() == syntax::T![')']))
273}
274fn is_not_whitespace(element: &NodeOrToken<ast::TokenTree, syntax::SyntaxToken>) -> bool {
275    !matches!(element, NodeOrToken::Token(token) if (token.kind() == SyntaxKind::WHITESPACE))
276}
277
278fn next_cfg_expr_from_syntax<I>(iter: &mut Peekable<I>) -> Option<CfgExpr>
279where
280    I: Iterator<Item = NodeOrToken<ast::TokenTree, syntax::SyntaxToken>>,
281{
282    let name = match iter.next() {
283        None => return None,
284        Some(NodeOrToken::Token(element)) => match element.kind() {
285            syntax::T![ident] => Symbol::intern(element.text()),
286            _ => return Some(CfgExpr::Invalid),
287        },
288        Some(_) => return Some(CfgExpr::Invalid),
289    };
290    let result = match &name {
291        s if [&sym::all, &sym::any, &sym::not].contains(&s) => {
292            let mut preds = Vec::new();
293            let Some(NodeOrToken::Node(tree)) = iter.next() else {
294                return Some(CfgExpr::Invalid);
295            };
296            let mut tree_iter = tree
297                .token_trees_and_tokens()
298                .filter(is_not_whitespace)
299                .skip(1)
300                .take_while(is_not_closing_paren)
301                .peekable();
302            while tree_iter.peek().is_some() {
303                let pred = next_cfg_expr_from_syntax(&mut tree_iter);
304                if let Some(pred) = pred {
305                    preds.push(pred);
306                }
307            }
308            let group = match &name {
309                s if *s == sym::all => CfgExpr::All(preds.into_boxed_slice()),
310                s if *s == sym::any => CfgExpr::Any(preds.into_boxed_slice()),
311                s if *s == sym::not => {
312                    CfgExpr::Not(Box::new(preds.pop().unwrap_or(CfgExpr::Invalid)))
313                }
314                _ => unreachable!(),
315            };
316            Some(group)
317        }
318        _ => match iter.peek() {
319            Some(NodeOrToken::Token(element)) if (element.kind() == syntax::T![=]) => {
320                iter.next();
321                match iter.next() {
322                    Some(NodeOrToken::Token(value_token))
323                        if (value_token.kind() == syntax::SyntaxKind::STRING) =>
324                    {
325                        let value = value_token.text();
326                        Some(CfgExpr::Atom(CfgAtom::KeyValue {
327                            key: name,
328                            value: Symbol::intern(value.trim_matches('"')),
329                        }))
330                    }
331                    _ => None,
332                }
333            }
334            _ => Some(CfgExpr::Atom(CfgAtom::Flag(name))),
335        },
336    };
337    if let Some(NodeOrToken::Token(element)) = iter.peek()
338        && element.kind() == syntax::T![,]
339    {
340        iter.next();
341    }
342    result
343}
344#[cfg(test)]
345mod tests {
346    use cfg::DnfExpr;
347    use expect_test::{Expect, expect};
348    use syntax::{AstNode, SourceFile, ast::Attr};
349
350    use crate::cfg_process::parse_from_attr_token_tree;
351
352    fn check_dnf_from_syntax(input: &str, expect: Expect) {
353        let parse = SourceFile::parse(input, span::Edition::CURRENT);
354        let node = match parse.tree().syntax().descendants().find_map(Attr::cast) {
355            Some(it) => it,
356            None => {
357                let node = std::any::type_name::<Attr>();
358                panic!("Failed to make ast node `{node}` from text {input}")
359            }
360        };
361        let node = node.clone_subtree();
362        assert_eq!(node.syntax().text_range().start(), 0.into());
363
364        let cfg = parse_from_attr_token_tree(&node.meta().unwrap().token_tree().unwrap()).unwrap();
365        let actual = format!("#![cfg({})]", DnfExpr::new(&cfg));
366        expect.assert_eq(&actual);
367    }
368    #[test]
369    fn cfg_from_attr() {
370        check_dnf_from_syntax(r#"#[cfg(test)]"#, expect![[r#"#![cfg(test)]"#]]);
371        check_dnf_from_syntax(r#"#[cfg(not(never))]"#, expect![[r#"#![cfg(not(never))]"#]]);
372    }
373}