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