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

davidcole1340 / ext-php-rs / 16824767628

08 Aug 2025 07:35AM UTC coverage: 27.748% (-0.03%) from 27.778%
16824767628

Pull #542

github

web-flow
Merge 44b890823 into 896cb945d
Pull Request #542: feat: Add constructor visability

5 of 21 new or added lines in 4 files covered. (23.81%)

1 existing line in 1 file now uncovered.

1156 of 4166 relevant lines covered (27.75%)

7.7 hits per line

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

0.0
/crates/macros/src/impl_.rs
1
use darling::util::Flag;
2
use darling::FromAttributes;
3
use proc_macro2::TokenStream;
4
use quote::quote;
5
use std::collections::{HashMap, HashSet};
6
use syn::{Expr, Ident, ItemImpl};
7

8
use crate::constant::PhpConstAttribute;
9
use crate::function::{Args, CallType, Function, MethodReceiver};
10
use crate::helpers::get_docs;
11
use crate::parsing::{PhpRename, RenameRule, Visibility};
12
use crate::prelude::*;
13

14
/// Method types.
15
#[derive(Debug)]
16
enum MethodTy {
17
    /// Regular PHP method.
18
    Normal,
19
    /// Constructor method.
20
    Constructor,
21
    /// Property getter method.
22
    Getter,
23
    /// Property setter method.
24
    Setter,
25
    /// Abstract method.
26
    Abstract,
27
}
28

29
#[derive(FromAttributes, Debug, Default)]
30
#[darling(attributes(php), default)]
31
pub struct PhpImpl {
32
    /// Rename methods to match the given rule.
33
    change_method_case: Option<RenameRule>,
34
    /// Rename constants to match the given rule.
35
    change_constant_case: Option<RenameRule>,
36
}
37

38
pub fn parser(mut input: ItemImpl) -> Result<TokenStream> {
×
39
    let args = PhpImpl::from_attributes(&input.attrs)?;
×
40
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
×
41
    let path = match &*input.self_ty {
×
42
        syn::Type::Path(ty) => &ty.path,
43
        _ => {
44
            bail!(input.self_ty => "The `#[php_impl]` attribute is only valid for struct implementations.")
×
45
        }
46
    };
47

48
    let mut parsed = ParsedImpl::new(
49
        path,
50
        args.change_method_case.unwrap_or(RenameRule::Camel),
51
        args.change_constant_case
52
            .unwrap_or(RenameRule::ScreamingSnake),
53
    );
54
    parsed.parse(input.items.iter_mut())?;
×
55

56
    let php_class_impl = parsed.generate_php_class_impl();
×
57
    Ok(quote::quote! {
58
        #input
59
        #php_class_impl
60
    })
61
}
62

63
/// Arguments applied to methods.
64
#[derive(Debug)]
65
struct MethodArgs {
66
    /// Method name. Only applies to PHP (not the Rust method name).
67
    name: String,
68
    /// The first optional argument of the function signature.
69
    optional: Option<Ident>,
70
    /// Default values for optional arguments.
71
    defaults: HashMap<Ident, Expr>,
72
    /// Visibility of the method (public, protected, private).
73
    vis: Visibility,
74
    /// Method type.
75
    ty: MethodTy,
76
}
77

78
#[derive(FromAttributes, Default, Debug)]
79
#[darling(default, attributes(php), forward_attrs(doc))]
80
pub struct PhpFunctionImplAttribute {
81
    #[darling(flatten)]
82
    rename: PhpRename,
83
    defaults: HashMap<Ident, Expr>,
84
    optional: Option<Ident>,
85
    vis: Option<Visibility>,
86
    attrs: Vec<syn::Attribute>,
87
    getter: Flag,
88
    setter: Flag,
89
    constructor: Flag,
90
    abstract_method: Flag,
91
}
92

