Skip to main content

cairo_lang_plugins/plugins/
config.rs

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