cairo_lang_plugins/plugins/
config.rs1use 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#[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 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
43pub enum ConfigPredicatePart<'db> {
45 Cfg(Cfg),
47 Call(ast::ExprFunctionCall<'db>),
49}
50
51#[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
98pub 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
119fn 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
167fn 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
187fn 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
202fn 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
216fn 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
298fn 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}