Skip to main content

bern_syntax/
build.rs

1//! Compile-time module selection, shared by the metadata compiler and Rust macros.
2use 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    /// Resolve module cfg/cfg_attr; leave unrelated Rust cfg to its existing checker.
66    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}