• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 16398853116

20 Jul 2025 10:21AM UTC coverage: 26.874% (+1.1%) from 25.766%
16398853116

Pull #489

github

Xenira
test(enum): add enum testcases

Refs: #178
Pull Request #489: feat(enum): add basic enum support

118 of 299 new or added lines in 8 files covered. (39.46%)

1 existing line in 1 file now uncovered.

1115 of 4149 relevant lines covered (26.87%)

5.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.44
/crates/macros/src/enum_.rs
1
use std::convert::TryFrom;
2

3
use darling::{util::Flag, FromAttributes};
4
use itertools::Itertools;
5
use proc_macro2::TokenStream;
6
use quote::{quote, ToTokens};
7
use syn::{Fields, Ident, ItemEnum, Lit};
8

9
use crate::{
10
    helpers::get_docs,
11
    parsing::{PhpRename, RenameRule, Visibility},
12
    prelude::*,
13
};
14

15
#[derive(FromAttributes, Default, Debug)]
16
#[darling(default, attributes(php), forward_attrs(doc))]
17
struct PhpEnumAttribute {
18
    #[darling(flatten)]
19
    rename: PhpRename,
20
    #[darling(default)]
21
    allow_native_discriminants: Flag,
22
    rename_cases: Option<RenameRule>,
23
    // TODO: Implement visibility support
24
    vis: Option<Visibility>,
25
    attrs: Vec<syn::Attribute>,
26
}
27

28
#[derive(FromAttributes, Default, Debug)]
29
#[darling(default, attributes(php), forward_attrs(doc))]
30
struct PhpEnumVariantAttribute {
31
    #[darling(flatten)]
32
    rename: PhpRename,
33
    #[darling(rename = "value")]
34
    discriminant: Option<Lit>,
35
    attrs: Vec<syn::Attribute>,
36
}
37

