Skip to main content

bern_syntax/
items.rs

1//! Item-level declarations: protocol modules, value types, session state
2//! and components. The macros and the collector parse them identically.
3
4use crate::ast::*;
5use crate::expr::{FullExpr, TrBody};
6use proc_macro2::{Span, TokenStream};
7use syn::spanned::Spanned;
8use syn::{Attribute, Ident, Item, ItemFn, ItemMod, Meta};
9
10/// Arguments of `#[bern::protocol(namespace = "…", version = N)]`.
11#[derive(Clone, Debug)]
12pub struct ProtocolArgs {
13    pub namespace: String,
14    pub version: u32,
15    pub span: Span,
16}
17
18#[derive(Clone, Debug)]
19pub struct FieldItem {
20    pub name: Ident,
21    pub ty: TypeRef,
22    pub syn_ty: syn::Type,
23    pub doc: String,
24    pub initial: Option<Expr>,
25    /// `#[content]`: text that is data, shown as it is.
26    pub content: bool,
27    pub span: Span,
28}
29
30#[derive(Clone, Debug)]
31pub struct VariantItem {
32    pub name: Ident,
33    pub fields: Vec<FieldItem>,
34    pub label: Option<Tr>,
35    pub doc: String,
36    pub span: Span,
37}
38
39#[derive(Clone, Debug)]
40pub enum ValueKind {
41    Enum(Vec<VariantItem>),
42    Record(Vec<FieldItem>),
43}
44
45/// An enum or record that may cross a Bern boundary.
46#[derive(Clone, Debug)]
47pub struct ValueItem {
48    pub name: Ident,
49    pub kind: ValueKind,
50    pub doc: String,
51    /// `#[bern::value(pages = "main")]`: the enum selecting the pages of a
52    /// page slot, which modules extend with their pages.
53    pub pages: Option<String>,
54    pub span: Span,
55}
56
57#[derive(Clone, Debug)]
58pub struct StateItem {
59    pub name: Ident,
60    pub fields: Vec<FieldItem>,
61    pub span: Span,
62}
63
64#[derive(Clone, Debug)]
65pub struct CommandParam {
66    pub name: Ident,
67    pub ty: TypeRef,
68    pub syn_ty: syn::Type,
69}
70
71#[derive(Clone, Debug)]
72pub struct CommandItem {
73    pub name: Ident,
74    pub params: Vec<CommandParam>,
75    pub result: TypeRef,
76    pub result_ty: syn::Type,
77    pub doc: String,
78    pub errors: Vec<String>,
79    pub idempotent: bool,
80    /// `#[on(module::event)]`: the event this command answers, as
81    /// `(module, event)`.
82    pub on: Option<(Ident, Ident)>,
83    pub span: Span,
84}
85
86/// An event a module emits: `fn opened(&self, path: String);`
87#[derive(Clone, Debug)]
88pub struct EventItem {
89    pub name: Ident,
90    pub params: Vec<CommandParam>,
91    pub doc: String,
92    pub span: Span,
93}
94
95#[derive(Clone, Debug)]
96pub struct EventsItem {
97    pub name: Ident,
98    pub methods: Vec<EventItem>,
99    pub span: Span,
100}
101
102#[derive(Clone, Debug)]
103pub struct CommandsItem {
104    pub name: Ident,
105    pub methods: Vec<CommandItem>,
106    pub span: Span,
107}
108
109/// A read-only query: `fn search(&self, text: String, answer: Answer<Vec<Item>>);`
110#[derive(Clone, Debug)]
111pub struct QueryItem {
112    pub name: Ident,
113    pub params: Vec<CommandParam>,
114    /// `T` of the final `Answer<T>` parameter.
115    pub result: TypeRef,
116    pub result_ty: syn::Type,
117    pub doc: String,
118    pub errors: Vec<String>,
119    /// State fields the answer depends on, from `#[reads(…)]`.
120    pub reads: Vec<Ident>,
121    pub span: Span,
122}
123
124#[derive(Clone, Debug)]
125pub struct QueriesItem {
126    pub name: Ident,
127    pub methods: Vec<QueryItem>,
128    pub span: Span,
129}
130
131/// A `#[bern::protocol]` module: value types, one state struct, one
132/// command trait and an optional query trait.
133#[derive(Clone, Debug)]
134pub struct ProtocolItem {
135    pub args: ProtocolArgs,
136    pub module: ItemMod,
137    pub doc: String,
138    pub values: Vec<ValueItem>,
139    pub state: StateItem,
140    pub commands: CommandsItem,
141    pub queries: Option<QueriesItem>,
142    pub events: Option<EventsItem>,
143    pub uses: Vec<syn::ItemUse>,
144}
145
146/// A `#[bern::session]` struct: typed drafts owned by a UI session.
147#[derive(Clone, Debug)]
148pub struct SessionItem {
149    pub name: Ident,
150    pub fields: Vec<FieldItem>,
151    pub doc: String,
152    pub span: Span,
153}
154
155/// One application player in a `#[bern::players]` struct.
156#[derive(Clone, Debug)]
157pub struct PlayerField {
158    pub name: Ident,
159    pub doc: String,
160    pub span: Span,
161}
162
163/// A `#[bern::players]` struct: media players owned by the application,
164/// shared by every window and addressed by their field names.
165#[derive(Clone, Debug)]
166pub struct PlayersItem {
167    pub name: Ident,
168    pub players: Vec<PlayerField>,
169    pub doc: String,
170    pub span: Span,
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub enum ParamKind {
175    /// Passed by the caller.
176    Prop,
177    /// Owned session state, scoped to the hosting window.
178    Session,
179}
180
181#[derive(Clone, Debug)]
182pub struct Param {
183    pub name: Ident,
184    pub ty: TypeRef,
185    pub syn_ty: syn::Type,
186    pub kind: ParamKind,
187    /// `#[content]`: text the component shows as data.
188    pub content: bool,
189    pub span: Span,
190}
191
192/// A `#[component]` function whose body is a single `ui!` declaration.
193#[derive(Clone, Debug)]
194pub struct ComponentItem {
195    pub func: ItemFn,
196    pub name: Ident,
197    pub params: Vec<Param>,
198    pub body: UiBody,
199    /// `#[component(features = X)]`: the contract this component scopes.
200    pub features: Option<syn::Path>,
201    pub doc: String,
202    pub span: Span,
203}
204
205/// A command a feature is implemented by: a protocol command such as
206/// `feature`, or a standard media command such as `media.play`.
207#[derive(Clone, Debug)]
208pub struct CommandRef {
209    pub name: String,
210    pub span: Span,
211}
212
213/// One variant of a `#[bern::features]` enum.
214#[derive(Clone, Debug)]
215pub struct FeatureItem {
216    pub name: Ident,
217    pub doc: String,
218    pub commands: Vec<CommandRef>,
219    /// `#[layouts(…)]`; empty for every size.
220    pub layouts: Vec<Ident>,
221    pub guaranteed: Option<syn::LitStr>,
222    /// `#[exempt(platform, "reason")]`.
223    pub exemptions: Vec<(Ident, syn::LitStr)>,
224    pub span: Span,
225}
226
227/// A `#[bern::features]` enum: a feature contract's features.
228#[derive(Clone, Debug)]
229pub struct FeaturesItem {
230    pub name: Ident,
231    pub features: Vec<FeatureItem>,
232    pub doc: String,
233    pub span: Span,
234}
235
236/// Every Bern declaration found in one source file, with module paths
237/// relative to the file's own module.
238#[derive(Clone, Debug, Default)]
239pub struct SourceItems {
240    pub protocols: Vec<(Vec<String>, ProtocolItem)>,
241    pub values: Vec<(Vec<String>, ValueItem)>,
242    pub sessions: Vec<(Vec<String>, SessionItem)>,
243    pub players: Vec<(Vec<String>, PlayersItem)>,
244    pub components: Vec<(Vec<String>, ComponentItem)>,
245    pub features: Vec<(Vec<String>, FeaturesItem)>,
246    pub styles: Vec<(Vec<String>, crate::StyleItem)>,
247}
248
249fn error(span: Span, message: impl std::fmt::Display) -> syn::Error {
250    syn::Error::new(span, message)
251}
252
253/// True for `#[name]` and `#[bern::name]`.
254pub fn is_bern_attr(attr: &Attribute, name: &str) -> bool {
255    let path = attr.path();
256    let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
257    match segments.as_slice() {
258        [only] => only == name,
259        [first, second] => first == "bern" && second == name,
260        _ => false,
261    }
262}
263
264fn find_attr<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
265    attrs.iter().find(|attr| is_bern_attr(attr, name))
266}
267
268pub fn doc_comment(attrs: &[Attribute]) -> String {
269    let mut lines = Vec::new();
270    for attr in attrs {
271        if attr.path().is_ident("doc")
272            && let Meta::NameValue(value) = &attr.meta
273            && let syn::Expr::Lit(syn::ExprLit {
274                lit: syn::Lit::Str(text),
275                ..
276            }) = &value.value
277        {
278            lines.push(text.value().trim().to_string());
279        }
280    }
281    lines.join("\n").trim().to_string()
282}
283
284fn reject_cfg(attrs: &[Attribute], what: &str) -> syn::Result<()> {
285    for attr in attrs {
286        if attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr") {
287            return Err(error(
288                attr.span(),
289                format!(
290                    "conditional compilation on {what} is not supported in this version; \
291                     the collector cannot match `cfg` conditions reliably"
292                ),
293            ));
294        }
295    }
296    Ok(())
297}
298
299/// Map a Rust type to Bern's type vocabulary.
300pub fn type_ref(ty: &syn::Type) -> syn::Result<TypeRef> {
301    match ty {
302        syn::Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(TypeRef::Unit),
303        syn::Type::Paren(inner) => type_ref(&inner.elem),
304        syn::Type::Path(path) if path.qself.is_none() => {
305            let last = path.path.segments.last().unwrap();
306            let name = last.ident.to_string();
307            let single = path.path.segments.len() == 1;
308            let generic = |index: usize| -> syn::Result<&syn::Type> {
309                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
310                    let types: Vec<_> = args
311                        .args
312                        .iter()
313                        .filter_map(|arg| match arg {
314                            syn::GenericArgument::Type(ty) => Some(ty),
315                            _ => None,
316                        })
317                        .collect();
318                    if let Some(ty) = types.get(index) {
319                        return Ok(ty);
320                    }
321                }
322                Err(error(
323                    last.span(),
324                    format!("`{name}` needs a type argument"),
325                ))
326            };
327            match name.as_str() {
328                "bool" if single => Ok(TypeRef::Bool),
329                "i64" if single => Ok(TypeRef::Int),
330                "f64" if single => Ok(TypeRef::Float),
331                "String" => Ok(TypeRef::Text),
332                "Option" => Ok(TypeRef::Optional(Box::new(type_ref(generic(0)?)?))),
333                "Vec" => Ok(TypeRef::List(Box::new(type_ref(generic(0)?)?))),
334                "i8" | "i16" | "i32" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
335                | "usize" => Err(error(
336                    last.span(),
337                    format!("`{name}` cannot cross a Bern boundary; use `i64`"),
338                )),
339                "f32" => Err(error(
340                    last.span(),
341                    "`f32` cannot cross a Bern boundary; use `f64`",
342                )),
343                "char" | "str" => Err(error(last.span(), "use `String` for text")),
344                "Box" | "Rc" | "Arc" | "HashMap" | "BTreeMap" | "HashSet" => Err(error(
345                    last.span(),
346                    format!(
347                        "`{name}` cannot cross a Bern boundary; the first version supports \
348                         bool, i64, f64, String, Option, Vec, enums and records"
349                    ),
350                )),
351                _ => {
352                    if !matches!(last.arguments, syn::PathArguments::None) {
353                        return Err(error(
354                            last.span(),
355                            "generic types cannot cross a Bern boundary",
356                        ));
357                    }
358                    Ok(TypeRef::Named(
359                        path.path
360                            .segments
361                            .iter()
362                            .map(|segment| segment.ident.to_string())
363                            .collect(),
364                    ))
365                }
366            }
367        }
368        syn::Type::Reference(_) => Err(error(
369            ty.span(),
370            "references cannot cross a Bern boundary; use owned values",
371        )),
372        _ => Err(error(
373            ty.span(),
374            "this type cannot cross a Bern boundary; the first version supports bool, i64, \
375             f64, String, Option, Vec, enums and records",
376        )),
377    }
378}
379
380fn parse_initial(attrs: &[Attribute]) -> syn::Result<Option<Expr>> {
381    match find_attr(attrs, "initial") {
382        Some(attr) => {
383            let tokens = attr.meta.require_list()?.tokens.clone();
384            Ok(Some(syn::parse2::<FullExpr>(tokens)?.0))
385        }
386        None => Ok(None),
387    }
388}
389
390fn parse_label(attrs: &[Attribute]) -> syn::Result<Option<Tr>> {
391    match find_attr(attrs, "label") {
392        Some(attr) => {
393            let tokens = attr.meta.require_list()?.tokens.clone();
394            let expr = syn::parse2::<FullExpr>(tokens.clone());
395            match expr {
396                Ok(FullExpr(Expr::Tr(tr))) => Ok(Some(tr)),
397                _ => {
398                    // Also accept the bare form `#[label("en" => "…")]`.
399                    let tr = syn::parse2::<TrBody>(tokens).map_err(|_| {
400                        error(attr.span(), "expected `#[label(tr! { \"en\" => \"…\" })]`")
401                    })?;
402                    Ok(Some(tr.0))
403                }
404            }
405        }
406        None => Ok(None),
407    }
408}
409
410fn check_known_attrs(attrs: &[Attribute], allowed: &[&str], what: &str) -> syn::Result<()> {
411    for attr in attrs {
412        let path = attr.path();
413        if path.is_ident("doc") || path.is_ident("allow") || path.is_ident("expect") {
414            continue;
415        }
416        if allowed.iter().any(|name| is_bern_attr(attr, name)) {
417            continue;
418        }
419        if path.is_ident("derive") {
420            return Err(error(
421                attr.span(),
422                format!("{what} derive their traits from Bern; remove `#[derive]`"),
423            ));
424        }
425        if path.is_ident("cfg") || path.is_ident("cfg_attr") {
426            reject_cfg(std::slice::from_ref(attr), what)?;
427        }
428        return Err(error(
429            attr.span(),
430            format!("attribute is not supported on {what}"),
431        ));
432    }
433    Ok(())
434}
435
436fn parse_named_fields(
437    fields: &syn::Fields,
438    owner: &Ident,
439    allow_initial: bool,
440    require_pub: bool,
441) -> syn::Result<Vec<FieldItem>> {
442    let named = match fields {
443        syn::Fields::Named(named) => named,
444        syn::Fields::Unit => return Ok(Vec::new()),
445        syn::Fields::Unnamed(_) => {
446            return Err(error(
447                owner.span(),
448                format!("`{owner}` must use named fields"),
449            ));
450        }
451    };
452    let mut result = Vec::new();
453    for field in &named.named {
454        let name = field.ident.clone().unwrap();
455        // Drafts are what the user typed, shown as it is; engine text says
456        // with `#[content]` when it is data rather than a sentence.
457        let allowed: &[&str] = if allow_initial {
458            &["initial"]
459        } else {
460            &["content"]
461        };
462        check_known_attrs(&field.attrs, allowed, "fields")?;
463        if require_pub && !matches!(field.vis, syn::Visibility::Public(_)) {
464            return Err(error(
465                name.span(),
466                format!("field `{name}` must be `pub` so every entry can read it"),
467            ));
468        }
469        result.push(FieldItem {
470            ty: type_ref(&field.ty)?,
471            syn_ty: field.ty.clone(),
472            doc: doc_comment(&field.attrs),
473            initial: if allow_initial {
474                parse_initial(&field.attrs)?
475            } else {
476                None
477            },
478            content: !allow_initial && find_attr(&field.attrs, "content").is_some(),
479            span: name.span(),
480            name,
481        });
482    }
483    Ok(result)
484}
485
486fn reject_generics(generics: &syn::Generics, name: &Ident) -> syn::Result<()> {
487    if !generics.params.is_empty() || generics.where_clause.is_some() {
488        return Err(error(
489            name.span(),
490            format!("`{name}` cannot be generic; Bern types have one schema"),
491        ));
492    }
493    Ok(())
494}
495
496/// The arguments of a `#[bern::value(…)]` marker: only `pages = "slot"`.
497pub fn parse_value_args(tokens: proc_macro2::TokenStream) -> syn::Result<Option<String>> {
498    if tokens.is_empty() {
499        return Ok(None);
500    }
501    let meta: syn::MetaNameValue = syn::parse2(tokens)?;
502    if !meta.path.is_ident("pages") {
503        return Err(error(
504            meta.path.span(),
505            "`value` takes only `pages = \"slot\"`",
506        ));
507    }
508    match &meta.value {
509        syn::Expr::Lit(syn::ExprLit {
510            lit: syn::Lit::Str(slot),
511            ..
512        }) if crate::valid_id(&slot.value()) => Ok(Some(slot.value())),
513        other => Err(error(
514            other.span(),
515            "`pages` names a slot in lower snake case, e.g. `pages = \"main\"`",
516        )),
517    }
518}
519
520/// The `pages` argument of the `#[bern::value(…)]` marker, if the caller
521/// left the marker in place.
522fn value_pages(attrs: &[Attribute]) -> syn::Result<Option<String>> {
523    for attr in attrs {
524        if is_bern_attr(attr, "value")
525            && let syn::Meta::List(list) = &attr.meta
526        {
527            return parse_value_args(list.tokens.clone());
528        }
529    }
530    Ok(None)
531}
532
533/// Parse an enum or struct marked as a Bern value type. The marker
534/// attribute may still be present; its `pages` argument is read.
535pub fn parse_value(item: &Item) -> syn::Result<ValueItem> {
536    match item {
537        Item::Enum(item) => {
538            reject_generics(&item.generics, &item.ident)?;
539            check_known_attrs(&item.attrs, &["value"], "value types")?;
540            let mut variants = Vec::new();
541            for variant in &item.variants {
542                check_known_attrs(&variant.attrs, &["label"], "enum variants")?;
543                if variant.discriminant.is_some() {
544                    return Err(error(
545                        variant.ident.span(),
546                        "explicit discriminants are not supported",
547                    ));
548                }
549                if matches!(variant.fields, syn::Fields::Unnamed(_)) {
550                    return Err(error(
551                        variant.ident.span(),
552                        "use named fields `Variant { field: Type }` instead of tuple variants",
553                    ));
554                }
555                variants.push(VariantItem {
556                    name: variant.ident.clone(),
557                    fields: parse_named_fields(&variant.fields, &variant.ident, false, false)?,
558                    label: parse_label(&variant.attrs)?,
559                    doc: doc_comment(&variant.attrs),
560                    span: variant.ident.span(),
561                });
562            }
563            if variants.is_empty() {
564                return Err(error(
565                    item.ident.span(),
566                    "an enum needs at least one variant",
567                ));
568            }
569            Ok(ValueItem {
570                name: item.ident.clone(),
571                kind: ValueKind::Enum(variants),
572                doc: doc_comment(&item.attrs),
573                pages: value_pages(&item.attrs)?,
574                span: item.ident.span(),
575            })
576        }
577        Item::Struct(item) => {
578            reject_generics(&item.generics, &item.ident)?;
579            check_known_attrs(&item.attrs, &["value"], "value types")?;
580            if value_pages(&item.attrs)?.is_some() {
581                return Err(error(
582                    item.ident.span(),
583                    "`pages` marks the enum selecting a slot's pages; a struct cannot",
584                ));
585            }
586            Ok(ValueItem {
587                name: item.ident.clone(),
588                pages: None,
589                kind: ValueKind::Record(parse_named_fields(
590                    &item.fields,
591                    &item.ident,
592                    false,
593                    true,
594                )?),
595                doc: doc_comment(&item.attrs),
596                span: item.ident.span(),
597            })
598        }
599        other => Err(error(
600            other.span(),
601            "`#[bern::value]` applies to enums and structs",
602        )),
603    }
604}
605
606/// Parse a `#[bern::session]` struct (marker attribute already removed).
607pub fn parse_session(item: &Item) -> syn::Result<SessionItem> {
608    let Item::Struct(item) = item else {
609        return Err(error(item.span(), "`#[bern::session]` applies to structs"));
610    };
611    reject_generics(&item.generics, &item.ident)?;
612    check_known_attrs(&item.attrs, &["session"], "session state")?;
613    let fields = parse_named_fields(&item.fields, &item.ident, true, true)?;
614    if fields.is_empty() {
615        return Err(error(
616            item.ident.span(),
617            "session state needs at least one field",
618        ));
619    }
620    Ok(SessionItem {
621        name: item.ident.clone(),
622        fields,
623        doc: doc_comment(&item.attrs),
624        span: item.ident.span(),
625    })
626}
627
628/// Parse a `#[bern::players]` struct (marker attribute already removed).
629/// Every field is a `Player`; its name is the stable `player_id`.
630pub fn parse_players(item: &Item) -> syn::Result<PlayersItem> {
631    let Item::Struct(item) = item else {
632        return Err(error(item.span(), "`#[bern::players]` applies to structs"));
633    };
634    reject_generics(&item.generics, &item.ident)?;
635    check_known_attrs(&item.attrs, &["players"], "player catalogs")?;
636    let syn::Fields::Named(fields) = &item.fields else {
637        return Err(error(
638            item.ident.span(),
639            "declare players as named fields: `pub preview: Player`",
640        ));
641    };
642    let mut players = Vec::new();
643    for field in &fields.named {
644        let name = field.ident.clone().unwrap();
645        check_known_attrs(&field.attrs, &[], "players")?;
646        if !matches!(field.vis, syn::Visibility::Public(_)) {
647            return Err(error(name.span(), format!("player `{name}` must be `pub`")));
648        }
649        let is_player = matches!(&field.ty, syn::Type::Path(path)
650        if path.qself.is_none()
651            && path.path.segments.last().is_some_and(|segment| {
652                segment.ident == "Player" && segment.arguments.is_none()
653            }));
654        if !is_player {
655            return Err(error(
656                field.ty.span(),
657                "every field of a player catalog has the type `Player`",
658            ));
659        }
660        if !crate::valid_id(&name.to_string()) {
661            return Err(error(
662                name.span(),
663                "player names are lower snake case; they become stable player ids",
664            ));
665        }
666        players.push(PlayerField {
667            doc: doc_comment(&field.attrs),
668            span: name.span(),
669            name,
670        });
671    }
672    if players.is_empty() {
673        return Err(error(item.ident.span(), "declare at least one player"));
674    }
675    Ok(PlayersItem {
676        name: item.ident.clone(),
677        players,
678        doc: doc_comment(&item.attrs),
679        span: item.ident.span(),
680    })
681}
682
683/// Parse protocol attribute arguments.
684pub fn parse_protocol_args(args: TokenStream, span: Span) -> syn::Result<ProtocolArgs> {
685    let mut namespace = None;
686    let mut version = None;
687    let parser = syn::meta::parser(|meta| {
688        if meta.path.is_ident("namespace") {
689            let value: syn::LitStr = meta.value()?.parse()?;
690            namespace = Some(value.value());
691            Ok(())
692        } else if meta.path.is_ident("version") {
693            let value: syn::LitInt = meta.value()?.parse()?;
694            version = Some(value.base10_parse()?);
695            Ok(())
696        } else {
697            Err(meta.error("expected `namespace = \"…\"` or `version = N`"))
698        }
699    });
700    syn::parse::Parser::parse2(parser, args)?;
701    Ok(ProtocolArgs {
702        namespace: namespace
703            .ok_or_else(|| error(span, "the protocol needs `namespace = \"…\"`"))?,
704        version: version.ok_or_else(|| error(span, "the protocol needs `version = N`"))?,
705        span,
706    })
707}
708
709fn parse_command(method: &syn::TraitItemFn) -> syn::Result<CommandItem> {
710    let sig = &method.sig;
711    let name = sig.ident.clone();
712    check_known_attrs(&method.attrs, &["errors", "idempotent", "on"], "commands")?;
713    if method.default.is_some() {
714        return Err(error(
715            name.span(),
716            "commands are implemented by the engine, not in the protocol",
717        ));
718    }
719    if sig.asyncness.is_some() || !sig.generics.params.is_empty() {
720        return Err(error(
721            name.span(),
722            "commands cannot be async or generic in this version",
723        ));
724    }
725    let mut inputs = sig.inputs.iter();
726    match inputs.next() {
727        Some(syn::FnArg::Receiver(receiver))
728            if receiver.reference.is_some() && receiver.mutability.is_some() => {}
729        _ => {
730            return Err(error(
731                name.span(),
732                "a command takes `&mut self` as its first parameter",
733            ));
734        }
735    }
736    let mut params = Vec::new();
737    for input in inputs {
738        let syn::FnArg::Typed(typed) = input else {
739            unreachable!()
740        };
741        let syn::Pat::Ident(pat) = &*typed.pat else {
742            return Err(error(typed.pat.span(), "parameters must be simple names"));
743        };
744        params.push(CommandParam {
745            name: pat.ident.clone(),
746            ty: type_ref(&typed.ty)?,
747            syn_ty: (*typed.ty).clone(),
748        });
749    }
750    let result_ty = match &sig.output {
751        syn::ReturnType::Type(_, ty) => match &**ty {
752            syn::Type::Path(path)
753                if path.path.segments.last().map(|s| s.ident == "Result") == Some(true) =>
754            {
755                let last = path.path.segments.last().unwrap();
756                let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
757                    return Err(error(ty.span(), "expected `Result<T, CommandError>`"));
758                };
759                let types: Vec<_> = args
760                    .args
761                    .iter()
762                    .filter_map(|arg| match arg {
763                        syn::GenericArgument::Type(ty) => Some(ty.clone()),
764                        _ => None,
765                    })
766                    .collect();
767                match types.as_slice() {
768                    [ok, syn::Type::Path(err)]
769                        if err.path.segments.last().map(|s| s.ident == "CommandError")
770                            == Some(true) =>
771                    {
772                        ok.clone()
773                    }
774                    _ => return Err(error(ty.span(), "expected `Result<T, CommandError>`")),
775                }
776            }
777            _ => return Err(error(ty.span(), "expected `Result<T, CommandError>`")),
778        },
779        syn::ReturnType::Default => {
780            return Err(error(
781                name.span(),
782                "commands return `Result<T, CommandError>` so failures reach every entry",
783            ));
784        }
785    };
786    let mut errors = Vec::new();
787    if let Some(attr) = find_attr(&method.attrs, "errors") {
788        let list = attr.parse_args_with(
789            syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated,
790        )?;
791        errors = list.iter().map(syn::LitStr::value).collect();
792    }
793    let on = match find_attr(&method.attrs, "on") {
794        Some(attr) => {
795            let path: syn::Path = attr.parse_args()?;
796            match path.segments.iter().collect::<Vec<_>>().as_slice() {
797                [module, event] => Some((module.ident.clone(), event.ident.clone())),
798                _ => {
799                    return Err(error(
800                        path.span(),
801                        "name the event as `#[on(module::event)]`",
802                    ));
803                }
804            }
805        }
806        None => None,
807    };
808    Ok(CommandItem {
809        result: type_ref(&result_ty)?,
810        result_ty,
811        params,
812        doc: doc_comment(&method.attrs),
813        errors,
814        idempotent: find_attr(&method.attrs, "idempotent").is_some(),
815        on,
816        span: name.span(),
817        name,
818    })
819}
820
821/// `T` when `ty` is `Answer<T>`.
822fn answer_type(ty: &syn::Type) -> Option<&syn::Type> {
823    let syn::Type::Path(path) = ty else {
824        return None;
825    };
826    let last = path.path.segments.last()?;
827    if last.ident != "Answer" {
828        return None;
829    }
830    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
831        return None;
832    };
833    match args.args.iter().collect::<Vec<_>>().as_slice() {
834        [syn::GenericArgument::Type(inner)] => Some(inner),
835        _ => None,
836    }
837}
838
839fn parse_query(method: &syn::TraitItemFn) -> syn::Result<QueryItem> {
840    let sig = &method.sig;
841    let name = sig.ident.clone();
842    check_known_attrs(&method.attrs, &["errors", "reads"], "queries")?;
843    if method.default.is_some() {
844        return Err(error(
845            name.span(),
846            "queries are answered by the engine, not in the protocol",
847        ));
848    }
849    if sig.asyncness.is_some() || !sig.generics.params.is_empty() {
850        return Err(error(
851            name.span(),
852            "queries are not async or generic; answer later through `answer: Answer<T>`",
853        ));
854    }
855    let mut inputs = sig.inputs.iter();
856    match inputs.next() {
857        Some(syn::FnArg::Receiver(receiver))
858            if receiver.reference.is_some() && receiver.mutability.is_none() => {}
859        Some(syn::FnArg::Receiver(receiver)) if receiver.mutability.is_some() => {
860            return Err(error(
861                receiver.span(),
862                "a query takes `&self`: queries only read engine state, commands change it",
863            ));
864        }
865        _ => {
866            return Err(error(
867                name.span(),
868                "a query takes `&self` as its first parameter",
869            ));
870        }
871    }
872    let typed: Vec<&syn::PatType> = inputs
873        .map(|input| match input {
874            syn::FnArg::Typed(typed) => typed,
875            syn::FnArg::Receiver(_) => unreachable!(),
876        })
877        .collect();
878    let Some((answer, rest)) = typed.split_last() else {
879        return Err(error(
880            name.span(),
881            "a query answers through its last parameter, `answer: Answer<T>`",
882        ));
883    };
884    let Some(result_ty) = answer_type(&answer.ty) else {
885        return Err(error(
886            answer.ty.span(),
887            "a query answers through its last parameter, `answer: Answer<T>`",
888        ));
889    };
890    let mut params = Vec::new();
891    for typed in rest {
892        let syn::Pat::Ident(pat) = &*typed.pat else {
893            return Err(error(typed.pat.span(), "parameters must be simple names"));
894        };
895        if answer_type(&typed.ty).is_some() {
896            return Err(error(
897                typed.ty.span(),
898                "a query has one `Answer<T>`, as its last parameter",
899            ));
900        }
901        params.push(CommandParam {
902            name: pat.ident.clone(),
903            ty: type_ref(&typed.ty)?,
904            syn_ty: (*typed.ty).clone(),
905        });
906    }
907    if let syn::ReturnType::Type(_, ty) = &sig.output {
908        return Err(error(
909            ty.span(),
910            "a query returns nothing; it answers through `answer`, now or later",
911        ));
912    }
913    let mut errors = Vec::new();
914    if let Some(attr) = find_attr(&method.attrs, "errors") {
915        let list = attr.parse_args_with(
916            syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated,
917        )?;
918        errors = list.iter().map(syn::LitStr::value).collect();
919    }
920    let mut reads = Vec::new();
921    if let Some(attr) = find_attr(&method.attrs, "reads") {
922        let list = attr.parse_args_with(
923            syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated,
924        )?;
925        reads = list.into_iter().collect();
926    }
927    Ok(QueryItem {
928        result: type_ref(result_ty)?,
929        result_ty: result_ty.clone(),
930        params,
931        doc: doc_comment(&method.attrs),
932        errors,
933        reads,
934        span: name.span(),
935        name,
936    })
937}
938
939fn strip_marker(item: &Item, marker: &str) -> Item {
940    let mut item = item.clone();
941    let attrs = match &mut item {
942        Item::Enum(item) => &mut item.attrs,
943        Item::Struct(item) => &mut item.attrs,
944        Item::Trait(item) => &mut item.attrs,
945        _ => return item,
946    };
947    attrs.retain(|attr| !is_bern_attr(attr, marker));
948    item
949}
950
951/// Parse a protocol module (protocol attribute already removed).
952pub fn parse_protocol(args: ProtocolArgs, module: ItemMod) -> syn::Result<ProtocolItem> {
953    let Some((_, items)) = &module.content else {
954        return Err(error(
955            module.ident.span(),
956            "the protocol module must be declared inline: `mod name { … }`",
957        ));
958    };
959    reject_cfg(&module.attrs, "protocol modules")?;
960    let mut values = Vec::new();
961    let mut state = None;
962    let mut commands = None;
963    let mut queries = None;
964    let mut events = None;
965    let mut uses = Vec::new();
966    for item in items {
967        match item {
968            Item::Use(item_use) => uses.push(item_use.clone()),
969            Item::Enum(syn::ItemEnum { attrs, .. })
970            | Item::Struct(syn::ItemStruct { attrs, .. })
971                if find_attr(attrs, "value").is_some() =>
972            {
973                values.push(parse_value(&strip_marker(item, "value"))?);
974            }
975            Item::Struct(item_struct) if find_attr(&item_struct.attrs, "state").is_some() => {
976                if state.is_some() {
977                    return Err(error(
978                        item_struct.ident.span(),
979                        "a protocol has exactly one `#[state]` struct",
980                    ));
981                }
982                let stripped = strip_marker(item, "state");
983                let Item::Struct(stripped) = stripped else {
984                    unreachable!()
985                };
986                reject_generics(&stripped.generics, &stripped.ident)?;
987                check_known_attrs(&stripped.attrs, &[], "state")?;
988                state = Some(StateItem {
989                    name: stripped.ident.clone(),
990                    fields: parse_named_fields(&stripped.fields, &stripped.ident, false, true)?,
991                    span: stripped.ident.span(),
992                });
993            }
994            Item::Trait(item_trait) if find_attr(&item_trait.attrs, "commands").is_some() => {
995                if commands.is_some() {
996                    return Err(error(
997                        item_trait.ident.span(),
998                        "a protocol has exactly one `#[commands]` trait",
999                    ));
1000                }
1001                reject_generics(&item_trait.generics, &item_trait.ident)?;
1002                let mut methods = Vec::new();
1003                for trait_item in &item_trait.items {
1004                    match trait_item {
1005                        syn::TraitItem::Fn(method) => methods.push(parse_command(method)?),
1006                        other => {
1007                            return Err(error(
1008                                other.span(),
1009                                "the command trait only contains command methods",
1010                            ));
1011                        }
1012                    }
1013                }
1014                commands = Some(CommandsItem {
1015                    name: item_trait.ident.clone(),
1016                    methods,
1017                    span: item_trait.ident.span(),
1018                });
1019            }
1020            Item::Trait(item_trait) if find_attr(&item_trait.attrs, "queries").is_some() => {
1021                if queries.is_some() {
1022                    return Err(error(
1023                        item_trait.ident.span(),
1024                        "a protocol has at most one `#[queries]` trait",
1025                    ));
1026                }
1027                reject_generics(&item_trait.generics, &item_trait.ident)?;
1028                let mut methods = Vec::new();
1029                for trait_item in &item_trait.items {
1030                    match trait_item {
1031                        syn::TraitItem::Fn(method) => methods.push(parse_query(method)?),
1032                        other => {
1033                            return Err(error(
1034                                other.span(),
1035                                "the query trait only contains query methods",
1036                            ));
1037                        }
1038                    }
1039                }
1040                queries = Some(QueriesItem {
1041                    name: item_trait.ident.clone(),
1042                    methods,
1043                    span: item_trait.ident.span(),
1044                });
1045            }
1046            Item::Trait(item_trait) if find_attr(&item_trait.attrs, "events").is_some() => {
1047                if events.is_some() {
1048                    return Err(error(
1049                        item_trait.ident.span(),
1050                        "a protocol has at most one `#[events]` trait",
1051                    ));
1052                }
1053                reject_generics(&item_trait.generics, &item_trait.ident)?;
1054                let mut methods = Vec::new();
1055                for trait_item in &item_trait.items {
1056                    match trait_item {
1057                        syn::TraitItem::Fn(method) => methods.push(parse_event(method)?),
1058                        other => {
1059                            return Err(error(
1060                                other.span(),
1061                                "the events trait only contains event methods",
1062                            ));
1063                        }
1064                    }
1065                }
1066                events = Some(EventsItem {
1067                    name: item_trait.ident.clone(),
1068                    methods,
1069                    span: item_trait.ident.span(),
1070                });
1071            }
1072            other => {
1073                return Err(error(
1074                    other.span(),
1075                    "a protocol module contains `use` items, `#[value]` types, one `#[state]` \
1076                     struct, one `#[commands]` trait, an optional `#[queries]` trait and an \
1077                     optional `#[events]` trait",
1078                ));
1079            }
1080        }
1081    }
1082    let state = state.ok_or_else(|| {
1083        error(
1084            module.ident.span(),
1085            "the protocol needs a `#[state] pub struct State { … }`",
1086        )
1087    })?;
1088    let commands = commands.ok_or_else(|| {
1089        error(
1090            module.ident.span(),
1091            "the protocol needs a `#[commands] pub trait Commands { … }`",
1092        )
1093    })?;
1094    Ok(ProtocolItem {
1095        args,
1096        doc: doc_comment(&module.attrs),
1097        module,
1098        values,
1099        state,
1100        commands,
1101        queries,
1102        events,
1103        uses,
1104    })
1105}
1106
1107/// Parse an event declaration: `fn opened(&self, path: String);`.
1108fn parse_event(method: &syn::TraitItemFn) -> syn::Result<EventItem> {
1109    let sig = &method.sig;
1110    let name = sig.ident.clone();
1111    check_known_attrs(&method.attrs, &[], "events")?;
1112    if method.default.is_some() || !matches!(sig.output, syn::ReturnType::Default) {
1113        return Err(error(
1114            name.span(),
1115            "an event is declared as `fn name(&self, …);` without a body or result",
1116        ));
1117    }
1118    let mut inputs = sig.inputs.iter();
1119    match inputs.next() {
1120        Some(syn::FnArg::Receiver(receiver))
1121            if receiver.reference.is_some() && receiver.mutability.is_none() => {}
1122        _ => {
1123            return Err(error(
1124                name.span(),
1125                "an event takes `&self` as its first parameter",
1126            ));
1127        }
1128    }
1129    let mut params = Vec::new();
1130    for input in inputs {
1131        let syn::FnArg::Typed(typed) = input else {
1132            unreachable!()
1133        };
1134        let syn::Pat::Ident(pat) = &*typed.pat else {
1135            return Err(error(typed.pat.span(), "parameters must be simple names"));
1136        };
1137        params.push(CommandParam {
1138            name: pat.ident.clone(),
1139            ty: type_ref(&typed.ty)?,
1140            syn_ty: (*typed.ty).clone(),
1141        });
1142    }
1143    Ok(EventItem {
1144        span: name.span(),
1145        doc: doc_comment(&method.attrs),
1146        params,
1147        name,
1148    })
1149}
1150
1151/// Parse a `#[component]` function (marker attribute already removed).
1152/// Parse `#[component]` arguments: nothing, or `features = Path`.
1153pub fn parse_component_args(args: TokenStream) -> syn::Result<Option<syn::Path>> {
1154    if args.is_empty() {
1155        return Ok(None);
1156    }
1157    let parser = |input: syn::parse::ParseStream| -> syn::Result<syn::Path> {
1158        let key: Ident = input.parse()?;
1159        if key != "features" {
1160            return Err(error(key.span(), "expected `features = FeaturesEnum`"));
1161        }
1162        input.parse::<syn::Token![=]>()?;
1163        let path: syn::Path = input.parse()?;
1164        if !input.is_empty() {
1165            return Err(input.error("expected only `features = FeaturesEnum`"));
1166        }
1167        Ok(path)
1168    };
1169    syn::parse::Parser::parse2(parser, args).map(Some)
1170}
1171
1172pub fn parse_component(func: ItemFn) -> syn::Result<ComponentItem> {
1173    parse_component_with(func, None)
1174}
1175
1176/// Parse a component whose `#[component(…)]` arguments were read already.
1177pub fn parse_component_with(
1178    func: ItemFn,
1179    features: Option<syn::Path>,
1180) -> syn::Result<ComponentItem> {
1181    let name = func.sig.ident.clone();
1182    reject_cfg(&func.attrs, "components")?;
1183    check_known_attrs(&func.attrs, &["component"], "components")?;
1184    if !func.sig.generics.params.is_empty() || func.sig.asyncness.is_some() {
1185        return Err(error(name.span(), "components cannot be generic or async"));
1186    }
1187    let returns_view = match &func.sig.output {
1188        syn::ReturnType::Type(_, ty) => match &**ty {
1189            syn::Type::ImplTrait(bounds) => bounds.bounds.iter().any(|bound| {
1190                matches!(bound, syn::TypeParamBound::Trait(t)
1191                    if t.path.segments.last().map(|s| s.ident == "View") == Some(true))
1192            }),
1193            syn::Type::Path(path) => {
1194                path.path.segments.last().map(|s| s.ident == "View") == Some(true)
1195            }
1196            _ => false,
1197        },
1198        syn::ReturnType::Default => false,
1199    };
1200    if !returns_view {
1201        return Err(error(name.span(), "a component returns `impl View`"));
1202    }
1203    let mut params = Vec::new();
1204    for input in &func.sig.inputs {
1205        let syn::FnArg::Typed(typed) = input else {
1206            return Err(error(input.span(), "components do not take `self`"));
1207        };
1208        let syn::Pat::Ident(pat) = &*typed.pat else {
1209            return Err(error(typed.pat.span(), "parameters must be simple names"));
1210        };
1211        let mut kind = ParamKind::Prop;
1212        let mut content = false;
1213        for attr in &typed.attrs {
1214            if is_bern_attr(attr, "session") {
1215                kind = ParamKind::Session;
1216            } else if is_bern_attr(attr, "content") {
1217                content = true;
1218            } else {
1219                return Err(error(
1220                    attr.span(),
1221                    "only `#[session]` and `#[content]` are allowed on component parameters",
1222                ));
1223            }
1224        }
1225        if content && kind == ParamKind::Session {
1226            return Err(error(
1227                pat.ident.span(),
1228                "`#[content]` marks a text parameter; session state is not one",
1229            ));
1230        }
1231        params.push(Param {
1232            name: pat.ident.clone(),
1233            ty: type_ref(&typed.ty)?,
1234            syn_ty: (*typed.ty).clone(),
1235            kind,
1236            content,
1237            span: pat.ident.span(),
1238        });
1239    }
1240    let stmts = &func.block.stmts;
1241    let tokens = match stmts.as_slice() {
1242        [syn::Stmt::Macro(stmt)] if stmt.mac.path.is_ident("ui") => stmt.mac.tokens.clone(),
1243        [syn::Stmt::Expr(syn::Expr::Macro(expr), None)] if expr.mac.path.is_ident("ui") => {
1244            expr.mac.tokens.clone()
1245        }
1246        _ => {
1247            return Err(error(
1248                func.block.span(),
1249                "a component body is exactly one `ui! { … }` declaration",
1250            ));
1251        }
1252    };
1253    let body: UiBody = syn::parse2(tokens)?;
1254    Ok(ComponentItem {
1255        doc: doc_comment(&func.attrs),
1256        span: name.span(),
1257        name,
1258        params,
1259        body,
1260        features,
1261        func,
1262    })
1263}
1264
1265fn parse_command_refs(attr: &Attribute) -> syn::Result<Vec<CommandRef>> {
1266    attr.parse_args_with(|input: syn::parse::ParseStream| {
1267        let mut refs = Vec::new();
1268        while !input.is_empty() {
1269            let first: Ident = input.parse()?;
1270            let mut name = first.to_string();
1271            if input.peek(syn::Token![.]) {
1272                input.parse::<syn::Token![.]>()?;
1273                let second: Ident = input.parse()?;
1274                name = format!("{name}.{second}");
1275            }
1276            refs.push(CommandRef {
1277                name,
1278                span: first.span(),
1279            });
1280            if input.is_empty() {
1281                break;
1282            }
1283            input.parse::<syn::Token![,]>()?;
1284        }
1285        Ok(refs)
1286    })
1287}
1288
1289/// Parse a `#[bern::features]` enum (marker attribute already removed).
1290pub fn parse_features(item: &Item) -> syn::Result<FeaturesItem> {
1291    let Item::Enum(item) = item else {
1292        return Err(error(
1293            item.span(),
1294            "`#[bern::features]` declares an enum whose variants are the features",
1295        ));
1296    };
1297    check_known_attrs(&item.attrs, &["features"], "feature contracts")?;
1298    if !item.generics.params.is_empty() {
1299        return Err(error(
1300            item.ident.span(),
1301            "feature contracts cannot be generic",
1302        ));
1303    }
1304    let mut features = Vec::new();
1305    for variant in &item.variants {
1306        if !matches!(variant.fields, syn::Fields::Unit) {
1307            return Err(error(
1308                variant.ident.span(),
1309                "features are plain variants without fields",
1310            ));
1311        }
1312        check_known_attrs(
1313            &variant.attrs,
1314            &["commands", "layouts", "guaranteed", "exempt"],
1315            "features",
1316        )?;
1317        let mut feature = FeatureItem {
1318            name: variant.ident.clone(),
1319            doc: doc_comment(&variant.attrs),
1320            commands: Vec::new(),
1321            layouts: Vec::new(),
1322            guaranteed: None,
1323            exemptions: Vec::new(),
1324            span: variant.ident.span(),
1325        };
1326        for attr in &variant.attrs {
1327            if is_bern_attr(attr, "commands") {
1328                feature.commands.extend(parse_command_refs(attr)?);
1329            } else if is_bern_attr(attr, "layouts") {
1330                let sizes = attr.parse_args_with(
1331                    syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated,
1332                )?;
1333                feature.layouts.extend(sizes);
1334            } else if is_bern_attr(attr, "guaranteed") {
1335                feature.guaranteed = Some(attr.parse_args::<syn::LitStr>()?);
1336            } else if is_bern_attr(attr, "exempt") {
1337                let (platform, reason) =
1338                    attr.parse_args_with(|input: syn::parse::ParseStream| {
1339                        let platform: Ident = input.parse()?;
1340                        input.parse::<syn::Token![,]>()?;
1341                        let reason: syn::LitStr = input.parse()?;
1342                        Ok((platform, reason))
1343                    })?;
1344                feature.exemptions.push((platform, reason));
1345            }
1346        }
1347        if feature.commands.is_empty() {
1348            return Err(error(
1349                variant.ident.span(),
1350                "a feature names the commands that implement it: `#[commands(…)]`",
1351            ));
1352        }
1353        features.push(feature);
1354    }
1355    if features.is_empty() {
1356        return Err(error(
1357            item.ident.span(),
1358            "a feature contract declares at least one feature",
1359        ));
1360    }
1361    Ok(FeaturesItem {
1362        name: item.ident.clone(),
1363        features,
1364        doc: doc_comment(&item.attrs),
1365        span: item.ident.span(),
1366    })
1367}
1368
1369fn strip_fn_marker(func: &ItemFn) -> ItemFn {
1370    let mut func = func.clone();
1371    func.attrs.retain(|attr| !is_bern_attr(attr, "component"));
1372    func
1373}
1374
1375fn collect(items: &[Item], path: &mut Vec<String>, out: &mut SourceItems) -> syn::Result<()> {
1376    for item in items {
1377        match item {
1378            Item::Mod(module) => {
1379                if let Some(attr) = find_attr(&module.attrs, "protocol") {
1380                    let args = match &attr.meta {
1381                        Meta::List(list) => list.tokens.clone(),
1382                        _ => TokenStream::new(),
1383                    };
1384                    let args = parse_protocol_args(args, attr.span())?;
1385                    let mut stripped = module.clone();
1386                    stripped
1387                        .attrs
1388                        .retain(|attr| !is_bern_attr(attr, "protocol"));
1389                    out.protocols
1390                        .push((path.clone(), parse_protocol(args, stripped)?));
1391                } else if let Some((_, content)) = &module.content {
1392                    path.push(module.ident.to_string());
1393                    collect(content, path, out)?;
1394                    path.pop();
1395                }
1396            }
1397            Item::Enum(syn::ItemEnum { attrs, .. })
1398            | Item::Struct(syn::ItemStruct { attrs, .. })
1399                if find_attr(attrs, "value").is_some() =>
1400            {
1401                reject_cfg(attrs, "value types")?;
1402                // The marker stays: it may carry `pages = "slot"`.
1403                out.values.push((path.clone(), parse_value(item)?));
1404            }
1405            Item::Struct(syn::ItemStruct { attrs, .. })
1406                if find_attr(attrs, "session").is_some() =>
1407            {
1408                reject_cfg(attrs, "session state")?;
1409                out.sessions
1410                    .push((path.clone(), parse_session(&strip_marker(item, "session"))?));
1411            }
1412            Item::Struct(syn::ItemStruct { attrs, .. })
1413                if find_attr(attrs, "players").is_some() =>
1414            {
1415                reject_cfg(attrs, "player catalogs")?;
1416                out.players
1417                    .push((path.clone(), parse_players(&strip_marker(item, "players"))?));
1418            }
1419            Item::Enum(syn::ItemEnum { attrs, .. }) if find_attr(attrs, "features").is_some() => {
1420                reject_cfg(attrs, "feature contracts")?;
1421                out.features.push((
1422                    path.clone(),
1423                    parse_features(&strip_marker(item, "features"))?,
1424                ));
1425            }
1426            Item::Macro(item)
1427                if item
1428                    .mac
1429                    .path
1430                    .segments
1431                    .last()
1432                    .is_some_and(|segment| segment.ident == "style") =>
1433            {
1434                out.styles
1435                    .push((path.clone(), crate::parse_style(item.mac.tokens.clone())?));
1436            }
1437            Item::Fn(func) if find_attr(&func.attrs, "component").is_some() => {
1438                let args = match &find_attr(&func.attrs, "component").map(|attr| &attr.meta) {
1439                    Some(Meta::List(list)) => list.tokens.clone(),
1440                    _ => TokenStream::new(),
1441                };
1442                let features = parse_component_args(args)?;
1443                out.components.push((
1444                    path.clone(),
1445                    parse_component_with(strip_fn_marker(func), features)?,
1446                ));
1447            }
1448            _ => {}
1449        }
1450    }
1451    Ok(())
1452}
1453
1454/// Collect Bern declarations from the text of one Rust source file.
1455pub fn parse_source_file(source: &str) -> syn::Result<SourceItems> {
1456    parse_source_file_with_modules(source, &crate::build::Modules::default())
1457}
1458
1459pub fn parse_source_file_with_modules(
1460    source: &str,
1461    modules: &crate::build::Modules,
1462) -> syn::Result<SourceItems> {
1463    let mut file = syn::parse_file(source)?;
1464    modules.items(&mut file.items)?;
1465    let mut out = SourceItems::default();
1466    collect(&file.items, &mut Vec::new(), &mut out)?;
1467    for (_, component) in &mut out.components {
1468        modules.component(component)?;
1469    }
1470    Ok(out)
1471}