cairo_lang_plugins/plugins/
config.rs1use 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#[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 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
41pub enum ConfigPredicatePart<'db> {
43 Cfg(Cfg),
45 Call(ast::ExprFunctionCall<'db>),
47}
48
49#[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
96pub 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
117fn 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
165fn 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
185fn 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
200fn 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
209fn 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 = call.path(db).as_syntax_node().get_text(db);
219 let args = call
220 .arguments(db)
221 .arguments(db)
222 .elements(db)
223 .map(|arg| AttributeArg::from_ast(arg, db))
224 .collect_vec();
225
226 match operator {
227 "not" => {
228 if args.len() != 1 {
229 diagnostics.push(PluginDiagnostic::error(
230 call.stable_ptr(db),
231 "`not` operator expects exactly one argument.".into(),
232 ));
233 None
234 } else {
235 Some(PredicateTree::Not(Box::new(parse_predicate_item(
236 db,
237 args[0].clone(),
238 diagnostics,
239 )?)))
240 }
241 }
242 "and" => {
243 if args.len() < 2 {
244 diagnostics.push(PluginDiagnostic::error(
245 call.stable_ptr(db),
246 "`and` operator expects at least two arguments.".into(),
247 ));
248 None
249 } else {
250 Some(PredicateTree::And(parse_predicate_items(db, args, diagnostics)?))
251 }
252 }
253 "or" => {
254 if args.len() < 2 {
255 diagnostics.push(PluginDiagnostic::error(
256 call.stable_ptr(db),
257 "`or` operator expects at least two arguments.".into(),
258 ));
259 None
260 } else {
261 Some(PredicateTree::Or(parse_predicate_items(db, args, diagnostics)?))
262 }
263 }
264 _ => {
265 diagnostics.push(PluginDiagnostic::error(
266 call.stable_ptr(db),
267 format!("Unsupported operator: `{operator}`."),
268 ));
269 None
270 }
271 }
272 }
273 None => {
274 diagnostics.push(PluginDiagnostic::error(
275 item.arg.stable_ptr(db).untyped(),
276 "Invalid configuration argument.".into(),
277 ));
278 None
279 }
280 }
281}
282
283fn parse_predicate_items<'a>(
286 db: &'a dyn Database,
287 args: Vec<AttributeArg<'a>>,
288 diagnostics: &mut Vec<PluginDiagnostic<'a>>,
289) -> Option<Vec<PredicateTree>> {
290 let args = args.into_iter();
291 let expected_size = args.len();
292 let mut items = Vec::with_capacity(expected_size);
293 for arg in args {
294 items.extend(parse_predicate_item(db, arg, diagnostics));
295 }
296 (items.len() == expected_size).then_some(items)
297}
298
299fn extract_config_predicate_part<'a>(
301 db: &dyn Database,
302 arg: &AttributeArg<'a>,
303) -> Option<ConfigPredicatePart<'a>> {
304 match &arg.variant {
305 AttributeArgVariant::Unnamed(ast::Expr::Path(path)) => {
306 if let Ok(ast::PathSegment::Simple(segment)) =
307 path.segments(db).elements(db).exactly_one()
308 {
309 Some(ConfigPredicatePart::Cfg(Cfg::name(segment.identifier(db).to_string(db))))
310 } else {
311 None
312 }
313 }
314 AttributeArgVariant::Unnamed(ast::Expr::FunctionCall(call)) => {
315 Some(ConfigPredicatePart::Call(call.clone()))
316 }
317 AttributeArgVariant::Named { name, value } => {
318 let value_text = match value {
319 ast::Expr::String(terminal) => terminal.string_value(db).unwrap_or_default(),
320 ast::Expr::ShortString(terminal) => terminal.string_value(db).unwrap_or_default(),
321 _ => return None,
322 };
323
324 Some(ConfigPredicatePart::Cfg(Cfg::kv(name.text.to_string(db), value_text)))
325 }
326 _ => None,
327 }
328}