93
impl MethodArgs {
94
    fn new(name: String, attr: PhpFunctionImplAttribute) -> Self {
×
95
        let ty = if name == "__construct" || attr.constructor.is_present() {
×
96
            MethodTy::Constructor
×
97
        } else if attr.getter.is_present() {
×
98
            MethodTy::Getter
×
99
        } else if attr.setter.is_present() {
×
100
            MethodTy::Setter
×
101
        } else if attr.abstract_method.is_present() {
×
102
            MethodTy::Abstract
×
103
        } else {
104
            MethodTy::Normal
×
105
        };
106

107
        Self {
108
            name,
109
            optional: attr.optional,
×
110
            defaults: attr.defaults,
×
111
            vis: attr.vis.unwrap_or(Visibility::Public),
×
112
            ty,
113
        }
114
    }
115
}
116

117
#[derive(Debug)]
118
struct ParsedImpl<'a> {
119
    path: &'a syn::Path,
120
    change_method_case: RenameRule,
121
    change_constant_case: RenameRule,
122
    functions: Vec<FnBuilder>,
123
    constructor: Option<(Function<'a>, Option<Visibility>)>,
124
    constants: Vec<Constant<'a>>,
125
}
126

127
#[derive(Debug, Eq, Hash, PartialEq)]
128
enum MethodModifier {
129
    Abstract,
130
    Static,
131
}
132

133
impl quote::ToTokens for MethodModifier {
134
    fn to_tokens(&self, tokens: &mut TokenStream) {
×
135
        match *self {
×
136
            Self::Abstract => quote! { ::ext_php_rs::flags::MethodFlags::Abstract },
×
137
            Self::Static => quote! { ::ext_php_rs::flags::MethodFlags::Static },
×
138
        }
139
        .to_tokens(tokens);
×
140
    }
141
}
142

143
#[derive(Debug)]
144
struct FnBuilder {
145
    /// Tokens which represent the `FunctionBuilder` for this function.
146
    pub builder: TokenStream,
147
    /// The visibility of this method.
148
    pub vis: Visibility,
149
    /// Whether this method is abstract.
150
    pub modifiers: HashSet<MethodModifier>,
151
}
152

153
#[derive(Debug)]
154
struct Constant<'a> {
155
    /// Name of the constant in PHP land.
156
    name: String,
157
    /// Identifier of the constant in Rust land.
158
    ident: &'a syn::Ident,
159
    /// Documentation for the constant.
160
    docs: Vec<String>,
161
}
162

