cairo_lang_plugins/plugins/
config.rs

1use std::vec;
2
3use cairo_lang_defs::patcher::PatchBuilder;
4use cairo_lang_defs::plugin::{
5    MacroPlugin, MacroPluginMetadata, PluginDiagnostic, PluginGeneratedFile, PluginResult,
6};
7use cairo_lang_filesystem::cfg::{Cfg, CfgSet};
8use cairo_lang_syntax::attribute::structured::{
9    Attribute, AttributeArg, AttributeArgVariant, AttributeStructurize,
10};
11use cairo_lang_syntax::node::db::SyntaxGroup;
12use cairo_lang_syntax::node::helpers::{BodyItems, QueryAttrs};
13use cairo_lang_syntax::node::{Terminal, TypedStablePtr, TypedSyntaxNode, ast};
14use cairo_lang_utils::try_extract_matches;
15use itertools::Itertools;
16
17/// Represents a predicate tree used to evaluate configuration attributes to handle nested
18/// predicates, such as logical `not` operations, and evaluate them based on a given set of
19/// configuration flags (`CfgSet`).
20#[derive(Debug, Clone)]
21enum PredicateTree {
22    Cfg(Cfg),
23    Not(Box<PredicateTree>),
24    And(Vec<PredicateTree>),
25    Or(Vec<PredicateTree>),
26}
27
28impl PredicateTree {
29    /// Evaluates the predicate tree against the provided configuration set (`CfgSet`) by traversing
30    /// the `PredicateTree` and determines whether the predicate is satisfied by the given
31    /// `cfg_set`.
32    fn evaluate(&self, cfg_set: &CfgSet) -> bool {
33        match self {
34            PredicateTree::Cfg(cfg) => cfg_set.contains(cfg),
35            PredicateTree::Not(inner) => !inner.evaluate(cfg_set),
36            PredicateTree::And(predicates) => predicates.iter().all(|p| p.evaluate(cfg_set)),
37            PredicateTree::Or(predicates) => predicates.iter().any(|p| p.evaluate(cfg_set)),
38        }
39    }
40}
41
42/// Represents a part of a configuration predicate.
43pub enum ConfigPredicatePart {
44    /// A configuration item, either a key-value pair or a simple name.
45    Cfg(Cfg),
46    /// A function call in the predicate (`not`, `and`, `or`).
47    Call(ast::ExprFunctionCall),
48}
49
50/// Plugin that enables ignoring modules not involved in the current config.
51///
52/// Mostly useful for marking test modules to prevent usage of their functionality out of tests,
53/// and reduce compilation time when the tests data isn't required.
54#[derive(Debug, Default)]
55#[non_exhaustive]
56pub struct ConfigPlugin;
57
58const CFG_ATTR: &str = "cfg";
59
60impl MacroPlugin for ConfigPlugin {
61    fn generate_code(
62        &self,
63        db: &dyn SyntaxGroup,
64        item_ast: ast::ModuleItem,
65        metadata: &MacroPluginMetadata<'_>,
66    ) -> PluginResult {
67        let mut diagnostics = vec![];
68
69        if should_drop(db, metadata.cfg_set, &item_ast, &mut diagnostics) {
70            PluginResult { code: None, diagnostics, remove_original_item: true }
71        } else if let Some(builder) =
72            handle_undropped_item(db, metadata.cfg_set, item_ast, &mut diagnostics)
73        {
74            let (content, code_mappings) = builder.build();
75            PluginResult {
76                code: Some(PluginGeneratedFile {
77                    name: "config".into(),
78                    content,
79                    code_mappings,
80                    aux_data: None,
81                    diagnostics_note: Default::default(),
82                }),
83                diagnostics,
84                remove_original_item: true,
85            }
86        } else {
87            PluginResult { code: None, diagnostics, remove_original_item: false }
88        }
89    }
90
91    fn declared_attributes(&self) -> Vec<String> {
92        vec![CFG_ATTR.to_string()]
93    }
94}
95
96/// Iterator over the items that are included in the given config set, among the given items in
97/// `iterator`.
98pub struct ItemsInCfg<'a, Item: QueryAttrs> {
99    db: &'a dyn SyntaxGroup,
100    cfg_set: &'a CfgSet,
101    iterator: <Vec<Item> as IntoIterator>::IntoIter,
102}
103
104impl<Item: QueryAttrs> Iterator for ItemsInCfg<'_, Item> {
105    type Item = Item;
106
107    fn next(&mut self) -> Option<Self::Item> {
108        self.iterator.find(|item| !should_drop(self.db, self.cfg_set, item, &mut vec![]))
109    }
110}
111
112/// Extension trait for `BodyItems` filtering out items that are not included in the cfg.
113pub trait HasItemsInCfgEx<Item: QueryAttrs>: BodyItems<Item = Item> {
114    fn iter_items_in_cfg<'a>(
115        &self,
116        db: &'a dyn SyntaxGroup,
117        cfg_set: &'a CfgSet,
118    ) -> ItemsInCfg<'a, Item>;
119}
120
121impl<Item: QueryAttrs, Body: BodyItems<Item = Item>> HasItemsInCfgEx<Item> for Body {
122    fn iter_items_in_cfg<'a>(
123        &self,
124        db: &'a dyn SyntaxGroup,
125        cfg_set: &'a CfgSet,
126    ) -> ItemsInCfg<'a, Item> {
127        ItemsInCfg { db, cfg_set, iterator: self.items_vec(db).into_iter() }
128    }
129}
130
131/// Handles an item that is not dropped from the AST completely due to not matching the config.
132/// In case it includes dropped elements and needs to be rewritten, it returns the appropriate
133/// PatchBuilder. Otherwise returns `None`, and it won't be rewritten or dropped.
134fn handle_undropped_item<'a>(
135    db: &'a dyn SyntaxGroup,
136    cfg_set: &CfgSet,
137    item_ast: ast::ModuleItem,
138    diagnostics: &mut Vec<PluginDiagnostic>,
139) -> Option<PatchBuilder<'a>> {
140    match item_ast {
141        ast::ModuleItem::Trait(trait_item) => {
142            let body = try_extract_matches!(trait_item.body(db), ast::MaybeTraitBody::Some)?;
143            let items = get_kept_items_nodes(db, cfg_set, &body.items_vec(db), diagnostics)?;
144            let mut builder = PatchBuilder::new(db, &trait_item);
145            builder.add_node(trait_item.attributes(db).as_syntax_node());
146            builder.add_node(trait_item.trait_kw(db).as_syntax_node());
147            builder.add_node(trait_item.name(db).as_syntax_node());
148            builder.add_node(trait_item.generic_params(db).as_syntax_node());
149            builder.add_node(body.lbrace(db).as_syntax_node());
150            for item in items {
151                builder.add_node(item);
152            }
153            builder.add_node(body.rbrace(db).as_syntax_node());
154            Some(builder)
155        }
156        ast::ModuleItem::Impl(impl_item) => {
157            let body = try_extract_matches!(impl_item.body(db), ast::MaybeImplBody::Some)?;
158            let items = get_kept_items_nodes(db, cfg_set, &body.items_vec(db), diagnostics)?;
159            let mut builder = PatchBuilder::new(db, &impl_item);
160            builder.add_node(impl_item.attributes(db).as_syntax_node());
161            builder.add_node(impl_item.impl_kw(db).as_syntax_node());
162            builder.add_node(impl_item.name(db).as_syntax_node());
163            builder.add_node(impl_item.generic_params(db).as_syntax_node());
164            builder.add_node(impl_item.of_kw(db).as_syntax_node());
165            builder.add_node(impl_item.trait_path(db).as_syntax_node());
166            builder.add_node(body.lbrace(db).as_syntax_node());
167            for item in items {
168                builder.add_node(item);
169            }
170            builder.add_node(body.rbrace(db).as_syntax_node());
171            Some(builder)
172        }
173        _ => None,
174    }
175}
176
177/// Gets the list of items that should be kept in the AST.
178/// Returns `None` if all items should be kept.
179fn get_kept_items_nodes<Item: QueryAttrs + TypedSyntaxNode>(
180    db: &dyn SyntaxGroup,
181    cfg_set: &CfgSet,
182    all_items: &[Item],
183    diagnostics: &mut Vec<PluginDiagnostic>,
184) -> Option<Vec<cairo_lang_syntax::node::SyntaxNode>> {
185    let mut any_dropped = false;
186    let mut kept_items_nodes = vec![];
187    for item in all_items {
188        if should_drop(db, cfg_set, item, diagnostics) {
189            any_dropped = true;
190        } else {
191            kept_items_nodes.push(item.as_syntax_node());
192        }
193    }
194    if any_dropped { Some(kept_items_nodes) } else { None }
195}
196
197/// Check if the given item should be dropped from the AST.
198fn should_drop<Item: QueryAttrs>(
199    db: &dyn SyntaxGroup,
200    cfg_set: &CfgSet,
201    item: &Item,
202    diagnostics: &mut Vec<PluginDiagnostic>,
203) -> bool {
204    item.query_attr(db, CFG_ATTR).into_iter().any(|attr| {
205        match parse_predicate(db, attr.structurize(db), diagnostics) {
206            Some(predicate_tree) => !predicate_tree.evaluate(cfg_set),
207            None => false,
208        }
209    })
210}
211
212/// Parse `#[cfg(not(ghf)...)]` attribute arguments as a predicate matching [`Cfg`] items.
213fn parse_predicate(
214    db: &dyn SyntaxGroup,
215    attr: Attribute,
216    diagnostics: &mut Vec<PluginDiagnostic>,
217) -> Option<PredicateTree> {
218    Some(PredicateTree::And(
219        attr.args
220            .into_iter()
221            .filter_map(|arg| parse_predicate_item(db, arg, diagnostics))
222            .collect(),
223    ))
224}
225
226/// Parse single `#[cfg(...)]` attribute argument as a [`Cfg`] item.
227fn parse_predicate_item(
228    db: &dyn SyntaxGroup,
229    item: AttributeArg,
230    diagnostics: &mut Vec<PluginDiagnostic>,
231) -> Option<PredicateTree> {
232    match extract_config_predicate_part(db, &item) {
233        Some(ConfigPredicatePart::Cfg(cfg)) => Some(PredicateTree::Cfg(cfg)),
234        Some(ConfigPredicatePart::Call(call)) => {
235            let operator = call.path(db).as_syntax_node().get_text(db);
236            let args = call
237                .arguments(db)
238                .arguments(db)
239                .elements(db)
240                .iter()
241                .map(|arg| AttributeArg::from_ast(arg.clone(), db))
242                .collect_vec();
243
244            match operator.as_str() {
245                "not" => {
246                    if args.len() != 1 {
247                        diagnostics.push(PluginDiagnostic::error(
248                            call.stable_ptr(),
249                            "`not` operator expects exactly one argument.".into(),
250                        ));
251                        None
252                    } else {
253                        Some(PredicateTree::Not(Box::new(parse_predicate_item(
254                            db,
255                            args[0].clone(),
256                            diagnostics,
257                        )?)))
258                    }
259                }
260                "and" => {
261                    if args.len() < 2 {
262                        diagnostics.push(PluginDiagnostic::error(
263                            call.stable_ptr(),
264                            "`and` operator expects at least two arguments.".into(),
265                        ));
266                        None
267                    } else {
268                        Some(PredicateTree::And(
269                            args.into_iter()
270                                .filter_map(|arg| parse_predicate_item(db, arg, diagnostics))
271                                .collect(),
272                        ))
273                    }
274                }
275                "or" => {
276                    if args.len() < 2 {
277                        diagnostics.push(PluginDiagnostic::error(
278                            call.stable_ptr(),
279                            "`or` operator expects at least two arguments.".into(),
280                        ));
281                        None
282                    } else {
283                        Some(PredicateTree::Or(
284                            args.into_iter()
285                                .filter_map(|arg| parse_predicate_item(db, arg, diagnostics))
286                                .collect(),
287                        ))
288                    }
289                }
290                _ => {
291                    diagnostics.push(PluginDiagnostic::error(
292                        call.stable_ptr(),
293                        format!("Unsupported operator: `{}`.", operator),
294                    ));
295                    None
296                }
297            }
298        }
299        None => {
300            diagnostics.push(PluginDiagnostic::error(
301                item.arg.stable_ptr().untyped(),
302                "Invalid configuration argument.".into(),
303            ));
304            None
305        }
306    }
307}
308
309/// Extracts a configuration predicate part from an attribute argument.
310fn extract_config_predicate_part(
311    db: &dyn SyntaxGroup,
312    arg: &AttributeArg,
313) -> Option<ConfigPredicatePart> {
314    match &arg.variant {
315        AttributeArgVariant::Unnamed(ast::Expr::Path(path)) => {
316            let segments = path.elements(db);
317            if let [ast::PathSegment::Simple(segment)] = &segments[..] {
318                Some(ConfigPredicatePart::Cfg(Cfg::name(segment.ident(db).text(db).to_string())))
319            } else {
320                None
321            }
322        }
323        AttributeArgVariant::Unnamed(ast::Expr::FunctionCall(call)) => {
324            Some(ConfigPredicatePart::Call(call.clone()))
325        }
326        AttributeArgVariant::Named { name, value } => {
327            let value_text = match value {
328                ast::Expr::String(terminal) => terminal.string_value(db).unwrap_or_default(),
329                ast::Expr::ShortString(terminal) => terminal.string_value(db).unwrap_or_default(),
330                _ => return None,
331            };
332
333            Some(ConfigPredicatePart::Cfg(Cfg::kv(name.text.to_string(), value_text)))
334        }
335        _ => None,
336    }
337}