38
pub fn parser(mut input: ItemEnum) -> Result<TokenStream> {
3✔
39
    let php_attr = PhpEnumAttribute::from_attributes(&input.attrs)?;
9✔
40
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
21✔
41

42
    let docs = get_docs(&php_attr.attrs)?;
3✔
43
    let mut cases = vec![];
44
    let mut discriminant_type = DiscriminantType::None;
45

46
    for variant in &mut input.variants {
17✔
47
        if variant.fields != Fields::Unit {
7✔
NEW
48
            bail!("Enum cases must be unit variants, found: {:?}", variant);
×
49
        }
50
        if !php_attr.allow_native_discriminants.is_present() && variant.discriminant.is_some() {
7✔
NEW
51
            bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_native_discriminants)]` attribute on the enum. To export discriminants, set the #[php(value = ...)] attribute on the enum case.");
×
52
        }
53

54
        let variant_attr = PhpEnumVariantAttribute::from_attributes(&variant.attrs)?;
7✔
55
        variant.attrs.retain(|attr| !attr.path().is_ident("php"));
27✔
56
        let docs = get_docs(&variant_attr.attrs)?;
7✔
57
        let discriminant = variant_attr
7✔
58
            .discriminant
59
            .as_ref()
60
            .map(TryInto::try_into)
61
            .transpose()?;
62

63
        if let Some(d) = &discriminant {
4✔
64
            match d {
65
                Discriminant::String(_) => {
66
                    if discriminant_type == DiscriminantType::Integer {
2✔
NEW
67
                        bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants");
×
68
                    }
69

70
                    discriminant_type = DiscriminantType::String;
71
                }
72
                Discriminant::Integer(_) => {
73
                    if discriminant_type == DiscriminantType::String {
2✔
NEW
74
                        bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants");
×
75
                    }
76

77
                    discriminant_type = DiscriminantType::Integer;
78
                }
79
            }
80
        } else if discriminant_type != DiscriminantType::None {
3✔
NEW
81
            bail!(variant => "Discriminant must be specified for all enum cases, found: {:?}", variant);
×
82
        }
83

84
        cases.push(EnumCase {
7✔
85
            ident: variant.ident.clone(),
86
            name: variant_attr.rename.rename(
87
                variant.ident.to_string(),
88
                php_attr.rename_cases.unwrap_or(RenameRule::Pascal),
89
            ),
90
            attrs: variant_attr,
91
            discriminant,
92
            docs,
93
        });
94

95
        if !cases
96
            .iter()
97
            .filter_map(|case| case.discriminant.as_ref())
24✔
98
            .all_unique()
99
        {
NEW
100
            bail!(variant => "Enum cases must have unique discriminants, found duplicates in: {:?}", cases);
×
101
        }
102
    }
103

104
    let enum_props = Enum::new(
105
        &input.ident,
106
        &php_attr,
107
        docs,
108
        cases,
109
        None, // TODO: Implement flags support
110
        discriminant_type,
111
    );
112

113
    Ok(quote! {
114
        #[allow(dead_code)]
115
        #input
116

117
        #enum_props
118
    })
119
}
120

121
#[derive(Debug)]
122
pub struct Enum<'a> {
123
    ident: &'a Ident,
124
    name: String,
125
    discriminant_type: DiscriminantType,
126
    docs: Vec<String>,
127
    cases: Vec<EnumCase>,
128
    flags: Option<String>,
129
}
130

131
impl<'a> Enum<'a> {
132
    fn new(
3✔
133
        ident: &'a Ident,
134
        attrs: &PhpEnumAttribute,
135
        docs: Vec<String>,
136
        cases: Vec<EnumCase>,
137
        flags: Option<String>,
138
        discriminant_type: DiscriminantType,
139
    ) -> Self {
140
        let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal);
18✔
141

142
        Self {
143
            ident,
144
            name,
145
            discriminant_type,
146
            docs,
147
            cases,
148
            flags,
149
        }
150
    }
151

152
    fn registered_class(&self) -> TokenStream {
3✔
153
        let ident = &self.ident;
6✔
154
        let name = &self.name;
6✔
155
        let flags = self
6✔
156
            .flags
3✔
157
            .as_ref()
158
            .map(|f| quote! { | #f })
3✔
159
            .unwrap_or_default();
160
        let flags = quote! { ::ext_php_rs::flags::ClassFlags::Enum #flags };
6✔
161
        let docs = &self.docs;
6✔
162

163
        quote! {
3✔
NEW
164
            impl ::ext_php_rs::class::RegisteredClass for #ident {
×
NEW
165
                const CLASS_NAME: &'static str = #name;
×
NEW
166
                const BUILDER_MODIFIER: ::std::option::Option<
×
NEW
167
                    fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder
×
NEW
168
                > = None;
×
NEW
169
                const EXTENDS: ::std::option::Option<
×
NEW
170
                    ::ext_php_rs::class::ClassEntryInfo
×
NEW
171
                > = None;
×
NEW
172
                const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[];
×
NEW
173
                const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags;
×
NEW
174
                const DOC_COMMENTS: &'static [&'static str] = &[
×
NEW
175
                    #(#docs,)*
×
176
                ];
177

NEW
178
                fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata<Self> {
×
NEW
179
                    static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> =
×
NEW
180
                        ::ext_php_rs::class::ClassMetadata::new();
×
NEW
181
                    &METADATA
×
182
                }
183

NEW
184
                #[inline]
×
NEW
185
                fn get_properties<'a>() -> ::std::collections::HashMap<
×
NEW
186
                    &'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>
×
NEW
187
                > {
×
NEW
188
                    ::std::collections::HashMap::new()
×
189
                }
190

NEW
191
                #[inline]
×
NEW
192
                fn method_builders() -> ::std::vec::Vec<
×
NEW
193
                    (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)
×
NEW
194
                > {
×
NEW
195
                    use ::ext_php_rs::internal::class::PhpClassImpl;
×
NEW
196
                    ::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_methods()
×
197
                }
198

NEW
199
                #[inline]
×
NEW
200
                fn constructor() -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<Self>> {
×
NEW
201
                    None
×
202
                }
203

NEW
204
                #[inline]
×
NEW
205
                fn constants() -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] {
×
NEW
206
                    use ::ext_php_rs::internal::class::PhpClassImpl;
×
NEW
207
                    ::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants()
×
208
                }
209
            }
210
        }
211
    }
212

213
    fn registered_enum(&self) -> TokenStream {
3✔
214
        let ident = &self.ident;
6✔
215
        let cases = &self.cases;
6✔
216
        let case_from_names = self.cases.iter().map(|case| {
16✔
217
            let ident = &case.ident;
14✔
218
            let name = &case.name;
14✔
219
            quote! {
7✔
NEW
220
                #name => Ok(Self::#ident)
×
221
            }
222
        });
223
        let case_to_names = self.cases.iter().map(|case| {
16✔
224
            let ident = &case.ident;
14✔
225
            let name = &case.name;
14✔
226
            quote! {
7✔
NEW
227
                Self::#ident => #name
×
228
            }
229
        });
230

231
        quote! {
3✔
NEW
232
            impl ::ext_php_rs::enum_::RegisteredEnum for #ident {
×
NEW
233
                const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[
×
NEW
234
                    #(#cases,)*
×
235
                ];
236

NEW
237
                fn from_name(name: &str) -> ::ext_php_rs::error::Result<Self> {
×
NEW
238
                    match name {
×
NEW
239
                        #(#case_from_names,)*
×
NEW
240
                        _ => Err(::ext_php_rs::error::Error::InvalidProperty),
×
241
                    }
242
                }
243

NEW
244
                fn to_name(&self) -> &'static str {
×
NEW
245
                    match self {
×
NEW
246
                        #(#case_to_names,)*
×
247
                    }
248
                }
249
            }
250
        }
251
    }
252

253
    pub fn impl_try_from(&self) -> TokenStream {
3✔
254
        if self.discriminant_type == DiscriminantType::None {
3✔
255
            return quote! {};
1✔
256
        }
257
        let discriminant_type = match self.discriminant_type {
2✔
258
            DiscriminantType::Integer => quote! { i64 },
1✔
259
            DiscriminantType::String => quote! { &str },
1✔
260
            DiscriminantType::None => unreachable!("Discriminant type should not be None here"),
261
        };
NEW
262
        let ident = &self.ident;
×
263
        let cases = self.cases.iter().map(|case| {
4✔
264
            let ident = &case.ident;
8✔
265
            match case
8✔
266
                .discriminant
8✔
267
                .as_ref()
8✔
268
                .expect("Discriminant should be set")
4✔
269
            {
270
                Discriminant::String(s) => quote! { #s => Ok(Self::#ident) },
2✔
271
                Discriminant::Integer(i) => quote! { #i => Ok(Self::#ident) },
4✔
272
            }
273
        });
274

NEW
275
        quote! {
×
NEW
276
            impl TryFrom<#discriminant_type> for #ident {
×
NEW
277
                type Error = ::ext_php_rs::error::Error;
×
278

NEW
279
                fn try_from(value: #discriminant_type) -> ::ext_php_rs::error::Result<Self> {
×
NEW
280
                    match value {
×
NEW
281
                        #(
×
NEW
282
                            #cases,
×
NEW
283
                        )*
×
NEW
284
                        _ => Err(::ext_php_rs::error::Error::InvalidProperty),
×
285
                    }
286
                }
287
            }
288
        }
289
    }
290

291
    pub fn impl_into(&self) -> TokenStream {
3✔
292
        if self.discriminant_type == DiscriminantType::None {
3✔
293
            return quote! {};
1✔
294
        }
295
        let discriminant_type = match self.discriminant_type {
2✔
296
            DiscriminantType::Integer => quote! { i64 },
1✔
297
            DiscriminantType::String => quote! { &'static str },
1✔
298
            DiscriminantType::None => unreachable!("Discriminant type should not be None here"),
299
        };
NEW
300
        let ident = &self.ident;
×
301
        let cases = self.cases.iter().map(|case| {
4✔
302
            let ident = &case.ident;
8✔
303
            match case
8✔
304
                .discriminant
8✔
305
                .as_ref()
8✔
306
                .expect("Discriminant should be set")
4✔
307
            {
308
                Discriminant::String(s) => quote! { Self::#ident => #s },
2✔
309
                Discriminant::Integer(i) => quote! { Self::#ident => #i },
4✔
310
            }
311
        });
312

NEW
313
        quote! {
×
NEW
314
            impl Into<#discriminant_type> for #ident {
×
NEW
315
                fn into(self) -> #discriminant_type {
×
NEW
316
                    match self {
×
NEW
317
                        #(
×
NEW
318
                            #cases,
×
NEW
319
                        )*
×
320
                    }
321
                }
322
            }
323
        }
324
    }
325
}
326

327
impl ToTokens for Enum<'_> {
328
    fn to_tokens(&self, tokens: &mut TokenStream) {
3✔
329
        let class = self.registered_class();
9✔
330
        let enum_impl = self.registered_enum();
9✔
331
        let impl_try_from = self.impl_try_from();
9✔
332
        let impl_into = self.impl_into();
9✔
333

334
        tokens.extend(quote! {
9✔
335
            #class
336
            #enum_impl
337
            #impl_try_from
338
            #impl_into
339
        });
340
    }
341
}
342

343
#[derive(Debug)]
344
struct EnumCase {
345
    ident: Ident,
346
    name: String,
347
    #[allow(dead_code)]
348
    attrs: PhpEnumVariantAttribute,
349
    discriminant: Option<Discriminant>,
350
    docs: Vec<String>,
351
}
352

353
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
354
enum Discriminant {
355
    String(String),
356
    Integer(i64),
357
}
358

359
impl TryFrom<&Lit> for Discriminant {
360
    type Error = syn::Error;
361

362
    fn try_from(lit: &Lit) -> Result<Self> {
4✔
363
        match lit {
4✔
364
            Lit::Str(s) => Ok(Discriminant::String(s.value())),
2✔
365
            Lit::Int(i) => i
4✔
366
                .base10_parse::<i64>()
367
                .map(Discriminant::Integer)
2✔
368
                .map_err(|_| err!(lit => "Invalid integer literal for enum case: {:?}", lit)),
2✔
NEW
369
            _ => bail!(lit => "Unsupported discriminant type: {:?}", lit),
×
370
        }
371
    }
372
}
373

374
impl ToTokens for Discriminant {
375
    fn to_tokens(&self, tokens: &mut TokenStream) {
4✔
376
        tokens.extend(match self {
12✔
377
            Discriminant::String(s) => {
2✔
378
                quote! { ::ext_php_rs::enum_::Discriminant::String(#s) }
379
            }
380
            Discriminant::Integer(i) => {
2✔
381
                quote! { ::ext_php_rs::enum_::Discriminant::Int(#i) }
2✔
382
            }
383
        });
384
    }
385
}
386

387
#[derive(Debug, Clone, PartialEq, Eq)]
388
enum DiscriminantType {
389
    None,
390
    String,
391
    Integer,
392
}
393

394
impl ToTokens for EnumCase {
395
    fn to_tokens(&self, tokens: &mut TokenStream) {
7✔
396
        let ident = &self.name;
14✔
397
        let discriminant = self
14✔
398
            .discriminant
7✔
399
            .as_ref()
400
            .map_or_else(|| quote! { None }, |v| quote! { Some(#v) });
14✔
401
        let docs = &self.docs;
14✔
402

403
        tokens.extend(quote! {
21✔
404
            ::ext_php_rs::enum_::EnumCase {
405
                name: #ident,
406
                discriminant: #discriminant,
407
                docs: &[#(#docs,)*],
408
            }
409
        });
410
    }
411
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc