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

davidcole1340 / ext-php-rs / 14285655645

05 Apr 2025 08:46PM CUT coverage: 13.062%. First build
14285655645

Pull #416

github

Xenira
ci(coverage): add coverage reporting

Refs: #415
Pull Request #416: ci(coverage): add coverage reporting

520 of 3981 relevant lines covered (13.06%)

1.23 hits per line

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

0.0
/crates/macros/src/function.rs
1
use std::collections::HashMap;
2

3
use darling::ast::NestedMeta;
4
use darling::{FromMeta, ToTokens};
5
use proc_macro2::{Ident, Span, TokenStream};
6
use quote::{format_ident, quote};
7
use syn::spanned::Spanned as _;
8
use syn::PatType;
9
use syn::{FnArg, GenericArgument, ItemFn, Lit, PathArguments, Type, TypePath};
10

11
use crate::helpers::get_docs;
12
use crate::prelude::*;
13
use crate::syn_ext::DropLifetimes;
14

15
pub fn wrap(input: syn::Path) -> Result<TokenStream> {
×
16
    let Some(func_name) = input.get_ident() else {
×
17
        bail!(input => "Pass a PHP function name into `wrap_function!()`.");
×
18
    };
19
    let builder_func = format_ident!("_internal_{func_name}");
×
20

21
    Ok(quote! {{
×
22
        (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)()
×
23
    }})
24
}
25

26
#[derive(Default, Debug, FromMeta)]
27
#[darling(default)]
28
pub struct FnArgs {
29
    /// The name of the function.
30
    name: Option<String>,
31
    /// The first optional argument of the function signature.
32
    optional: Option<Ident>,
33
    /// Default values for optional arguments.
34
    defaults: HashMap<Ident, Lit>,
35
}
36

