1use crate::{ComponentItem, ast::*};
3use std::collections::BTreeMap;
4use syn::{Attribute, Item, Meta, Token, punctuated::Punctuated, spanned::Spanned};
5
6#[derive(Clone, Debug, Default)]
7pub struct Modules(pub BTreeMap<String, bool>);
8
9fn retain<T>(
10 values: &mut Vec<T>,
11 mut keep: impl FnMut(&mut T) -> syn::Result<bool>,
12) -> syn::Result<()> {
13 let mut result = Vec::new();
14 for mut value in std::mem::take(values) {
15 if keep(&mut value)? {
16 result.push(value);
17 }
18 }
19 *values = result;
20 Ok(())
21}
22
23impl Modules {
24 fn cfg(&self, meta: &Meta) -> syn::Result<Option<bool>> {
25 match meta {
26 Meta::NameValue(value) if value.path.is_ident("feature") => {
27 if let syn::Expr::Lit(expr) = &value.value
28 && let syn::Lit::Str(name) = &expr.lit
29 && let Some(id) = name.value().strip_prefix("bern-module-")
30 {
31 return self.0.get(id).copied().map(Some).ok_or_else(|| {
32 syn::Error::new(name.span(), format!("unknown build module `{id}`"))
33 });
34 }
35 Ok(None)
36 }
37 Meta::List(list)
38 if list.path.is_ident("not")
39 || list.path.is_ident("all")
40 || list.path.is_ident("any") =>
41 {
42 let values =
43 list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
44 let results = values
45 .iter()
46 .map(|m| self.cfg(m))
47 .collect::<syn::Result<Vec<_>>>()?;
48 if list.path.is_ident("not") {
49 if results.len() != 1 {
50 return Err(syn::Error::new(list.span(), "not() needs one condition"));
51 }
52 Ok(results[0].map(|v| !v))
53 } else if results.iter().any(Option::is_none) {
54 Ok(None)
55 } else if list.path.is_ident("all") {
56 Ok(Some(results.iter().all(|v| v == &Some(true))))
57 } else {
58 Ok(Some(results.iter().any(|v| v == &Some(true))))
59 }
60 }
61 _ => Ok(None),
62 }
63 }
64
65 pub fn attrs(&self, attrs: &mut Vec<Attribute>) -> syn::Result<bool> {
67 let mut output = Vec::new();
68 for attr in std::mem::take(attrs) {
69 if attr.path().is_ident("cfg") {
70 match self.cfg(&attr.parse_args::<Meta>()?)? {
71 Some(false) => return Ok(false),
72 Some(true) => continue,
73 None => {}
74 }
75 } else if attr.path().is_ident("cfg_attr") {
76 let args = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
77 if let Some(condition) = args.first()
78 && let Some(enabled) = self.cfg(condition)?
79 {
80 if enabled {
81 for meta in args.iter().skip(1) {
82 output.push(syn::parse_quote!(#[#meta]));
83 }
84 }
85 continue;
86 }
87 }
88 output.push(attr);
89 }
90 *attrs = output;
91 Ok(true)
92 }
93
94 pub fn function(&self, func: &mut syn::ItemFn) -> syn::Result<()> {
95 let mut inputs = Vec::new();
96 for mut arg in std::mem::take(&mut func.sig.inputs) {
97 let attrs = match &mut arg {
98 syn::FnArg::Typed(t) => &mut t.attrs,
99 syn::FnArg::Receiver(r) => &mut r.attrs,
100 };
101 if self.attrs(attrs)? {
102 inputs.push(arg);
103 }
104 }
105 func.sig.inputs = inputs.into_iter().collect();
106 Ok(())
107 }
108
109 fn fields(&self, fields: &mut syn::Fields) -> syn::Result<()> {
110 let mut kept = Vec::new();
111 let original = match fields {
112 syn::Fields::Named(f) => std::mem::take(&mut f.named),
113 syn::Fields::Unnamed(f) => std::mem::take(&mut f.unnamed),
114 syn::Fields::Unit => return Ok(()),
115 };
116 for mut field in original {
117 if self.attrs(&mut field.attrs)? {
118 kept.push(field);
119 }
120 }
121 match fields {
122 syn::Fields::Named(f) => f.named = kept.into_iter().collect(),
123 syn::Fields::Unnamed(f) => f.unnamed = kept.into_iter().collect(),
124 _ => {}
125 }
126 Ok(())
127 }
128
129 pub fn items(&self, items: &mut Vec<Item>) -> syn::Result<()> {
130 retain(items, |item| {
131 let attrs = match item {
132 Item::Mod(i) => &mut i.attrs,
133 Item::Fn(i) => &mut i.attrs,
134 Item::Struct(i) => &mut i.attrs,
135 Item::Enum(i) => &mut i.attrs,
136 Item::Trait(i) => &mut i.attrs,
137 Item::Use(i) => &mut i.attrs,
138 _ => return Ok(true),
139 };
140 if !self.attrs(attrs)? {
141 return Ok(false);
142 }
143 match item {
144 Item::Mod(i) => {
145 if let Some((_, children)) = &mut i.content {
146 self.items(children)?;
147 }
148 }
149 Item::Fn(i) => self.function(i)?,
150 Item::Struct(i) => self.fields(&mut i.fields)?,
151 Item::Enum(i) => {
152 let mut kept = Vec::new();
153 for mut v in std::mem::take(&mut i.variants) {
154 if self.attrs(&mut v.attrs)? {
155 self.fields(&mut v.fields)?;
156 kept.push(v);
157 }
158 }
159 i.variants = kept.into_iter().collect();
160 }
161 Item::Trait(i) => retain(&mut i.items, |item| match item {
162 syn::TraitItem::Fn(f) => self.attrs(&mut f.attrs),
163 _ => Ok(true),
164 })?,
165 _ => {}
166 }
167 Ok(true)
168 })
169 }
170
171 pub fn component(&self, component: &mut ComponentItem) -> syn::Result<()> {
172 self.nodes(&mut component.body.nodes)
173 }
174
175 fn boolean(expr: &Expr) -> Option<bool> {
176 match expr {
177 Expr::Literal {
178 value: Literal::Bool(v),
179 ..
180 } => Some(*v),
181 _ => None,
182 }
183 }
184
185 fn expr(&self, expr: &mut Expr) -> syn::Result<()> {
186 let span = expr.span();
187 match expr {
188 Expr::Path(path) if path.segments.first().is_some_and(|s| s == "build") => {
189 if path.segments.len() != 3 || path.segments[1] != "modules" {
190 return Err(syn::Error::new(span, "use build.modules.<module_id>"));
191 }
192 let id = path.segments[2].to_string();
193 let value =
194 self.0.get(&id).copied().ok_or_else(|| {
195 syn::Error::new(span, format!("unknown build module `{id}`"))
196 })?;
197 *expr = Expr::Literal {
198 value: Literal::Bool(value),
199 span,
200 };
201 }
202 Expr::Unary {
203 op, expr: inner, ..
204 } => {
205 self.expr(inner)?;
206 if *op == UnaryOp::Not
207 && let Some(value) = Self::boolean(inner)
208 {
209 *expr = Expr::Literal {
210 value: Literal::Bool(!value),
211 span,
212 };
213 }
214 }
215 Expr::Binary { op, lhs, rhs, .. } => {
216 self.expr(lhs)?;
217 self.expr(rhs)?;
218 let (a, b) = (Self::boolean(lhs), Self::boolean(rhs));
219 let result = match op {
220 BinaryOp::And if a == Some(false) || b == Some(false) => Some(false),
221 BinaryOp::Or if a == Some(true) || b == Some(true) => Some(true),
222 BinaryOp::Eq if a.is_some() && b.is_some() => Some(a == b),
223 BinaryOp::Ne if a.is_some() && b.is_some() => Some(a != b),
224 _ => None,
225 };
226 if let Some(value) = result {
227 *expr = Expr::Literal {
228 value: Literal::Bool(value),
229 span,
230 };
231 } else if (*op == BinaryOp::And && a == Some(true))
232 || (*op == BinaryOp::Or && a == Some(false))
233 {
234 *expr = *rhs.clone();
235 } else if (*op == BinaryOp::And && b == Some(true))
236 || (*op == BinaryOp::Or && b == Some(false))
237 {
238 *expr = *lhs.clone();
239 }
240 }
241 Expr::If {
242 cond,
243 then,
244 otherwise,
245 ..
246 } => {
247 self.expr(cond)?;
248 if let Some(value) = Self::boolean(cond) {
249 *expr = if value {
250 *then.clone()
251 } else {
252 *otherwise.clone()
253 };
254 self.expr(expr)?;
255 } else {
256 self.expr(then)?;
257 self.expr(otherwise)?;
258 }
259 }
260 Expr::Tr(tr) => {
261 for arg in &mut tr.args {
262 self.expr(&mut arg.value)?;
263 }
264 }
265 Expr::Variant(v) => {
266 for (_, value) in &mut v.fields {
267 self.expr(value)?;
268 }
269 }
270 Expr::Query(a) => {
271 for arg in &mut a.args {
272 self.expr(arg)?;
273 }
274 }
275 _ => {}
276 }
277 Ok(())
278 }
279
280 fn props(&self, props: &mut Vec<Prop>) -> syn::Result<()> {
281 retain(props, |prop| {
282 if !self.attrs(&mut prop.attrs)? {
283 return Ok(false);
284 }
285 if !prop.attrs.is_empty() {
286 return Err(syn::Error::new(
287 prop.attrs[0].span(),
288 "UI properties accept only module cfg attributes",
289 ));
290 }
291 self.expr(&mut prop.value)?;
292 Ok(true)
293 })
294 }
295
296 fn nodes(&self, nodes: &mut Vec<Node>) -> syn::Result<()> {
297 let mut out = Vec::new();
298 for mut node in std::mem::take(nodes) {
299 match &mut node {
300 Node::Element(e) => {
301 self.props(&mut e.props)?;
302 if let Some(children) = &mut e.children {
303 self.nodes(children)?;
304 }
305 if let Some(action) = &mut e.action {
306 for arg in &mut action.args {
307 self.expr(arg)?;
308 }
309 }
310 }
311 Node::Component(c) => self.props(&mut c.args)?,
312 Node::If(i) => {
313 let mut branches = Vec::new();
314 let mut fallback = i.otherwise.take();
315 for (mut condition, mut body) in std::mem::take(&mut i.branches) {
316 self.expr(&mut condition)?;
317 match Self::boolean(&condition) {
318 Some(false) => continue,
319 Some(true) => {
320 fallback = Some(body);
321 break;
322 }
323 None => {
324 self.nodes(&mut body)?;
325 branches.push((condition, body));
326 }
327 }
328 }
329 if let Some(body) = &mut fallback {
330 self.nodes(body)?;
331 }
332 if branches.is_empty() {
333 out.extend(fallback.unwrap_or_default());
334 continue;
335 }
336 i.branches = branches;
337 i.otherwise = fallback;
338 }
339 Node::IfLet(i) => {
340 self.expr(&mut i.value)?;
341 self.nodes(&mut i.then)?;
342 if let Some(body) = &mut i.otherwise {
343 self.nodes(body)?;
344 }
345 }
346 Node::Match(m) => {
347 self.expr(&mut m.value)?;
348 retain(&mut m.arms, |arm| {
349 if !self.attrs(&mut arm.attrs)? {
350 return Ok(false);
351 }
352 if !arm.attrs.is_empty() {
353 return Err(syn::Error::new(
354 arm.span,
355 "match arms accept only module cfg attributes",
356 ));
357 }
358 self.nodes(&mut arm.body)?;
359 Ok(true)
360 })?;
361 }
362 }
363 out.push(node);
364 }
365 *nodes = out;
366 Ok(())
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 fn modules() -> Modules {
375 Modules(BTreeMap::from([
376 ("notes".into(), false),
377 ("tags".into(), true),
378 ]))
379 }
380
381 #[test]
382 fn prunes_branches_expressions_parameters_and_props_before_checks() {
383 let source = r#"
384 #[component]
385 pub fn page(#[cfg(feature = "bern-module-notes")] notes: notes::Client, show: bool) -> impl View {
386 ui! { Column {
387 if build.modules.notes && notes.open { missing(notes: notes) }
388 else if show { child(#[cfg(feature = "bern-module-notes")] notes: notes) }
389 else if build.modules.tags { Text(content: if build.modules.notes { notes.title } else { "present" }) }
390 else { unknown() }
391 match show {
392 #[cfg(feature = "bern-module-notes")] Missing::Variant => { missing() }
393 _ => { Spacer }
394 }
395 } }
396 }
397 "#;
398 let items = crate::items::parse_source_file_with_modules(source, &modules()).unwrap();
399 let component = &items.components[0].1;
400 assert_eq!(component.params.len(), 1);
401 let Node::Element(root) = &component.body.nodes[0] else {
402 panic!()
403 };
404 let children = root.children.as_ref().unwrap();
405 let Node::If(branches) = &children[0] else {
406 panic!()
407 };
408 assert_eq!(branches.branches.len(), 1);
409 let Node::Component(call) = &branches.branches[0].1[0] else {
410 panic!()
411 };
412 assert!(call.args.is_empty());
413 let Node::Element(text) = &branches.otherwise.as_ref().unwrap()[0] else {
414 panic!()
415 };
416 assert!(
417 matches!(&text.props[0].value, Expr::Literal { value: Literal::Text(v), .. } if v == "present")
418 );
419 let Node::Match(m) = &children[1] else {
420 panic!()
421 };
422 assert_eq!(m.arms.len(), 1);
423 }
424
425 #[test]
426 fn cfg_and_cfg_attr_share_protocol_selection() {
427 let mut items = syn::parse_file(
428 r#"
429 #[cfg(feature = "bern-module-notes")] fn removed() {}
430 #[cfg(not(feature = "bern-module-notes"))] fn kept() {}
431 trait Commands {
432 #[cfg_attr(feature = "bern-module-notes", on(notes::changed))]
433 fn plain();
434 #[cfg(feature = "bern-module-notes")] fn removed();
435 }
436 "#,
437 )
438 .unwrap()
439 .items;
440 modules().items(&mut items).unwrap();
441 assert_eq!(items.len(), 2);
442 let Item::Trait(t) = &items[1] else { panic!() };
443 assert_eq!(t.items.len(), 1);
444 let syn::TraitItem::Fn(f) = &t.items[0] else {
445 panic!()
446 };
447 assert!(f.attrs.is_empty());
448 }
449
450 #[test]
451 fn misspelled_build_module_is_an_error() {
452 let source = "#[component] fn page() -> impl View { ui! { if build.modules.note { Spacer } else { Spacer } } }";
453 let error = crate::items::parse_source_file_with_modules(source, &modules()).unwrap_err();
454 assert!(error.to_string().contains("unknown build module `note`"));
455 }
456}