relm4_macros/widgets/parse/
properties.rs

1use proc_macro2::{Literal, Punct};
2use syn::ext::IdentExt;
3use syn::parse::ParseStream;
4use syn::parse::discouraged::Speculative;
5use syn::punctuated::{Pair, Punctuated};
6use syn::token::{And, At, Caret, Colon, Dot, Gt, Lt, Or, Question, Slash, Tilde, Underscore};
7use syn::{Ident, Lifetime, Token, braced, bracketed, parenthesized, token};
8
9use crate::widgets::{ParseError, Properties, Property, PropertyName, PropertyType, parse_util};
10
11impl Properties {
12    pub(super) fn parse(input: ParseStream<'_>) -> Self {
13        let mut props: Punctuated<Property, Token![,]> = Punctuated::new();
14        loop {
15            if input.is_empty() {
16                break;
17            }
18            let parse_input = input.fork();
19            let (prop, contains_error) = Property::parse(&parse_input);
20            props.push(prop);
21
22            // Everything worked, advance input
23            if !contains_error {
24                input.advance_to(&parse_input);
25            }
26
27            if input.is_empty() {
28                break;
29            }
30
31            if let Some(prop) = parse_comma_error(input) {
32                // If there's already an error, ignore the additional comma error
33                if contains_error {
34                    // Skip to next token to start with "fresh" and hopefully correct syntax.
35                    while !parse_next_token(input).unwrap() {
36                        let next_input = input.fork();
37                        let (prop, contains_error) = Property::parse(&next_input);
38                        if !contains_error {
39                            // Point with correct syntax was found!
40                            props.push(prop);
41                            input.advance_to(&next_input);
42
43                            // Now we should definitely have a comma
44                            if let Some(prop) = parse_comma_error(input) {
45                                props.push(prop);
46                            }
47                            break;
48                        }
49                    }
50                } else {
51                    props.push(prop);
52                }
53            }
54        }
55
56        let properties = props.into_pairs().map(Pair::into_value).collect();
57        Properties { properties }
58    }
59}
60
61fn parse_comma_error(input: ParseStream<'_>) -> Option<Property> {
62    let lookahead = input.lookahead1();
63    if lookahead.peek(Token![,]) {
64        input.parse::<Token![,]>().unwrap();
65        None
66    } else {
67        let err = lookahead.error();
68        Some(Property {
69            name: PropertyName::Ident(parse_util::string_to_snake_case("comma_error")),
70            ty: PropertyType::ParseError(ParseError::Generic(err.to_compile_error())),
71        })
72    }
73}
74
75macro_rules! parse_type {
76    ($input:ident, $ty:ty) => {
77        let _: $ty = $input.parse()?;
78        return Ok(false);
79    };
80}
81
82fn skip_inner_tokens(input: ParseStream<'_>) -> Result<(), syn::Error> {
83    while !input.is_empty() {
84        parse_next_token(input)?;
85    }
86    Ok(())
87}
88
89fn parse_next_token(input: ParseStream<'_>) -> Result<bool, syn::Error> {
90    let inner_tokens;
91    if input.is_empty() {
92        Ok(true)
93    } else if input.peek(Token![,]) {
94        let _comma: Token![,] = input.parse()?;
95        Ok(true)
96    } else if input.peek(token::Paren) {
97        parenthesized!(inner_tokens in input);
98        skip_inner_tokens(&inner_tokens)?;
99        Ok(false)
100    } else if input.peek(token::Bracket) {
101        bracketed!(inner_tokens in input);
102        skip_inner_tokens(&inner_tokens)?;
103        Ok(false)
104    } else if input.peek(token::Brace) {
105        braced!(inner_tokens in input);
106        skip_inner_tokens(&inner_tokens)?;
107        Ok(false)
108    } else if Ident::parse_any(input).is_ok() {
109        Ok(false)
110    } else if input.peek(And) {
111        parse_type!(input, And);
112    } else if input.peek(At) {
113        parse_type!(input, At);
114    } else if input.peek(Colon) {
115        parse_type!(input, Colon);
116    } else if input.peek(Slash) {
117        parse_type!(input, Slash);
118    } else if input.peek(token::Eq) {
119        parse_type!(input, token::Eq);
120    } else if input.peek(Gt) {
121        parse_type!(input, Gt);
122    } else if input.peek(Lt) {
123        parse_type!(input, Lt);
124    } else if input.peek(Or) {
125        parse_type!(input, Or);
126    } else if input.peek(Tilde) {
127        parse_type!(input, Tilde);
128    } else if input.peek(Caret) {
129        parse_type!(input, Caret);
130    } else if input.peek(Underscore) {
131        parse_type!(input, Underscore);
132    } else if input.peek(Question) {
133        parse_type!(input, Question);
134    } else if input.peek(Dot) {
135        parse_type!(input, Dot);
136    } else if input.peek(Lifetime) {
137        parse_type!(input, Lifetime);
138    } else if input.parse::<Punct>().is_ok() || input.parse::<Literal>().is_ok() {
139        Ok(false)
140    } else {
141        unreachable!(
142            "Every possible token should be covered. Please report this error at Relm4! \nContext: '''{input}''' \n"
143        );
144    }
145}