163
impl<'a> ParsedImpl<'a> {
164
    /// Create a new, empty parsed impl block.
165
    ///
166
    /// # Parameters
167
    ///
168
    /// * `path` - Path of the type the `impl` block is for.
169
    /// * `rename_methods` - Rule to rename methods with.
170
    /// * `rename_constants` - Rule to rename constants with.
171
    fn new(path: &'a syn::Path, rename_methods: RenameRule, rename_constants: RenameRule) -> Self {
×
172
        Self {
173
            path,
174
            change_method_case: rename_methods,
175
            change_constant_case: rename_constants,
176
            functions: Vec::default(),
×
177
            constructor: Option::default(),
×
178
            constants: Vec::default(),
×
179
        }
180
    }
181

182
    /// Parses an impl block from `items`, populating `self`.
183
    fn parse(&mut self, items: impl Iterator<Item = &'a mut syn::ImplItem>) -> Result<()> {
×
184
        for items in items {
×
185
            match items {
×
186
                syn::ImplItem::Const(c) => {
×
187
                    let attr = PhpConstAttribute::from_attributes(&c.attrs)?;
×
188
                    let name = attr
×
189
                        .rename
×
190
                        .rename(c.ident.to_string(), self.change_constant_case);
×
191
                    let docs = get_docs(&attr.attrs)?;
×
192
                    c.attrs.retain(|attr| !attr.path().is_ident("php"));
×
193

194
                    self.constants.push(Constant {
×
195
                        name,
×
196
                        ident: &c.ident,
×
197
                        docs,
×
198
                    });
199
                }
200
                syn::ImplItem::Fn(method) => {
×
201
                    let attr = PhpFunctionImplAttribute::from_attributes(&method.attrs)?;
×
202
                    let name = attr
×
203
                        .rename
×
204
                        .rename_method(method.sig.ident.to_string(), self.change_method_case);
×
205
                    let docs = get_docs(&attr.attrs)?;
×
206
                    method.attrs.retain(|attr| !attr.path().is_ident("php"));
×
207

208
                    let opts = MethodArgs::new(name, attr);
×
209
                    let args = Args::parse_from_fnargs(method.sig.inputs.iter(), opts.defaults)?;
×
210
                    let mut func = Function::new(&method.sig, opts.name, args, opts.optional, docs);
×
211

212
                    let mut modifiers: HashSet<MethodModifier> = HashSet::new();
×
213

214
                    if matches!(opts.ty, MethodTy::Constructor) {
×
NEW
215
                        if self.constructor.replace((func, opts.vis.into())).is_some() {
×
216
                            bail!(method => "Only one constructor can be provided per class.");
×
217
                        }
218
                    } else {
219
                        let call_type = CallType::Method {
220
                            class: self.path,
×
221
                            receiver: if func.args.receiver.is_some() {
×
222
                                // `&self` or `&mut self`
223
                                MethodReceiver::Class
224
                            } else if func
225
                                .args
226
                                .typed
227
                                .first()
228
                                .is_some_and(|arg| arg.name == "self_")
229
                            {
230
                                // `self_: &[mut] ZendClassObject<Self>`
231
                                // Need to remove arg from argument list
232
                                func.args.typed.remove(0);
233
                                MethodReceiver::ZendClassObject
234
                            } else {
235
                                modifiers.insert(MethodModifier::Static);
236
                                // Static method
237
                                MethodReceiver::Static
238
                            },
239
                        };
240
                        if matches!(opts.ty, MethodTy::Abstract) {
×
241
                            modifiers.insert(MethodModifier::Abstract);
×
242
                        }
243

244
                        let builder = func.function_builder(call_type);
×
245

246
                        self.functions.push(FnBuilder {
×
247
                            builder,
×
248
                            vis: opts.vis,
×
249
                            modifiers,
×
250
                        });
251
                    }
252
                }
253
                _ => {}
×
254
            }
255
        }
256
        Ok(())
×
257
    }
258

259
    /// Generates an `impl PhpClassImpl<Self> for PhpClassImplCollector<Self>`
260
    /// block.
261
    fn generate_php_class_impl(&self) -> TokenStream {
×
262
        let path = &self.path;
×
263
        let functions = &self.functions;
×
264
        let constructor = self
×
265
            .constructor
×
266
            .as_ref()
NEW
267
            .map(|(func, vis)| func.constructor_meta(self.path, vis.as_ref()))
×
268
            .option_tokens();
269
        let constants = self.constants.iter().map(|c| {
×
270
            let name = &c.name;
×
271
            let ident = c.ident;
×
272
            let docs = &c.docs;
×
273
            quote! {
×
274
                (#name, &#path::#ident, &[#(#docs),*])
×
275
            }
276
        });
277

278
        quote! {
×
279
            impl ::ext_php_rs::internal::class::PhpClassImpl<#path>
×
280
                for ::ext_php_rs::internal::class::PhpClassImplCollector<#path>
×
281
            {
282
                fn get_methods(self) -> ::std::vec::Vec<
×
283
                    (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)
×
284
                > {
×
285
                    vec![#(#functions),*]
×
286
                }
287

288
                fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> {
×
289
                    todo!()
×
290
                }
291

292
                fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> {
×
293
                    #constructor
×
294
                }
295

296
                fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] {
×
297
                    &[#(#constants),*]
×
298
                }
299
            }
300
        }
301
    }
302
}
303

304
impl quote::ToTokens for FnBuilder {
305
    fn to_tokens(&self, tokens: &mut TokenStream) {
×
306
        let builder = &self.builder;
×
307
        // TODO(cole_d): allow more flags via attributes
308
        let mut flags = vec![];
×
NEW
309
        let vis = &self.vis;
×
NEW
310
        flags.push(quote! { #vis });
×
UNCOV
311
        for flag in &self.modifiers {
×
312
            flags.push(quote! { #flag });
313
        }
314

315
        quote! {
×
316
            (#builder, #(#flags)|*)
317
        }
318
        .to_tokens(tokens);
×
319
    }
320
}
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