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

extphprs / ext-php-rs / 20396336964

20 Dec 2025 03:24PM UTC coverage: 35.945% (-0.07%) from 36.015%
20396336964

Pull #625

github

web-flow
Merge 44acd6d0d into 62c1aa7d6
Pull Request #625: fix(zend_bailout): Fix zend_bailout handling #537

12 of 61 new or added lines in 4 files covered. (19.67%)

100 existing lines in 3 files now uncovered.

1656 of 4607 relevant lines covered (35.95%)

11.05 hits per line

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

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

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

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

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

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

25
#[derive(FromAttributes, Default, Debug)]
26
#[darling(default, attributes(php), forward_attrs(doc))]
27
struct PhpFunctionAttribute {
28
    #[darling(flatten)]
29
    rename: PhpRename,
30
    defaults: HashMap<Ident, Expr>,
31
    optional: Option<Ident>,
32
    vis: Option<Visibility>,
33
    attrs: Vec<syn::Attribute>,
34
}
35

36
pub fn parser(mut input: ItemFn) -> Result<TokenStream> {
×
37
    let php_attr = PhpFunctionAttribute::from_attributes(&input.attrs)?;
×
38
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
×
39

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

45
    let docs = get_docs(&php_attr.attrs)?;
×
46

47
    let func = Function::new(
48
        &input.sig,
×
49
        php_attr
×
50
            .rename
×
51
            .rename(input.sig.ident.to_string(), RenameRule::Snake),
×
52
        args,
×
53
        php_attr.optional,
×
54
        docs,
×
55
    );
56
    let function_impl = func.php_function_impl();
×
57

58
    Ok(quote! {
×
59
        #input
×
60
        #function_impl
×
61
    })
62
}
63

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

80
#[derive(Debug)]
81
pub enum CallType<'a> {
82
    Function,
83
    Method {
84
        class: &'a syn::Path,
85
        receiver: MethodReceiver,
86
    },
87
}
88

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

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

129
    /// Generates an internal identifier for the function.
130
    pub fn internal_ident(&self) -> Ident {
×
131
        format_ident!("_internal_{}", &self.ident)
×
132
    }
133

134
    pub fn abstract_function_builder(&self) -> TokenStream {
3✔
135
        let name = &self.name;
6✔
136
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
15✔
137

138
        // `entry` impl
139
        let required_args = required
6✔
140
            .iter()
141
            .map(TypedArg::arg_builder)
3✔
142
            .collect::<Vec<_>>();
143
        let not_required_args = not_required
6✔
144
            .iter()
145
            .map(TypedArg::arg_builder)
3✔
146
            .collect::<Vec<_>>();
147

148
        let returns = self.build_returns();
9✔
149
        let docs = if self.docs.is_empty() {
9✔
150
            quote! {}
2✔
151
        } else {
152
            let docs = &self.docs;
2✔
153
            quote! {
1✔
154
                .docs(&[#(#docs),*])
×
155
            }
156
        };
157

158
        quote! {
3✔
159
            ::ext_php_rs::builders::FunctionBuilder::new_abstract(#name)
×
160
            #(.arg(#required_args))*
×
161
            .not_required()
×
162
            #(.arg(#not_required_args))*
×
163
            #returns
×
164
            #docs
×
165
        }
166
    }
167

168
    /// Generates the function builder for the function.
169
    pub fn function_builder(&self, call_type: CallType) -> TokenStream {
×
170
        let name = &self.name;
×
171
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
172

173
        // `handler` impl
174
        let arg_declarations = self
×
175
            .args
×
176
            .typed
×
177
            .iter()
178
            .map(TypedArg::arg_declaration)
×
179
            .collect::<Vec<_>>();
180

181
        // `entry` impl
182
        let required_args = required
×
183
            .iter()
184
            .map(TypedArg::arg_builder)
×
185
            .collect::<Vec<_>>();
186
        let not_required_args = not_required
×
187
            .iter()
188
            .map(TypedArg::arg_builder)
×
189
            .collect::<Vec<_>>();
190

191
        let returns = self.build_returns();
×
192
        let result = self.build_result(call_type, required, not_required);
×
193
        let docs = if self.docs.is_empty() {
×
194
            quote! {}
×
195
        } else {
196
            let docs = &self.docs;
×
197
            quote! {
×
198
                .docs(&[#(#docs),*])
×
199
            }
200
        };
201

202
        quote! {
×
203
            ::ext_php_rs::builders::FunctionBuilder::new(#name, {
×
204
                ::ext_php_rs::zend_fastcall! {
×
205
                    extern fn handler(
×
206
                        ex: &mut ::ext_php_rs::zend::ExecuteData,
×
207
                        retval: &mut ::ext_php_rs::types::Zval,
×
208
                    ) {
209
                        use ::ext_php_rs::convert::IntoZval;
×
NEW
UNCOV
210
                        use ::ext_php_rs::zend::try_catch;
×
NEW
211
                        use ::std::panic::AssertUnwindSafe;
×
212

213
                        // Wrap the handler body with try_catch to ensure Rust destructors
214
                        // are called if a bailout occurs (issue #537)
NEW
UNCOV
215
                        let catch_result = try_catch(AssertUnwindSafe(|| {
×
NEW
216
                            #(#arg_declarations)*
×
NEW
217
                            let result = {
×
NEW
218
                                #result
×
219
                            };
220

NEW
UNCOV
221
                            if let Err(e) = result.set_zval(retval, false) {
×
NEW
222
                                let e: ::ext_php_rs::exception::PhpException = e.into();
×
NEW
UNCOV
223
                                e.throw().expect("Failed to throw PHP exception.");
×
224
                            }
225
                        }));
226

227
                        // If there was a bailout, re-trigger it after Rust cleanup
NEW
228
                        if catch_result.is_err() {
×
NEW
UNCOV
229
                            unsafe { ::ext_php_rs::zend::bailout(); }
×
230
                        }
231
                    }
232
                }
UNCOV
233
                handler
×
234
            })
UNCOV
235
            #(.arg(#required_args))*
×
236
            .not_required()
×
237
            #(.arg(#not_required_args))*
×
238
            #returns
×
239
            #docs
×
240
        }
241
    }
242

243
    fn build_returns(&self) -> Option<TokenStream> {
3✔
244
        self.output.cloned().map(|mut output| {
12✔
245
            output.drop_lifetimes();
6✔
246
            quote! {
3✔
UNCOV
247
                .returns(
×
UNCOV
248
                    <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
×
UNCOV
249
                    false,
×
UNCOV
250
                    <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
251
                )
252
            }
253
        })
254
    }
255

256
    fn build_result(
×
257
        &self,
258
        call_type: CallType,
259
        required: &[TypedArg<'_>],
260
        not_required: &[TypedArg<'_>],
261
    ) -> TokenStream {
UNCOV
262
        let ident = self.ident;
×
UNCOV
263
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
264
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
265

UNCOV
266
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
×
UNCOV
267
            if arg.variadic {
×
268
                let name = arg.name;
×
269
                let variadic_name = format_ident!("__variadic_{}", name);
×
270
                let clean_ty = arg.clean_ty();
×
271
                Some(quote! {
×
272
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
273
                })
274
            } else {
UNCOV
275
                None
×
276
            }
277
        });
278

279
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
280
            arg.accessor(|e| {
×
281
                quote! {
×
282
                    #e.throw().expect("Failed to throw PHP exception.");
×
283
                    return;
×
284
                }
285
            })
286
        });
287

UNCOV
288
        match call_type {
×
289
            CallType::Function => quote! {
×
UNCOV
290
                let parse = ex.parser()
×
291
                    #(.arg(&mut #required_arg_names))*
×
292
                    .not_required()
×
293
                    #(.arg(&mut #not_required_arg_names))*
×
294
                    .parse();
×
295
                if parse.is_err() {
×
296
                    return;
×
297
                }
298
                #(#variadic_bindings)*
×
299

300
                #ident(#({#arg_accessors}),*)
×
301
            },
302
            CallType::Method { class, receiver } => {
×
303
                let this = match receiver {
×
304
                    MethodReceiver::Static => quote! {
×
305
                        let parse = ex.parser();
×
306
                    },
307
                    MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! {
×
308
                        let (parse, this) = ex.parser_method::<#class>();
×
309
                        let this = match this {
×
310
                            Some(this) => this,
×
311
                            None => {
×
312
                                ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into())
×
313
                                    .throw()
×
314
                                    .unwrap();
×
315
                                return;
×
316
                            }
317
                        };
318
                    },
319
                };
UNCOV
320
                let call = match receiver {
×
UNCOV
321
                    MethodReceiver::Static => {
×
322
                        quote! { #class::#ident(#({#arg_accessors}),*) }
×
323
                    }
324
                    MethodReceiver::Class => quote! { this.#ident(#({#arg_accessors}),*) },
×
UNCOV
325
                    MethodReceiver::ZendClassObject => {
×
326
                        quote! { #class::#ident(this, #({#arg_accessors}),*) }
×
327
                    }
328
                };
UNCOV
329
                quote! {
×
UNCOV
330
                    #this
×
331
                    let parse_result = parse
×
332
                        #(.arg(&mut #required_arg_names))*
×
333
                        .not_required()
×
334
                        #(.arg(&mut #not_required_arg_names))*
×
335
                        .parse();
×
336
                    if parse_result.is_err() {
×
337
                        return;
×
338
                    }
339
                    #(#variadic_bindings)*
×
340

341
                    #call
×
342
                }
343
            }
344
        }
345
    }
346

347
    /// Generates a struct and impl for the `PhpFunction` trait.
UNCOV
348
    pub fn php_function_impl(&self) -> TokenStream {
×
UNCOV
349
        let internal_ident = self.internal_ident();
×
350
        let builder = self.function_builder(CallType::Function);
×
351

352
        quote! {
×
UNCOV
353
            #[doc(hidden)]
×
354
            #[allow(non_camel_case_types)]
×
355
            struct #internal_ident;
×
356

357
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
×
UNCOV
358
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
×
359
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
×
360
                    {
361
                        #builder
×
362
                    }
363
                    entry
×
364
                };
365
            }
366
        }
367
    }
368

369
    /// Returns a constructor metadata object for this function. This doesn't
370
    /// check if the function is a constructor, however.
UNCOV
371
    pub fn constructor_meta(
×
372
        &self,
373
        class: &syn::Path,
374
        visibility: Option<&Visibility>,
375
    ) -> TokenStream {
UNCOV
376
        let ident = self.ident;
×
UNCOV
377
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
378
        let required_args = required
×
379
            .iter()
380
            .map(TypedArg::arg_builder)
×
381
            .collect::<Vec<_>>();
382
        let not_required_args = not_required
×
383
            .iter()
384
            .map(TypedArg::arg_builder)
×
385
            .collect::<Vec<_>>();
386

UNCOV
387
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
UNCOV
388
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
389
        let arg_declarations = self
×
390
            .args
×
391
            .typed
×
392
            .iter()
393
            .map(TypedArg::arg_declaration)
×
394
            .collect::<Vec<_>>();
395
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
×
UNCOV
396
            if arg.variadic {
×
397
                let name = arg.name;
×
398
                let variadic_name = format_ident!("__variadic_{}", name);
×
399
                let clean_ty = arg.clean_ty();
×
400
                Some(quote! {
×
401
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
402
                })
403
            } else {
UNCOV
404
                None
×
405
            }
406
        });
UNCOV
407
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
UNCOV
408
            arg.accessor(
×
409
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
×
410
            )
411
        });
UNCOV
412
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
×
UNCOV
413
            quote! {
×
414
                .variadic()
×
415
            }
416
        });
UNCOV
417
        let docs = &self.docs;
×
UNCOV
418
        let flags = visibility.option_tokens();
×
419

420
        quote! {
×
UNCOV
421
            ::ext_php_rs::class::ConstructorMeta {
×
422
                constructor: {
×
423
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
×
NEW
424
                        use ::ext_php_rs::zend::try_catch;
×
NEW
425
                        use ::std::panic::AssertUnwindSafe;
×
426

427
                        // Wrap the constructor body with try_catch to ensure Rust destructors
428
                        // are called if a bailout occurs (issue #537)
NEW
429
                        let catch_result = try_catch(AssertUnwindSafe(|| {
×
NEW
430
                            #(#arg_declarations)*
×
NEW
431
                            let parse = ex.parser()
×
NEW
432
                                #(.arg(&mut #required_arg_names))*
×
NEW
433
                                .not_required()
×
NEW
434
                                #(.arg(&mut #not_required_arg_names))*
×
NEW
UNCOV
435
                                #variadic
×
NEW
436
                                .parse();
×
NEW
437
                            if parse.is_err() {
×
NEW
UNCOV
438
                                return ::ext_php_rs::class::ConstructorResult::ArgError;
×
439
                            }
NEW
UNCOV
440
                            #(#variadic_bindings)*
×
NEW
441
                            #class::#ident(#({#arg_accessors}),*).into()
×
442
                        }));
443

444
                        // If there was a bailout, re-trigger it after Rust cleanup
NEW
445
                        match catch_result {
×
NEW
446
                            Ok(result) => result,
×
NEW
447
                            Err(_) => unsafe { ::ext_php_rs::zend::bailout() },
×
448
                        }
449
                    }
450
                    inner
×
451
                },
452
                build_fn: {
×
UNCOV
453
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
×
UNCOV
454
                        func
×
UNCOV
455
                            .docs(&[#(#docs),*])
×
UNCOV
456
                            #(.arg(#required_args))*
×
UNCOV
457
                            .not_required()
×
UNCOV
458
                            #(.arg(#not_required_args))*
×
UNCOV
459
                            #variadic
×
460
                    }
UNCOV
461
                    inner
×
462
                },
UNCOV
463
                flags: #flags
×
464
            }
465
        }
466
    }
467
}
468

469
#[derive(Debug)]
470
pub struct ReceiverArg {
471
    pub _mutable: bool,
472
    pub span: Span,
473
}
474

475
#[derive(Debug)]
476
pub struct TypedArg<'a> {
477
    pub name: &'a Ident,
478
    pub ty: Type,
479
    pub nullable: bool,
480
    pub default: Option<Expr>,
481
    pub as_ref: bool,
482
    pub variadic: bool,
483
}
484

485
#[derive(Debug)]
486
pub struct Args<'a> {
487
    pub receiver: Option<ReceiverArg>,
488
    pub typed: Vec<TypedArg<'a>>,
489
}
490

491
impl<'a> Args<'a> {
492
    pub fn parse_from_fnargs(
3✔
493
        args: impl Iterator<Item = &'a FnArg>,
494
        mut defaults: HashMap<Ident, Expr>,
495
    ) -> Result<Self> {
496
        let mut result = Self {
497
            receiver: None,
498
            typed: vec![],
3✔
499
        };
500
        for arg in args {
13✔
501
            match arg {
5✔
502
                FnArg::Receiver(receiver) => {
3✔
503
                    if receiver.reference.is_none() {
6✔
504
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
×
505
                    } else if result.receiver.is_some() {
6✔
UNCOV
506
                        bail!(receiver => "Too many receivers specified.")
×
507
                    }
508
                    result.receiver.replace(ReceiverArg {
9✔
509
                        _mutable: receiver.mutability.is_some(),
9✔
510
                        span: receiver.span(),
3✔
511
                    });
512
                }
513
                FnArg::Typed(PatType { pat, ty, .. }) => {
4✔
514
                    let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else {
4✔
UNCOV
515
                        bail!(pat => "Unsupported argument.");
×
516
                    };
517

518
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
519
                    let default = defaults.remove(ident);
8✔
520
                    let nullable = type_is_nullable(ty.as_ref(), default.is_some())?;
10✔
521
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
8✔
522
                    result.typed.push(TypedArg {
6✔
523
                        name: ident,
4✔
524
                        ty,
4✔
525
                        nullable,
4✔
526
                        default,
4✔
527
                        as_ref,
2✔
528
                        variadic,
2✔
529
                    });
530
                }
531
            }
532
        }
533
        Ok(result)
3✔
534
    }
535

536
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
2✔
537
        match ty {
2✔
UNCOV
538
            Type::Reference(ref_) => {
×
UNCOV
539
                let as_ref = ref_.mutability.is_some();
×
UNCOV
540
                match ref_.elem.as_ref() {
×
UNCOV
541
                    Type::Slice(slice) => (
×
542
                        // TODO: Allow specifying the variadic type.
UNCOV
543
                        slice.elem.to_token_stream().to_string() == "& Zval",
×
UNCOV
544
                        as_ref,
×
UNCOV
545
                        ty.clone(),
×
546
                    ),
UNCOV
547
                    _ => (false, as_ref, ty.clone()),
×
548
                }
549
            }
550
            Type::Path(TypePath { path, .. }) => {
2✔
551
                let mut as_ref = false;
4✔
552

553
                // For for types that are `Option<&mut T>` to turn them into
554
                // `Option<&T>`, marking the Arg as as "passed by reference".
555
                let ty = path
4✔
556
                    .segments
2✔
557
                    .last()
558
                    .filter(|seg| seg.ident == "Option")
6✔
559
                    .and_then(|seg| {
2✔
560
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
×
561
                            args.args
×
562
                                .iter()
×
563
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
×
564
                                .and_then(|ga| match ga {
×
565
                                    GenericArgument::Type(ty) => Some(match ty {
×
566
                                        Type::Reference(r) => {
×
567
                                            let mut new_ref = r.clone();
×
568
                                            new_ref.mutability = None;
×
569
                                            as_ref = true;
×
570
                                            Type::Reference(new_ref)
×
571
                                        }
572
                                        _ => ty.clone(),
×
573
                                    }),
574
                                    _ => None,
×
575
                                })
576
                        } else {
577
                            None
×
578
                        }
579
                    })
580
                    .unwrap_or_else(|| ty.clone());
6✔
581
                (false, as_ref, ty.clone())
4✔
582
            }
UNCOV
583
            _ => (false, false, ty.clone()),
×
584
        }
585
    }
586

587
    /// Splits the typed arguments into two slices:
588
    ///
589
    /// 1. Required arguments.
590
    /// 2. Non-required arguments.
591
    ///
592
    /// # Parameters
593
    ///
594
    /// * `optional` - The first optional argument. If [`None`], the optional
595
    ///   arguments will be from the first nullable argument after the last
596
    ///   non-nullable argument to the end of the arguments.
597
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
3✔
598
        let mut mid = None;
6✔
599
        for (i, arg) in self.typed.iter().enumerate() {
10✔
600
            if let Some(optional) = optional {
2✔
UNCOV
601
                if optional == arg.name {
×
UNCOV
602
                    mid.replace(i);
×
603
                }
604
            } else if mid.is_none() && arg.nullable {
6✔
UNCOV
605
                mid.replace(i);
×
606
            } else if !arg.nullable {
4✔
607
                mid.take();
2✔
608
            }
609
        }
610
        match mid {
3✔
UNCOV
611
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
×
612
            None => (&self.typed[..], &self.typed[0..0]),
6✔
613
        }
614
    }
615
}
616

617
impl TypedArg<'_> {
618
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
619
    /// to be used outside of the original function context.
620
    fn clean_ty(&self) -> Type {
2✔
621
        let mut ty = self.ty.clone();
6✔
622
        ty.drop_lifetimes();
4✔
623

624
        // Variadic arguments are passed as &[&Zval], so we need to extract the
625
        // inner type.
626
        if self.variadic {
2✔
UNCOV
627
            let Type::Reference(reference) = &ty else {
×
UNCOV
628
                return ty;
×
629
            };
630

UNCOV
631
            if let Type::Slice(inner) = &*reference.elem {
×
632
                return *inner.elem.clone();
×
633
            }
634
        }
635

636
        ty
2✔
637
    }
638

639
    /// Returns a token stream containing an argument declaration, where the
640
    /// name of the variable holding the arg is the name of the argument.
UNCOV
641
    fn arg_declaration(&self) -> TokenStream {
×
UNCOV
642
        let name = self.name;
×
UNCOV
643
        let val = self.arg_builder();
×
UNCOV
644
        quote! {
×
645
            let mut #name = #val;
646
        }
647
    }
648

649
    /// Returns a token stream containing the `Arg` definition to be passed to
650
    /// `ext-php-rs`.
651
    fn arg_builder(&self) -> TokenStream {
2✔
652
        let name = self.name.to_string();
6✔
653
        let ty = self.clean_ty();
6✔
654
        let null = if self.nullable {
4✔
UNCOV
655
            Some(quote! { .allow_null() })
×
656
        } else {
657
            None
2✔
658
        };
659
        let default = self.default.as_ref().map(|val| {
8✔
660
            let val = val.to_token_stream().to_string();
×
UNCOV
661
            quote! {
×
662
                .default(#val)
663
            }
664
        });
665
        let as_ref = if self.as_ref {
4✔
666
            Some(quote! { .as_ref() })
×
667
        } else {
668
            None
2✔
669
        };
670
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
6✔
671
        quote! {
2✔
672
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
673
                #null
674
                #default
675
                #as_ref
676
                #variadic
677
        }
678
    }
679

680
    /// Get the accessor used to access the value of the argument.
UNCOV
681
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
×
UNCOV
682
        let name = self.name;
×
UNCOV
683
        if let Some(default) = &self.default {
×
UNCOV
684
            quote! {
×
UNCOV
685
                #name.val().unwrap_or(#default.into())
×
686
            }
687
        } else if self.variadic {
×
688
            let variadic_name = format_ident!("__variadic_{}", name);
×
689
            quote! {
×
690
                #variadic_name.as_slice()
×
691
            }
692
        } else if self.nullable {
×
693
            // Originally I thought we could just use the below case for `null` options, as
694
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
695
            // the argument isn't given, as the underlying zval is null.
UNCOV
696
            quote! {
×
697
                #name.val()
×
698
            }
699
        } else {
UNCOV
700
            let bail = bail_fn(quote! {
×
701
                ::ext_php_rs::exception::PhpException::default(
×
702
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
703
                )
704
            });
705
            quote! {
×
706
                match #name.val() {
×
707
                    Some(val) => val,
×
UNCOV
708
                    None => {
×
UNCOV
709
                        #bail;
×
710
                    }
711
                }
712
            }
713
        }
714
    }
715
}
716

717
/// Returns true of the given type is nullable in PHP.
718
// TODO(david): Eventually move to compile-time constants for this (similar to
719
// FromZval::NULLABLE).
720
pub fn type_is_nullable(ty: &Type, has_default: bool) -> Result<bool> {
2✔
721
    Ok(match ty {
2✔
722
        syn::Type::Path(path) => {
2✔
723
            has_default
2✔
724
                || path
2✔
725
                    .path
2✔
726
                    .segments
2✔
727
                    .iter()
2✔
728
                    .next_back()
2✔
729
                    .is_some_and(|seg| seg.ident == "Option")
6✔
730
        }
UNCOV
731
        syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */
×
732
        // wrapped in `Option` (in that case it'd be a Path).
UNCOV
733
        _ => bail!(ty => "Unsupported argument type."),
×
734
    })
735
}
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

© 2026 Coveralls, Inc