37
pub fn parser(opts: TokenStream, input: ItemFn) -> Result<TokenStream> {
×
38
    let meta = NestedMeta::parse_meta_list(opts)?;
×
39
    let opts = match FnArgs::from_list(&meta) {
×
40
        Ok(opts) => opts,
×
41
        Err(e) => bail!("Failed to parse attribute options: {:?}", e),
×
42
    };
43

44
    let args = Args::parse_from_fnargs(input.sig.inputs.iter(), opts.defaults)?;
×
45
    if let Some(ReceiverArg { span, .. }) = args.receiver {
×
46
        bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`.");
×
47
    }
48

49
    let docs = get_docs(&input.attrs);
×
50

51
    let func = Function::new(&input.sig, opts.name, args, opts.optional, docs)?;
×
52
    let function_impl = func.php_function_impl()?;
×
53

54
    Ok(quote! {
×
55
        #input
×
56
        #function_impl
×
57
    })
58
}
59

60
#[derive(Debug)]
61
pub struct Function<'a> {
62
    /// Identifier of the Rust function associated with the function.
63
    pub ident: &'a Ident,
64
    /// Name of the function in PHP.
65
    pub name: String,
66
    /// Function arguments.
67
    pub args: Args<'a>,
68
    /// Function outputs.
69
    pub output: Option<&'a Type>,
70
    /// The first optional argument of the function.
71
    pub optional: Option<Ident>,
72
    /// Doc comments for the function.
73
    pub docs: Vec<String>,
74
}
75

76
#[derive(Debug)]
77
pub enum CallType<'a> {
78
    Function,
79
    Method {
80
        class: &'a syn::Path,
81
        receiver: MethodReceiver,
82
    },
83
}
84

85
/// Type of receiver on the method.
86
#[derive(Debug)]
87
pub enum MethodReceiver {
88
    /// Static method - has no receiver.
89
    Static,
90
    /// Class method, takes `&self` or `&mut self`.
91
    Class,
92
    /// Class method, takes `&mut ZendClassObject<Self>`.
93
    ZendClassObject,
94
}
95

96
impl<'a> Function<'a> {
97
    /// Parse a function.
98
    ///
99
    /// # Parameters
100
    ///
101
    /// * `sig` - Function signature.
102
    /// * `name` - Function name in PHP land.
103
    /// * `args` - Function arguments.
104
    /// * `optional` - The ident of the first optional argument.
105
    pub fn new(
×
106
        sig: &'a syn::Signature,
107
        name: Option<String>,
108
        args: Args<'a>,
109
        optional: Option<Ident>,
110
        docs: Vec<String>,
111
    ) -> Result<Self> {
112
        Ok(Self {
×
113
            ident: &sig.ident,
×
114
            name: name.unwrap_or_else(|| sig.ident.to_string()),
×
115
            args,
×
116
            output: match &sig.output {
×
117
                syn::ReturnType::Default => None,
×
118
                syn::ReturnType::Type(_, ty) => Some(&**ty),
×
119
            },
120
            optional,
×
121
            docs,
×
122
        })
123
    }
124

125
    /// Generates an internal identifier for the function.
126
    pub fn internal_ident(&self) -> Ident {
×
127
        format_ident!("_internal_{}", &self.ident)
×
128
    }
129

130
    /// Generates the function builder for the function.
131
    pub fn function_builder(&self, call_type: CallType) -> Result<TokenStream> {
×
132
        let ident = self.ident;
×
133
        let name = &self.name;
×
134
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
135

136
        // `handler` impl
137
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
138
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
139
        let arg_declarations = self
×
140
            .args
×
141
            .typed
×
142
            .iter()
143
            .map(TypedArg::arg_declaration)
×
144
            .collect::<Result<Vec<_>>>()?;
145
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
146
            arg.accessor(|e| {
×
147
                quote! {
×
148
                    #e.throw().expect("Failed to throw PHP exception.");
×
149
                    return;
×
150
                }
151
            })
152
        });
153

154
        // `entry` impl
155
        let required_args = required
×
156
            .iter()
157
            .map(TypedArg::arg_builder)
×
158
            .collect::<Result<Vec<_>>>()?;
159
        let not_required_args = not_required
×
160
            .iter()
161
            .map(TypedArg::arg_builder)
×
162
            .collect::<Result<Vec<_>>>()?;
163
        let returns = self.output.as_ref().map(|output| {
×
164
            quote! {
×
165
                .returns(
×
166
                    <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
×
167
                    false,
×
168
                    <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
169
                )
170
            }
171
        });
172

173
        let result = match call_type {
×
174
            CallType::Function => quote! {
×
175
                let parse = ex.parser()
×
176
                    #(.arg(&mut #required_arg_names))*
×
177
                    .not_required()
×
178
                    #(.arg(&mut #not_required_arg_names))*
×
179
                    .parse();
×
180
                if parse.is_err() {
×
181
                    return;
×
182
                }
183

184
                #ident(#({#arg_accessors}),*)
×
185
            },
186
            CallType::Method { class, receiver } => {
×
187
                let this = match receiver {
×
188
                    MethodReceiver::Static => quote! {
×
189
                        let parse = ex.parser();
×
190
                    },
191
                    MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! {
×
192
                        let (parse, this) = ex.parser_method::<#class>();
×
193
                        let this = match this {
×
194
                            Some(this) => this,
×
195
                            None => {
×
196
                                ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into())
×
197
                                    .throw()
×
198
                                    .unwrap();
×
199
                                return;
×
200
                            }
201
                        };
202
                    },
203
                };
204
                let call = match receiver {
×
205
                    MethodReceiver::Static => {
×
206
                        quote! { #class::#ident(#({#arg_accessors}),*) }
×
207
                    }
208
                    MethodReceiver::Class => quote! { this.#ident(#({#arg_accessors}),*) },
×
209
                    MethodReceiver::ZendClassObject => {
×
210
                        quote! { #class::#ident(this, #({#arg_accessors}),*) }
×
211
                    }
212
                };
213
                quote! {
×
214
                    #this
×
215
                    let parse_result = parse
×
216
                        #(.arg(&mut #required_arg_names))*
×
217
                        .not_required()
×
218
                        #(.arg(&mut #not_required_arg_names))*
×
219
                        .parse();
×
220
                    if parse_result.is_err() {
×
221
                        return;
×
222
                    }
223

224
                    #call
×
225
                }
226
            }
227
        };
228

229
        let docs = if !self.docs.is_empty() {
×
230
            let docs = &self.docs;
×
231
            quote! {
×
232
                .docs(&[#(#docs),*])
×
233
            }
234
        } else {
235
            quote! {}
×
236
        };
237

238
        Ok(quote! {
×
239
            ::ext_php_rs::builders::FunctionBuilder::new(#name, {
×
240
                ::ext_php_rs::zend_fastcall! {
×
241
                    extern fn handler(
×
242
                        ex: &mut ::ext_php_rs::zend::ExecuteData,
×
243
                        retval: &mut ::ext_php_rs::types::Zval,
×
244
                    ) {
245
                        use ::ext_php_rs::convert::IntoZval;
×
246

247
                        #(#arg_declarations)*
×
248
                        let result = {
×
249
                            #result
×
250
                        };
251

252
                        if let Err(e) = result.set_zval(retval, false) {
×
253
                            let e: ::ext_php_rs::exception::PhpException = e.into();
×
254
                            e.throw().expect("Failed to throw PHP exception.");
×
255
                        }
256
                    }
257
                }
258
                handler
×
259
            })
260
            #(.arg(#required_args))*
×
261
            .not_required()
×
262
            #(.arg(#not_required_args))*
×
263
            #returns
×
264
            #docs
×
265
        })
266
    }
267

268
    /// Generates a struct and impl for the `PhpFunction` trait.
269
    pub fn php_function_impl(&self) -> Result<TokenStream> {
×
270
        let internal_ident = self.internal_ident();
×
271
        let builder = self.function_builder(CallType::Function)?;
×
272

273
        Ok(quote! {
×
274
            #[doc(hidden)]
×
275
            #[allow(non_camel_case_types)]
×
276
            struct #internal_ident;
×
277

278
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
×
279
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
×
280
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
×
281
                    {
282
                        #builder
×
283
                    }
284
                    entry
×
285
                };
286
            }
287
        })
288
    }
289

290
    /// Returns a constructor metadata object for this function. This doesn't
291
    /// check if the function is a constructor, however.
292
    pub fn constructor_meta(&self, class: &syn::Path) -> Result<TokenStream> {
×
293
        let ident = self.ident;
×
294
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
295
        let required_args = required
×
296
            .iter()
297
            .map(TypedArg::arg_builder)
×
298
            .collect::<Result<Vec<_>>>()?;
299
        let not_required_args = not_required
×
300
            .iter()
301
            .map(TypedArg::arg_builder)
×
302
            .collect::<Result<Vec<_>>>()?;
303

304
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
305
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
306
        let arg_declarations = self
×
307
            .args
×
308
            .typed
×
309
            .iter()
310
            .map(TypedArg::arg_declaration)
×
311
            .collect::<Result<Vec<_>>>()?;
312
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
313
            arg.accessor(
×
314
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
×
315
            )
316
        });
317
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
×
318
            quote! {
×
319
                .variadic()
×
320
            }
321
        });
322

323
        Ok(quote! {
×
324
            ::ext_php_rs::class::ConstructorMeta {
×
325
                constructor: {
×
326
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
×
327
                        #(#arg_declarations)*
×
328
                        let parse = ex.parser()
×
329
                            #(.arg(&mut #required_arg_names))*
×
330
                            .not_required()
×
331
                            #(.arg(&mut #not_required_arg_names))*
×
332
                            #variadic
×
333
                            .parse();
×
334
                        if parse.is_err() {
×
335
                            return ::ext_php_rs::class::ConstructorResult::ArgError;
×
336
                        }
337
                        #class::#ident(#({#arg_accessors}),*).into()
×
338
                    }
339
                    inner
×
340
                },
341
                build_fn: {
×
342
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
×
343
                        func
×
344
                            #(.arg(#required_args))*
×
345
                            .not_required()
×
346
                            #(.arg(#not_required_args))*
×
347
                            #variadic
×
348
                    }
349
                    inner
×
350
                }
351
            }
352
        })
353
    }
354
}
355

356
#[derive(Debug)]
357
pub struct ReceiverArg {
358
    pub _mutable: bool,
359
    pub span: Span,
360
}
361

362
#[derive(Debug)]
363
pub struct TypedArg<'a> {
364
    pub name: &'a Ident,
365
    pub ty: Type,
366
    pub nullable: bool,
367
    pub default: Option<Lit>,
368
    pub as_ref: bool,
369
    pub variadic: bool,
370
}
371

372
#[derive(Debug)]
373
pub struct Args<'a> {
374
    pub receiver: Option<ReceiverArg>,
375
    pub typed: Vec<TypedArg<'a>>,
376
}
377

378
impl<'a> Args<'a> {
379
    pub fn parse_from_fnargs(
×
380
        args: impl Iterator<Item = &'a FnArg>,
381
        mut defaults: HashMap<Ident, Lit>,
382
    ) -> Result<Self> {
383
        let mut result = Self {
384
            receiver: None,
385
            typed: vec![],
×
386
        };
387
        for arg in args {
×
388
            match arg {
×
389
                FnArg::Receiver(receiver) => {
×
390
                    if receiver.reference.is_none() {
×
391
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
×
392
                    } else if result.receiver.is_some() {
×
393
                        bail!(receiver => "Too many receivers specified.")
×
394
                    }
395
                    result.receiver.replace(ReceiverArg {
×
396
                        _mutable: receiver.mutability.is_some(),
×
397
                        span: receiver.span(),
×
398
                    });
399
                }
400
                FnArg::Typed(PatType { pat, ty, .. }) => {
×
401
                    let ident = match &**pat {
×
402
                        syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident,
×
403
                        _ => bail!(pat => "Unsupported argument."),
×
404
                    };
405

406
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
407
                    let default = defaults.remove(ident);
×
408
                    let nullable = type_is_nullable(ty.as_ref(), default.is_some())?;
×
409
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
×
410
                    result.typed.push(TypedArg {
×
411
                        name: ident,
×
412
                        ty,
×
413
                        nullable,
×
414
                        default,
×
415
                        as_ref,
×
416
                        variadic,
×
417
                    });
418
                }
419
            }
420
        }
421
        Ok(result)
×
422
    }
423

424
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
×
425
        match ty {
×
426
            Type::Reference(ref_) => {
×
427
                let as_ref = ref_.mutability.is_some();
×
428
                match ref_.elem.as_ref() {
×
429
                    Type::Slice(slice) => (
×
430
                        // TODO: Allow specifying the variadic type.
431
                        slice.elem.to_token_stream().to_string() == "& Zval",
×
432
                        as_ref,
×
433
                        ty.clone(),
×
434
                    ),
435
                    _ => (false, as_ref, ty.clone()),
×
436
                }
437
            }
438
            Type::Path(TypePath { path, .. }) => {
×
439
                let mut as_ref = false;
×
440

441
                // For for types that are `Option<&mut T>` to turn them into
442
                // `Option<&T>`, marking the Arg as as "passed by reference".
443
                let ty = path
×
444
                    .segments
×
445
                    .last()
446
                    .filter(|seg| seg.ident == "Option")
×
447
                    .and_then(|seg| {
×
448
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
×
449
                            args.args
×
450
                                .iter()
×
451
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
×
452
                                .and_then(|ga| match ga {
×
453
                                    GenericArgument::Type(ty) => Some(match ty {
×
454
                                        Type::Reference(r) => {
×
455
                                            let mut new_ref = r.clone();
×
456
                                            new_ref.mutability = None;
×
457
                                            as_ref = true;
×
458
                                            Type::Reference(new_ref)
×
459
                                        }
460
                                        _ => ty.clone(),
×
461
                                    }),
462
                                    _ => None,
×
463
                                })
464
                        } else {
465
                            None
×
466
                        }
467
                    })
468
                    .unwrap_or_else(|| ty.clone());
×
469
                (false, as_ref, ty.clone())
×
470
            }
471
            _ => (false, false, ty.clone()),
×
472
        }
473
    }
474

475
    /// Splits the typed arguments into two slices:
476
    ///
477
    /// 1. Required arguments.
478
    /// 2. Non-required arguments.
479
    ///
480
    /// # Parameters
481
    ///
482
    /// * `optional` - The first optional argument. If [`None`], the optional
483
    ///   arguments will be from the first nullable argument after the last
484
    ///   non-nullable argument to the end of the arguments.
485
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
×
486
        let mut mid = None;
×
487
        for (i, arg) in self.typed.iter().enumerate() {
×
488
            if let Some(optional) = optional {
×
489
                if optional == arg.name {
×
490
                    mid.replace(i);
×
491
                }
492
            } else if mid.is_none() && arg.nullable {
×
493
                mid.replace(i);
×
494
            } else if !arg.nullable {
×
495
                mid.take();
×
496
            }
497
        }
498
        match mid {
×
499
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
×
500
            None => (&self.typed[..], &self.typed[0..0]),
×
501
        }
502
    }
503
}
504

505
impl TypedArg<'_> {
506
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
507
    /// to be used outside of the original function context.
508
    fn clean_ty(&self) -> Type {
×
509
        let mut ty = self.ty.clone();
×
510
        ty.drop_lifetimes();
×
511

512
        // Variadic arguments are passed as slices, so we need to extract the
513
        // inner type.
514
        if self.variadic {
×
515
            let reference = if let Type::Reference(r) = &ty {
×
516
                r
×
517
            } else {
518
                return ty;
×
519
            };
520

521
            if let Type::Slice(inner) = &*reference.elem {
×
522
                return *inner.elem.clone();
×
523
            }
524
        }
525

526
        ty
×
527
    }
528

529
    /// Returns a token stream containing an argument declaration, where the
530
    /// name of the variable holding the arg is the name of the argument.
531
    fn arg_declaration(&self) -> Result<TokenStream> {
×
532
        let name = self.name;
×
533
        let val = self.arg_builder()?;
×
534
        Ok(quote! {
×
535
            let mut #name = #val;
×
536
        })
537
    }
538

539
    /// Returns a token stream containing the `Arg` definition to be passed to
540
    /// `ext-php-rs`.
541
    fn arg_builder(&self) -> Result<TokenStream> {
×
542
        let name = self.name.to_string();
×
543
        let ty = self.clean_ty();
×
544
        let null = if self.nullable {
×
545
            Some(quote! { .allow_null() })
×
546
        } else {
547
            None
×
548
        };
549
        let default = self.default.as_ref().map(|val| {
×
550
            let val = val.to_token_stream().to_string();
×
551
            quote! {
×
552
                .default(#val)
×
553
            }
554
        });
555
        let as_ref = if self.as_ref {
×
556
            Some(quote! { .as_ref() })
×
557
        } else {
558
            None
×
559
        };
560
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
×
561
        Ok(quote! {
×
562
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
×
563
                #null
×
564
                #default
×
565
                #as_ref
×
566
                #variadic
×
567
        })
568
    }
569

570
    /// Get the accessor used to access the value of the argument.
571
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
×
572
        let name = self.name;
×
573
        if let Some(default) = &self.default {
×
574
            quote! {
×
575
                #name.val().unwrap_or(#default.into())
×
576
            }
577
        } else if self.variadic {
×
578
            quote! {
×
579
                &#name.variadic_vals()
×
580
            }
581
        } else if self.nullable {
×
582
            // Originally I thought we could just use the below case for `null` options, as
583
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
584
            // the argument isn't given, as the underlying zval is null.
585
            quote! {
×
586
                #name.val()
×
587
            }
588
        } else {
589
            let bail = bail_fn(quote! {
×
590
                ::ext_php_rs::exception::PhpException::default(
×
591
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
592
                )
593
            });
594
            quote! {
×
595
                match #name.val() {
×
596
                    Some(val) => val,
×
597
                    None => {
×
598
                        #bail;
×
599
                    }
600
                }
601
            }
602
        }
603
    }
604
}
605

606
/// Returns true of the given type is nullable in PHP.
607
// TODO(david): Eventually move to compile-time constants for this (similar to
608
// FromZval::NULLABLE).
609
pub fn type_is_nullable(ty: &Type, has_default: bool) -> Result<bool> {
×
610
    Ok(match ty {
×
611
        syn::Type::Path(path) => {
×
612
            has_default
×
613
                || path
×
614
                    .path
×
615
                    .segments
×
616
                    .iter()
×
617
                    .next_back()
×
618
                    .map(|seg| seg.ident == "Option")
×
619
                    .unwrap_or(false)
×
620
        }
621
        syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */
×
622
        // wrapped in `Option` (in that case it'd be a Path).
623
        _ => bail!(ty => "Unsupported argument type."),
×
624
    })
625
}
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