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

extphprs / ext-php-rs / 30633613198

31 Jul 2026 01:13PM UTC coverage: 66.511% (+0.03%) from 66.481%
30633613198

Pull #756

github

ptondereau
chore(deps): update syn to 3 and darling to 0.24
Pull Request #756: chore(deps): update syn to 3 and darling to 0.24

15 of 16 new or added lines in 4 files covered. (93.75%)

1 existing line in 1 file now uncovered.

8854 of 13312 relevant lines covered (66.51%)

32.9 hits per line

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

90.36
/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, quote_spanned};
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::{
11
    PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name, validate_php_name,
12
};
13
use crate::prelude::*;
14
use crate::syn_ext::DropLifetimes;
15

16
/// Checks if the return type is a reference to Self (`&Self` or `&mut Self`).
17
/// This is used to detect methods that return `$this` in PHP.
18
fn returns_self_ref(output: Option<&Type>) -> bool {
1,419✔
19
    let Some(ty) = output else {
1,419✔
20
        return false;
188✔
21
    };
22
    if let Type::Reference(ref_) = ty
1,231✔
23
        && let Type::Path(path) = &*ref_.elem
100✔
24
        && path.path.segments.len() == 1
100✔
25
        && let Some(segment) = path.path.segments.last()
100✔
26
    {
27
        return segment.ident == "Self";
100✔
28
    }
1,131✔
29
    false
1,131✔
30
}
1,419✔
31

32
/// Checks if the return type is `Self` (not a reference).
33
/// This is used to detect methods that return a new instance of the same class.
34
fn returns_self(output: Option<&Type>) -> bool {
255✔
35
    let Some(ty) = output else {
255✔
36
        return false;
×
37
    };
38
    if let Type::Path(path) = ty
255✔
39
        && path.path.segments.len() == 1
235✔
40
        && let Some(segment) = path.path.segments.last()
235✔
41
    {
42
        return segment.ident == "Self";
235✔
43
    }
20✔
44
    false
20✔
45
}
255✔
46

47
pub fn wrap(input: &syn::Path) -> Result<TokenStream> {
165✔
48
    let Some(func_name) = input.get_ident() else {
165✔
49
        bail!(input => "Pass a PHP function name into `wrap_function!()`.");
×
50
    };
51
    let builder_func = format_ident!("_internal_{func_name}");
165✔
52

53
    Ok(quote! {{
165✔
54
        (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)()
165✔
55
    }})
165✔
56
}
165✔
57

58
#[derive(FromAttributes, Default, Debug)]
59
#[darling(default, attributes(php), forward_attrs(doc))]
60
struct PhpFunctionAttribute {
61
    #[darling(flatten)]
62
    rename: PhpRename,
63
    defaults: HashMap<Ident, Expr>,
64
    optional: Option<Ident>,
65
    vis: Option<Visibility>,
66
    attrs: Vec<syn::Attribute>,
67
}
68

69
pub fn parser(mut input: ItemFn) -> Result<TokenStream> {
202✔
70
    let php_attr = PhpFunctionAttribute::from_attributes(&input.attrs)?;
202✔
71
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
202✔
72

73
    let args = Args::parse_from_fnargs(input.sig.inputs.iter(), php_attr.defaults)?;
202✔
74
    if let Some(ReceiverArg { span }) = args.receiver {
202✔
75
        bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`.");
×
76
    }
202✔
77

78
    let docs = get_docs(&php_attr.attrs)?;
202✔
79

80
    let func_name = php_attr
202✔
81
        .rename
202✔
82
        .rename(ident_to_php_name(&input.sig.ident), RenameRule::Snake);
202✔
83
    validate_php_name(&func_name, PhpNameContext::Function, input.sig.ident.span())?;
202✔
84
    let func = Function::new(&input.sig, func_name, args, php_attr.optional, docs);
202✔
85
    let function_impl = func.php_function_impl();
202✔
86

87
    Ok(quote! {
202✔
88
        #input
202✔
89
        #function_impl
202✔
90
    })
202✔
91
}
202✔
92

93
#[derive(Debug)]
94
pub struct Function<'a> {
95
    /// Identifier of the Rust function associated with the function.
96
    pub ident: &'a Ident,
97
    /// Name of the function in PHP.
98
    pub name: String,
99
    /// Function arguments.
100
    pub args: Args<'a>,
101
    /// Function outputs.
102
    pub output: Option<&'a Type>,
103
    /// The first optional argument of the function.
104
    pub optional: Option<Ident>,
105
    /// Doc comments for the function.
106
    pub docs: Vec<String>,
107
}
108

109
#[derive(Debug)]
110
pub enum CallType<'a> {
111
    Function,
112
    Method {
113
        class: &'a syn::Path,
114
        receiver: MethodReceiver,
115
    },
116
}
117

118
/// Type of receiver on the method.
119
#[derive(Debug)]
120
pub enum MethodReceiver {
121
    /// Static method - has no receiver.
122
    Static,
123
    /// Class method, takes `&self` or `&mut self`.
124
    Class,
125
    /// Class method, takes `&mut ZendClassObject<Self>`.
126
    ZendClassObject,
127
}
128

129
impl<'a> Function<'a> {
130
    /// Parse a function.
131
    ///
132
    /// # Parameters
133
    ///
134
    /// * `sig` - Function signature.
135
    /// * `name` - Function name in PHP land.
136
    /// * `args` - Function arguments.
137
    /// * `optional` - The ident of the first optional argument.
138
    pub fn new(
340✔
139
        sig: &'a syn::Signature,
340✔
140
        name: String,
340✔
141
        args: Args<'a>,
340✔
142
        optional: Option<Ident>,
340✔
143
        docs: Vec<String>,
340✔
144
    ) -> Self {
340✔
145
        Self {
146
            ident: &sig.ident,
340✔
147
            name,
340✔
148
            args,
340✔
149
            output: match &sig.output {
340✔
150
                syn::ReturnType::Default => None,
51✔
151
                syn::ReturnType::Type(_, ty) => Some(&**ty),
289✔
152
            },
153
            optional,
340✔
154
            docs,
340✔
155
        }
156
    }
340✔
157

158
    /// Generates an internal identifier for the function.
159
    pub fn internal_ident(&self) -> Ident {
202✔
160
        format_ident!("_internal_{}", &self.ident)
202✔
161
    }
202✔
162

163
    pub fn abstract_function_builder(&self) -> TokenStream {
16✔
164
        let name = &self.name;
16✔
165
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
16✔
166

167
        // `entry` impl
168
        let required_args = required
16✔
169
            .iter()
16✔
170
            .map(TypedArg::arg_builder)
16✔
171
            .collect::<Vec<_>>();
16✔
172
        let not_required_args = not_required
16✔
173
            .iter()
16✔
174
            .map(TypedArg::arg_builder)
16✔
175
            .collect::<Vec<_>>();
16✔
176

177
        let returns = self.build_returns(None);
16✔
178
        let docs = if self.docs.is_empty() {
16✔
179
            quote! {}
14✔
180
        } else {
181
            let docs = &self.docs;
2✔
182
            quote! {
2✔
183
                .docs(&[#(#docs),*])
184
            }
185
        };
186

187
        quote! {
16✔
188
            ::ext_php_rs::builders::FunctionBuilder::new_abstract(#name)
189
            #(.arg(#required_args))*
190
            .not_required()
191
            #(.arg(#not_required_args))*
192
            #returns
193
            #docs
194
        }
195
    }
16✔
196

197
    /// Generates the function builder for the function.
198
    pub fn function_builder(&self, call_type: &CallType) -> TokenStream {
293✔
199
        let name = &self.name;
293✔
200
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
293✔
201

202
        // `handler` impl
203
        let arg_declarations = self
293✔
204
            .args
293✔
205
            .typed
293✔
206
            .iter()
293✔
207
            .map(TypedArg::arg_declaration)
293✔
208
            .collect::<Vec<_>>();
293✔
209

210
        // `entry` impl
211
        let required_args = required
293✔
212
            .iter()
293✔
213
            .map(TypedArg::arg_builder)
293✔
214
            .collect::<Vec<_>>();
293✔
215
        let not_required_args = not_required
293✔
216
            .iter()
293✔
217
            .map(TypedArg::arg_builder)
293✔
218
            .collect::<Vec<_>>();
293✔
219

220
        let returns = self.build_returns(Some(call_type));
293✔
221
        let result = self.build_result(call_type, required, not_required);
293✔
222
        let docs = if self.docs.is_empty() {
293✔
223
            quote! {}
189✔
224
        } else {
225
            let docs = &self.docs;
104✔
226
            quote! {
104✔
227
                .docs(&[#(#docs),*])
228
            }
229
        };
230

231
        // Static methods cannot return &Self or &mut Self
232
        if returns_self_ref(self.output)
293✔
233
            && let CallType::Method {
234
                receiver: MethodReceiver::Static,
235
                ..
236
            } = call_type
3✔
237
            && let Some(output) = self.output
×
238
        {
239
            return quote_spanned! { output.span() =>
×
240
                compile_error!(
241
                    "Static methods cannot return `&Self` or `&mut Self`. \
242
                     Only instance methods can use fluent interface pattern returning `$this`."
243
                )
244
            };
245
        }
293✔
246

247
        // Check if this method returns &Self or &mut Self
248
        // In that case, we need to return `this` (the ZendClassObject) directly
249
        let returns_this = returns_self_ref(self.output)
293✔
250
            && matches!(
×
251
                call_type,
3✔
252
                CallType::Method {
253
                    receiver: MethodReceiver::Class | MethodReceiver::ZendClassObject,
254
                    ..
255
                }
256
            );
257

258
        let handler_body = if self.is_fast_path_eligible(call_type) {
293✔
259
            self.build_fast_handler_body(call_type)
282✔
260
        } else if returns_this {
11✔
261
            quote! {
×
262
                use ::ext_php_rs::convert::IntoZval;
263

264
                #(#arg_declarations)*
265
                #result
266

267
                // The method returns &Self or &mut Self, use `this` directly
268
                if let Err(e) = this.set_zval(retval, false) {
269
                    let e: ::ext_php_rs::exception::PhpException = e.into();
270
                    e.throw().expect("Failed to throw PHP exception.");
271
                }
272
            }
273
        } else {
274
            quote! {
11✔
275
                use ::ext_php_rs::convert::IntoZval;
276

277
                #(#arg_declarations)*
278
                let result = {
279
                    #result
280
                };
281

282
                if let Err(e) = result.set_zval(retval, false) {
283
                    let e: ::ext_php_rs::exception::PhpException = e.into();
284
                    e.throw().expect("Failed to throw PHP exception.");
285
                }
286
            }
287
        };
288

289
        quote! {
293✔
290
            ::ext_php_rs::builders::FunctionBuilder::new(#name, {
291
                ::ext_php_rs::zend_fastcall! {
292
                    #[allow(clippy::used_underscore_binding)]
293
                    extern fn handler(
294
                        ex: &mut ::ext_php_rs::zend::ExecuteData,
295
                        retval: &mut ::ext_php_rs::types::Zval,
296
                    ) {
297
                        use ::ext_php_rs::zend::try_catch;
298
                        use ::std::panic::AssertUnwindSafe;
299

300
                        // Wrap the handler body with try_catch to ensure Rust destructors
301
                        // are called if a bailout occurs (issue #537)
302
                        let catch_result = try_catch(AssertUnwindSafe(|| {
303
                            #handler_body
304
                        }));
305

306
                        // If there was a bailout, run BailoutGuard cleanups and re-trigger
307
                        if catch_result.is_err() {
308
                            ::ext_php_rs::zend::run_bailout_cleanups();
309
                            unsafe { ::ext_php_rs::zend::bailout(); }
310
                        }
311
                    }
312
                }
313
                handler
314
            })
315
            #(.arg(#required_args))*
316
            .not_required()
317
            #(.arg(#not_required_args))*
318
            #returns
319
            #docs
320
        }
321
    }
293✔
322

323
    fn build_returns(&self, call_type: Option<&CallType>) -> TokenStream {
309✔
324
        let Some(output) = self.output.cloned() else {
309✔
325
            // PHP magic methods __destruct and __clone cannot have return types
326
            // (only applies to class methods, not standalone functions)
327
            if matches!(call_type, Some(CallType::Method { .. }))
47✔
328
                && (self.name == "__destruct" || self.name == "__clone")
20✔
329
            {
330
                return quote! {};
1✔
331
            }
50✔
332
            // No return type means void in PHP
333
            return quote! {
50✔
334
                .returns(::ext_php_rs::flags::DataType::Void, false, false)
335
            };
336
        };
337

338
        let mut output = output;
258✔
339
        output.drop_lifetimes();
258✔
340

341
        // If returning &Self or &mut Self from a method, use the class type
342
        // for return type information since we return `this` (ZendClassObject)
343
        if returns_self_ref(self.output)
258✔
344
            && let Some(CallType::Method { class, .. }) = call_type
3✔
345
        {
346
            return quote! {
3✔
347
                .returns(
348
                    <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE,
349
                    false,
350
                    <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE,
351
                )
352
            };
353
        }
255✔
354

355
        // If returning Self (new instance) from a method, replace Self with
356
        // the actual class type since Self won't resolve in generated code
357
        if returns_self(self.output)
255✔
358
            && let Some(CallType::Method { class, .. }) = call_type
1✔
359
        {
360
            return quote! {
1✔
361
                .returns(
362
                    <#class as ::ext_php_rs::convert::IntoZval>::TYPE,
363
                    false,
364
                    <#class as ::ext_php_rs::convert::IntoZval>::NULLABLE,
365
                )
366
            };
367
        }
254✔
368

369
        quote! {
254✔
370
            .returns(
371
                <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
372
                false,
373
                <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
374
            )
375
        }
376
    }
309✔
377

378
    fn build_result(
293✔
379
        &self,
293✔
380
        call_type: &CallType,
293✔
381
        required: &[TypedArg<'_>],
293✔
382
        not_required: &[TypedArg<'_>],
293✔
383
    ) -> TokenStream {
293✔
384
        let ident = self.ident;
293✔
385
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
293✔
386
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
293✔
387

388
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
293✔
389
            if arg.variadic {
179✔
390
                let name = arg.name;
11✔
391
                let variadic_name = format_ident!("__variadic_{}", name);
11✔
392
                let clean_ty = arg.clean_ty();
11✔
393
                Some(quote! {
11✔
394
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
11✔
395
                })
11✔
396
            } else {
397
                None
168✔
398
            }
399
        });
179✔
400

401
        let arg_accessors = self.args.typed.iter().map(|arg| {
293✔
402
            arg.accessor(|e| {
179✔
403
                quote! {
161✔
404
                    #e.throw().expect("Failed to throw PHP exception.");
405
                    return;
406
                }
407
            })
161✔
408
        });
179✔
409

410
        // Check if this method returns &Self or &mut Self
411
        let returns_this = returns_self_ref(self.output);
293✔
412

413
        match call_type {
293✔
414
            CallType::Function => quote! {
202✔
415
                let parse = ex.parser()
416
                    #(.arg(&mut #required_arg_names))*
417
                    .not_required()
418
                    #(.arg(&mut #not_required_arg_names))*
419
                    .parse();
420
                if parse.is_err() {
421
                    return;
422
                }
423
                #(#variadic_bindings)*
424

425
                #ident(#({#arg_accessors}),*)
426
            },
427
            CallType::Method { class, receiver } => {
91✔
428
                let this = match receiver {
91✔
429
                    MethodReceiver::Static => quote! {
13✔
430
                        let parse = ex.parser();
431
                    },
432
                    MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! {
78✔
433
                        let (parse, this) = ex.parser_method::<#class>();
434
                        let this = match this {
435
                            Some(this) => this,
436
                            None => {
437
                                ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into())
438
                                    .throw()
439
                                    .unwrap();
440
                                return;
441
                            }
442
                        };
443
                    },
444
                };
445

446
                // When returning &Self or &mut Self, discard the return value
447
                // (we'll use `this` directly in the handler)
448
                let call = match (receiver, returns_this) {
91✔
449
                    (MethodReceiver::Static, _) => {
450
                        quote! { #class::#ident(#({#arg_accessors}),*) }
13✔
451
                    }
452
                    (MethodReceiver::Class, true) => {
453
                        quote! { let _ = this.#ident(#({#arg_accessors}),*); }
3✔
454
                    }
455
                    (MethodReceiver::Class, false) => {
456
                        quote! { this.#ident(#({#arg_accessors}),*) }
71✔
457
                    }
458
                    (MethodReceiver::ZendClassObject, true) => {
459
                        // Explicit scope helps with mutable borrow lifetime when
460
                        // the method returns `&mut Self`
461
                        quote! {
×
462
                            {
463
                                let _ = #class::#ident(this, #({#arg_accessors}),*);
464
                            }
465
                        }
466
                    }
467
                    (MethodReceiver::ZendClassObject, false) => {
468
                        quote! { #class::#ident(this, #({#arg_accessors}),*) }
4✔
469
                    }
470
                };
471

472
                quote! {
91✔
473
                    #this
474
                    let parse_result = parse
475
                        #(.arg(&mut #required_arg_names))*
476
                        .not_required()
477
                        #(.arg(&mut #not_required_arg_names))*
478
                        .parse();
479
                    if parse_result.is_err() {
480
                        return;
481
                    }
482
                    #(#variadic_bindings)*
483

484
                    #call
485
                }
486
            }
487
        }
488
    }
293✔
489

490
    /// Whether this function is eligible for the zero-alloc fast path.
491
    /// Requires: no variadic parameters.
492
    fn is_fast_path_eligible(&self, call_type: &CallType) -> bool {
293✔
493
        let no_variadic = !self.args.typed.iter().any(|arg| arg.variadic);
293✔
494
        let supported_call_type = matches!(
293✔
495
            call_type,
91✔
496
            CallType::Function
497
                | CallType::Method {
498
                    receiver: MethodReceiver::Static
499
                        | MethodReceiver::Class
500
                        | MethodReceiver::ZendClassObject,
501
                    ..
502
                }
503
        );
504
        no_variadic && supported_call_type
293✔
505
    }
293✔
506

507
    /// Generates a zero-alloc fast path handler body.
508
    ///
509
    /// Instead of building `ArgParser` with `Vec`/`String` heap allocations,
510
    /// reads zvals directly from the call frame via pointer arithmetic
511
    /// and converts with `FromZvalMut` inline. Matches the pattern used by
512
    /// PHP's `ZEND_PARSE_PARAMETERS_START`/`END` C macros.
513
    fn restore_mutability(ty: &Type) -> Type {
17✔
514
        if let Type::Reference(r) = ty {
17✔
515
            let mut mref = r.clone();
13✔
516
            mref.mutability = Some(syn::token::Mut::default());
13✔
517
            Type::Reference(mref)
13✔
518
        } else {
519
            ty.clone()
4✔
520
        }
521
    }
17✔
522

523
    fn build_fast_arg_binding(i: usize, arg: &TypedArg<'_>, min_num_args: usize) -> TokenStream {
162✔
524
        let name = arg.name;
162✔
525
        let ty = arg.clean_ty();
162✔
526
        let zval_ident = format_ident!("__zval_{}", i);
162✔
527

528
        // parse_typed unwraps Option<T> → T and strips &mut → &.
529
        // Restore mutability for as_ref args so FromZvalMut resolves correctly.
530
        let convert_ty = if arg.as_ref {
162✔
531
            Self::restore_mutability(&ty)
16✔
532
        } else {
533
            ty.clone()
146✔
534
        };
535

536
        let binding_ty: Type = if !arg.nullable {
162✔
537
            ty.clone()
149✔
538
        } else if arg.as_ref {
13✔
539
            let mty = Self::restore_mutability(&ty);
1✔
540
            syn::parse_quote! { Option<#mty> }
1✔
541
        } else {
542
            syn::parse_quote! { Option<#ty> }
12✔
543
        };
544

545
        let read_zval = quote! {
162✔
546
            let #zval_ident = unsafe { ex.zend_call_arg(#i) };
547
            let Some(#zval_ident) = #zval_ident else { return; };
548
        };
549

550
        let from_zval = quote! {
162✔
551
            <#convert_ty as ::ext_php_rs::convert::FromZvalMut>::from_zval_mut(
552
                #zval_ident.dereference_mut()
553
            )
554
        };
555

556
        let convert = if arg.nullable {
162✔
557
            from_zval.clone()
13✔
558
        } else {
559
            quote! {
149✔
560
                match #from_zval {
561
                    Some(val) => val,
562
                    None => {
563
                        ::ext_php_rs::exception::PhpException::default(
564
                            concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
565
                        ).throw().expect("Failed to throw PHP exception.");
566
                        return;
567
                    }
568
                }
569
            }
570
        };
571

572
        let throw_invalid = quote! {
162✔
573
            ::ext_php_rs::exception::PhpException::default(
574
                concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
575
            ).throw().expect("Failed to throw PHP exception.");
576
            return;
577
        };
578

579
        let throw_null = quote! {
162✔
580
            ::ext_php_rs::exception::PhpException::new(
581
                concat!("Argument `$", stringify!(#name), "` must not be null").into(),
582
                0,
583
                ::ext_php_rs::zend::ce::type_error(),
584
            ).throw().expect("Failed to throw PHP exception.");
585
            return;
586
        };
587

588
        // Required arg — always present
589
        if i < min_num_args {
162✔
590
            return quote! {
144✔
591
                #read_zval
592
                let #name: #binding_ty = #convert;
593
            };
594
        }
18✔
595

596
        // Optional arg — may be omitted
597
        let fallback = match (&arg.default, arg.nullable) {
18✔
598
            (Some(expr), _) => quote! { #expr },
11✔
599
            (None, true) => quote! { None },
7✔
600
            (None, false) => throw_invalid.clone(),
×
601
        };
602

603
        // Non-nullable with default: explicit null must throw TypeError
604
        if !arg.nullable && arg.default.is_some() {
18✔
605
            return quote! {
7✔
606
                let #name: #binding_ty = if __num_args > #i {
607
                    #read_zval
608
                    if #zval_ident.is_null() { #throw_null }
609
                    #convert
610
                } else {
611
                    #fallback
612
                };
613
            };
614
        }
11✔
615

616
        quote! {
11✔
617
            let #name: #binding_ty = if __num_args > #i {
618
                #read_zval
619
                #convert
620
            } else {
621
                #fallback
622
            };
623
        }
624
    }
162✔
625

626
    fn build_fast_count_check(min_num_args: usize, max_num_args: usize) -> TokenStream {
282✔
627
        let min_u32 = u32::try_from(min_num_args).expect("too many args");
282✔
628
        let max_u32 = u32::try_from(max_num_args).expect("too many args");
282✔
629

630
        if min_num_args == max_num_args {
282✔
631
            quote! {
267✔
632
                let __num_args = unsafe { ex.This.u2.num_args } as usize;
633
                if __num_args != #min_num_args {
634
                    unsafe {
635
                        ::ext_php_rs::ffi::zend_wrong_parameters_count_error(#min_u32, #max_u32);
636
                    };
637
                    return;
638
                }
639
            }
640
        } else {
641
            quote! {
15✔
642
                let __num_args = unsafe { ex.This.u2.num_args } as usize;
643
                if !(#min_num_args..=#max_num_args).contains(&__num_args) {
644
                    unsafe {
645
                        ::ext_php_rs::ffi::zend_wrong_parameters_count_error(#min_u32, #max_u32);
646
                    };
647
                    return;
648
                }
649
            }
650
        }
651
    }
282✔
652

653
    fn build_fast_handler_body(&self, call_type: &CallType) -> TokenStream {
282✔
654
        let ident = self.ident;
282✔
655
        let (required, _not_required) = self.args.split_args(self.optional.as_ref());
282✔
656
        let min_num_args = required.len();
282✔
657
        let max_num_args = self.args.typed.len();
282✔
658

659
        // Arg count validation (matches zend_wrong_parameters_count_error)
660
        let count_check = Self::build_fast_count_check(min_num_args, max_num_args);
282✔
661

662
        let arg_bindings: Vec<TokenStream> = self
282✔
663
            .args
282✔
664
            .typed
282✔
665
            .iter()
282✔
666
            .enumerate()
282✔
667
            .map(|(i, arg)| Self::build_fast_arg_binding(i, arg, min_num_args))
282✔
668
            .collect();
282✔
669

670
        let arg_names: Vec<_> = self.args.typed.iter().map(|arg| arg.name).collect();
282✔
671

672
        let this_error = quote! {
282✔
673
            ::ext_php_rs::exception::PhpException::default(
674
                "Failed to retrieve reference to `$this`".into()
675
            ).throw().unwrap();
676
            return;
677
        };
678

679
        let returns_this = returns_self_ref(self.output);
282✔
680

681
        let (this_binding, call) = match call_type {
282✔
682
            CallType::Function => (quote! {}, quote! { #ident(#(#arg_names),*) }),
191✔
683
            CallType::Method {
684
                class,
13✔
685
                receiver: MethodReceiver::Static,
686
                ..
687
            } => (quote! {}, quote! { #class::#ident(#(#arg_names),*) }),
13✔
688
            CallType::Method {
689
                class,
74✔
690
                receiver: MethodReceiver::Class,
691
                ..
692
            } => (
693
                quote! {
74✔
694
                    let __this = match ex.get_object::<#class>() {
695
                        Some(v) => v,
696
                        None => { #this_error }
697
                    };
698
                },
699
                if returns_this {
74✔
700
                    quote! { let _ = __this.#ident(#(#arg_names),*); }
3✔
701
                } else {
702
                    quote! { __this.#ident(#(#arg_names),*) }
71✔
703
                },
704
            ),
705
            CallType::Method {
706
                class,
4✔
707
                receiver: MethodReceiver::ZendClassObject,
708
                ..
709
            } => (
710
                quote! {
4✔
711
                    let __this = match ex.get_object::<#class>() {
712
                        Some(v) => v,
713
                        None => { #this_error }
714
                    };
715
                },
716
                if returns_this {
4✔
717
                    quote! { { let _ = #class::#ident(__this, #(#arg_names),*); } }
×
718
                } else {
719
                    quote! { #class::#ident(__this, #(#arg_names),*) }
4✔
720
                },
721
            ),
722
        };
723

724
        if returns_this {
282✔
725
            quote! {
3✔
726
                use ::ext_php_rs::convert::{FromZvalMut, IntoZval};
727

728
                #count_check
729
                #(#arg_bindings)*
730
                #this_binding
731
                #call
732

733
                if let Err(e) = __this.set_zval(retval, false) {
734
                    let e: ::ext_php_rs::exception::PhpException = e.into();
735
                    e.throw().expect("Failed to throw PHP exception.");
736
                }
737
            }
738
        } else {
739
            quote! {
279✔
740
                use ::ext_php_rs::convert::{FromZvalMut, IntoZval};
741

742
                #count_check
743
                #(#arg_bindings)*
744
                #this_binding
745
                let __result = { #call };
746

747
                if let Err(e) = __result.set_zval(retval, false) {
748
                    let e: ::ext_php_rs::exception::PhpException = e.into();
749
                    e.throw().expect("Failed to throw PHP exception.");
750
                }
751
            }
752
        }
753
    }
282✔
754

755
    /// Generates a struct and impl for the `PhpFunction` trait.
756
    pub fn php_function_impl(&self) -> TokenStream {
202✔
757
        let internal_ident = self.internal_ident();
202✔
758
        let builder = self.function_builder(&CallType::Function);
202✔
759

760
        quote! {
202✔
761
            #[doc(hidden)]
762
            #[allow(non_camel_case_types)]
763
            struct #internal_ident;
764

765
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
766
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
767
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
768
                    {
769
                        #builder
770
                    }
771
                    entry
772
                };
773
            }
774
        }
775
    }
202✔
776

777
    /// Returns a constructor metadata object for this function. This doesn't
778
    /// check if the function is a constructor, however.
779
    pub fn constructor_meta(
31✔
780
        &self,
31✔
781
        class: &syn::Path,
31✔
782
        visibility: Option<&Visibility>,
31✔
783
    ) -> TokenStream {
31✔
784
        let ident = self.ident;
31✔
785
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
31✔
786
        let required_args = required
31✔
787
            .iter()
31✔
788
            .map(TypedArg::arg_builder)
31✔
789
            .collect::<Vec<_>>();
31✔
790
        let not_required_args = not_required
31✔
791
            .iter()
31✔
792
            .map(TypedArg::arg_builder)
31✔
793
            .collect::<Vec<_>>();
31✔
794

795
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
31✔
796
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
31✔
797
        let arg_declarations = self
31✔
798
            .args
31✔
799
            .typed
31✔
800
            .iter()
31✔
801
            .map(TypedArg::arg_declaration)
31✔
802
            .collect::<Vec<_>>();
31✔
803
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
31✔
804
            if arg.variadic {
19✔
805
                let name = arg.name;
×
806
                let variadic_name = format_ident!("__variadic_{}", name);
×
807
                let clean_ty = arg.clean_ty();
×
808
                Some(quote! {
×
809
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
810
                })
×
811
            } else {
812
                None
19✔
813
            }
814
        });
19✔
815
        let arg_accessors = self.args.typed.iter().map(|arg| {
31✔
816
            arg.accessor(
19✔
817
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
19✔
818
            )
819
        });
19✔
820
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
31✔
821
            quote! {
×
822
                .variadic()
823
            }
824
        });
×
825
        let docs = &self.docs;
31✔
826
        let flags = visibility.option_tokens();
31✔
827

828
        quote! {
31✔
829
            ::ext_php_rs::class::ConstructorMeta {
830
                constructor: {
831
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
832
                        use ::ext_php_rs::zend::try_catch;
833
                        use ::std::panic::AssertUnwindSafe;
834

835
                        // Wrap the constructor body with try_catch to ensure Rust destructors
836
                        // are called if a bailout occurs (issue #537)
837
                        let catch_result = try_catch(AssertUnwindSafe(|| {
838
                            #(#arg_declarations)*
839
                            let parse = ex.parser()
840
                                #(.arg(&mut #required_arg_names))*
841
                                .not_required()
842
                                #(.arg(&mut #not_required_arg_names))*
843
                                #variadic
844
                                .parse();
845
                            if parse.is_err() {
846
                                return ::ext_php_rs::class::ConstructorResult::ArgError;
847
                            }
848
                            #(#variadic_bindings)*
849
                            #class::#ident(#({#arg_accessors}),*).into()
850
                        }));
851

852
                        // If there was a bailout, run BailoutGuard cleanups and re-trigger
853
                        match catch_result {
854
                            Ok(result) => result,
855
                            Err(_) => {
856
                                ::ext_php_rs::zend::run_bailout_cleanups();
857
                                unsafe { ::ext_php_rs::zend::bailout() }
858
                            }
859
                        }
860
                    }
861
                    inner
862
                },
863
                build_fn: {
864
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
865
                        func
866
                            .docs(&[#(#docs),*])
867
                            #(.arg(#required_args))*
868
                            .not_required()
869
                            #(.arg(#not_required_args))*
870
                            #variadic
871
                    }
872
                    inner
873
                },
874
                flags: #flags
875
            }
876
        }
877
    }
31✔
878
}
879

880
#[derive(Debug)]
881
pub struct ReceiverArg {
882
    pub span: Span,
883
}
884

885
#[derive(Debug)]
886
pub struct TypedArg<'a> {
887
    pub name: &'a Ident,
888
    pub ty: Type,
889
    pub nullable: bool,
890
    pub default: Option<Expr>,
891
    pub as_ref: bool,
892
    pub variadic: bool,
893
}
894

895
#[derive(Debug)]
896
pub struct Args<'a> {
897
    pub receiver: Option<ReceiverArg>,
898
    pub typed: Vec<TypedArg<'a>>,
899
}
900

901
impl<'a> Args<'a> {
902
    pub fn parse_from_fnargs(
344✔
903
        args: impl Iterator<Item = &'a FnArg>,
344✔
904
        mut defaults: HashMap<Ident, Expr>,
344✔
905
    ) -> Result<Self> {
344✔
906
        let mut result = Self {
344✔
907
            receiver: None,
344✔
908
            typed: vec![],
344✔
909
        };
344✔
910
        for arg in args {
344✔
911
            match arg {
301✔
912
                FnArg::Receiver(receiver) => {
92✔
913
                    let syn::ReceiverKind::Reference(..) = &receiver.kind else {
92✔
914
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
2✔
915
                    };
916
                    if result.receiver.is_some() {
90✔
UNCOV
917
                        bail!(receiver => "Too many receivers specified.")
×
918
                    }
90✔
919
                    result.receiver.replace(ReceiverArg {
90✔
920
                        span: receiver.span(),
90✔
921
                    });
90✔
922
                }
923
                FnArg::Typed(PatType { pat, ty, .. }) => {
209✔
924
                    let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else {
209✔
925
                        bail!(pat => "Unsupported argument.");
×
926
                    };
927

928
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
929
                    let default = defaults.remove(ident);
209✔
930
                    let nullable = type_is_nullable(ty.as_ref())?;
209✔
931
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
209✔
932
                    result.typed.push(TypedArg {
209✔
933
                        name: ident,
209✔
934
                        ty,
209✔
935
                        nullable,
209✔
936
                        default,
209✔
937
                        as_ref,
209✔
938
                        variadic,
209✔
939
                    });
209✔
940
                }
941
            }
942
        }
943
        Ok(result)
342✔
944
    }
344✔
945

946
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
209✔
947
        match ty {
209✔
948
            Type::Reference(ref_) => {
62✔
949
                let as_ref = ref_.mutability.is_some();
62✔
950
                match ref_.elem.as_ref() {
62✔
951
                    Type::Slice(slice) => (
11✔
952
                        // TODO: Allow specifying the variadic type.
11✔
953
                        slice.elem.to_token_stream().to_string() == "& Zval",
11✔
954
                        as_ref,
11✔
955
                        ty.clone(),
11✔
956
                    ),
11✔
957
                    _ => (false, as_ref, ty.clone()),
51✔
958
                }
959
            }
960
            Type::Path(TypePath { path, .. }) => {
147✔
961
                let mut as_ref = false;
147✔
962

963
                // PhpRef<'a> explicitly requires PHP pass-by-reference.
964
                // Separated<'a> is handled by default (as_ref stays false).
965
                if path
147✔
966
                    .segments
147✔
967
                    .last()
147✔
968
                    .is_some_and(|seg| seg.ident == "PhpRef")
147✔
969
                {
4✔
970
                    as_ref = true;
4✔
971
                }
143✔
972

973
                // For for types that are `Option<&mut T>` to turn them into
974
                // `Option<&T>`, marking the Arg as as "passed by reference".
975
                let ty = path
147✔
976
                    .segments
147✔
977
                    .last()
147✔
978
                    .filter(|seg| seg.ident == "Option")
147✔
979
                    .and_then(|seg| {
147✔
980
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
14✔
981
                            args.args
14✔
982
                                .iter()
14✔
983
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
14✔
984
                                .and_then(|ga| match ga {
14✔
985
                                    GenericArgument::Type(ty) => Some(match ty {
14✔
986
                                        Type::Reference(r) => {
2✔
987
                                            // Only mark as_ref for mutable references
988
                                            // (Option<&mut T>), not immutable ones (Option<&T>)
989
                                            as_ref = r.mutability.is_some();
2✔
990
                                            let mut new_ref = r.clone();
2✔
991
                                            new_ref.mutability = None;
2✔
992
                                            Type::Reference(new_ref)
2✔
993
                                        }
994
                                        _ => ty.clone(),
12✔
995
                                    }),
996
                                    _ => None,
×
997
                                })
14✔
998
                        } else {
999
                            None
×
1000
                        }
1001
                    })
14✔
1002
                    .unwrap_or_else(|| ty.clone());
147✔
1003
                (false, as_ref, ty.clone())
147✔
1004
            }
1005
            _ => (false, false, ty.clone()),
×
1006
        }
1007
    }
209✔
1008

1009
    /// Splits the typed arguments into two slices:
1010
    ///
1011
    /// 1. Required arguments.
1012
    /// 2. Non-required arguments.
1013
    ///
1014
    /// # Parameters
1015
    ///
1016
    /// * `optional` - The first optional argument. If [`None`], the optional
1017
    ///   arguments will be from the first optional argument (nullable or has
1018
    ///   default) after the last required argument to the end of the arguments.
1019
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
622✔
1020
        let mut mid = None;
622✔
1021
        for (i, arg) in self.typed.iter().enumerate() {
622✔
1022
            // An argument is optional if it's nullable (Option<T>) or has a default value.
1023
            let is_optional = arg.nullable || arg.default.is_some();
367✔
1024
            if let Some(optional) = optional {
367✔
1025
                if optional == arg.name {
6✔
1026
                    mid.replace(i);
2✔
1027
                }
4✔
1028
            } else if mid.is_none() && is_optional {
361✔
1029
                mid.replace(i);
33✔
1030
            } else if !is_optional {
328✔
1031
                mid.take();
322✔
1032
            }
322✔
1033
        }
1034
        match mid {
622✔
1035
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
32✔
1036
            None => (&self.typed[..], &self.typed[0..0]),
590✔
1037
        }
1038
    }
622✔
1039
}
1040

1041
impl TypedArg<'_> {
1042
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
1043
    /// to be used outside of the original function context.
1044
    fn clean_ty(&self) -> Type {
576✔
1045
        let mut ty = self.ty.clone();
576✔
1046
        ty.drop_lifetimes();
576✔
1047

1048
        // Variadic arguments are passed as &[&Zval], so we need to extract the
1049
        // inner type.
1050
        if self.variadic {
576✔
1051
            let Type::Reference(reference) = &ty else {
33✔
1052
                return ty;
×
1053
            };
1054

1055
            if let Type::Slice(inner) = &*reference.elem {
33✔
1056
                return *inner.elem.clone();
33✔
1057
            }
×
1058
        }
543✔
1059

1060
        ty
543✔
1061
    }
576✔
1062

1063
    /// Returns a token stream containing an argument declaration, where the
1064
    /// name of the variable holding the arg is the name of the argument.
1065
    fn arg_declaration(&self) -> TokenStream {
198✔
1066
        let name = self.name;
198✔
1067
        let val = self.arg_builder();
198✔
1068
        quote! {
198✔
1069
            let mut #name = #val;
1070
        }
1071
    }
198✔
1072

1073
    /// Returns a token stream containing the `Arg` definition to be passed to
1074
    /// `ext-php-rs`.
1075
    fn arg_builder(&self) -> TokenStream {
403✔
1076
        let name = ident_to_php_name(self.name);
403✔
1077
        let ty = self.clean_ty();
403✔
1078
        let null = if self.nullable {
403✔
1079
            Some(quote! { .allow_null() })
28✔
1080
        } else {
1081
            None
375✔
1082
        };
1083
        let default = self.default.as_ref().map(|val| {
403✔
1084
            let val = expr_to_php_stub(val);
24✔
1085
            quote! {
24✔
1086
                .default(#val)
1087
            }
1088
        });
24✔
1089
        let as_ref = if self.as_ref {
403✔
1090
            Some(quote! { .as_ref() })
32✔
1091
        } else {
1092
            None
371✔
1093
        };
1094
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
403✔
1095
        quote! {
403✔
1096
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
1097
                #null
1098
                #default
1099
                #as_ref
1100
                #variadic
1101
        }
1102
    }
403✔
1103

1104
    /// Get the accessor used to access the value of the argument.
1105
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
198✔
1106
        let name = self.name;
198✔
1107
        if let Some(default) = &self.default {
198✔
1108
            if self.nullable {
11✔
1109
                // For nullable types with defaults, null is acceptable
1110
                quote! {
4✔
1111
                    #name.val().unwrap_or(#default.into())
1112
                }
1113
            } else {
1114
                // For non-nullable types with defaults:
1115
                // - If argument was omitted: use default
1116
                // - If null was explicitly passed: throw TypeError
1117
                // - If a value was passed: try to convert it
1118
                let bail_null = bail_fn(quote! {
7✔
1119
                    ::ext_php_rs::exception::PhpException::new(
7✔
1120
                        concat!("Argument `$", stringify!(#name), "` must not be null").into(),
7✔
1121
                        0,
7✔
1122
                        ::ext_php_rs::zend::ce::type_error(),
7✔
1123
                    )
7✔
1124
                });
7✔
1125
                let bail_invalid = bail_fn(quote! {
7✔
1126
                    ::ext_php_rs::exception::PhpException::default(
7✔
1127
                        concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
7✔
1128
                    )
7✔
1129
                });
7✔
1130
                quote! {
7✔
1131
                    match #name.zval() {
1132
                        Some(zval) if zval.is_null() => {
1133
                            // Null was explicitly passed to a non-nullable parameter
1134
                            #bail_null
1135
                        }
1136
                        Some(_) => {
1137
                            // A value was passed, try to convert it
1138
                            match #name.val() {
1139
                                Some(val) => val,
1140
                                None => {
1141
                                    #bail_invalid
1142
                                }
1143
                            }
1144
                        }
1145
                        None => {
1146
                            // Argument was omitted, use default
1147
                            #default.into()
1148
                        }
1149
                    }
1150
                }
1151
            }
1152
        } else if self.variadic {
187✔
1153
            let variadic_name = format_ident!("__variadic_{}", name);
11✔
1154
            quote! {
11✔
1155
                #variadic_name.as_slice()
1156
            }
1157
        } else if self.nullable {
176✔
1158
            // Originally I thought we could just use the below case for `null` options, as
1159
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
1160
            // the argument isn't given, as the underlying zval is null.
1161
            quote! {
10✔
1162
                #name.val()
1163
            }
1164
        } else {
1165
            let bail = bail_fn(quote! {
166✔
1166
                ::ext_php_rs::exception::PhpException::default(
166✔
1167
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
166✔
1168
                )
166✔
1169
            });
166✔
1170
            quote! {
166✔
1171
                match #name.val() {
1172
                    Some(val) => val,
1173
                    None => {
1174
                        #bail;
1175
                    }
1176
                }
1177
            }
1178
        }
1179
    }
198✔
1180
}
1181

1182
/// Converts a Rust expression to a PHP stub-compatible default value string.
1183
///
1184
/// This function handles common Rust patterns and converts them to valid PHP
1185
/// syntax for use in generated stub files:
1186
///
1187
/// - `None` → `"null"`
1188
/// - `Some(expr)` → converts the inner expression
1189
/// - `42`, `3.14` → numeric literals as-is
1190
/// - `true`/`false` → as-is
1191
/// - `"string"` → `"string"`
1192
/// - `"string".to_string()` or `String::from("string")` → `"string"`
1193
fn expr_to_php_stub(expr: &Expr) -> String {
42✔
1194
    match expr {
42✔
1195
        // Handle None -> null
1196
        Expr::Path(path) => {
7✔
1197
            let path_str = path.path.to_token_stream().to_string();
7✔
1198
            if path_str == "None" {
7✔
1199
                "null".to_string()
7✔
1200
            } else if path_str == "true" || path_str == "false" {
×
1201
                path_str
×
1202
            } else {
1203
                // For other paths (constants, etc.), use the raw representation
1204
                path_str
×
1205
            }
1206
        }
1207

1208
        // Handle Some(expr) -> convert inner expression
1209
        Expr::Call(call) => {
3✔
1210
            if let Expr::Path(func_path) = &*call.func {
3✔
1211
                let func_name = func_path.path.to_token_stream().to_string();
3✔
1212

1213
                // Some(value) -> convert inner value
1214
                if func_name == "Some"
3✔
1215
                    && let Some(arg) = call.args.first()
3✔
1216
                {
1217
                    return expr_to_php_stub(arg);
3✔
1218
                }
×
1219

1220
                // String::from("...") -> "..."
1221
                if (func_name == "String :: from" || func_name == "String::from")
×
1222
                    && let Some(arg) = call.args.first()
×
1223
                {
1224
                    return expr_to_php_stub(arg);
×
1225
                }
×
1226
            }
×
1227

1228
            // Default: use raw representation
1229
            expr.to_token_stream().to_string()
×
1230
        }
1231

1232
        // Handle method calls like "string".to_string()
1233
        Expr::MethodCall(method_call) => {
2✔
1234
            let method_name = method_call.method.to_string();
2✔
1235

1236
            // "...".to_string() or "...".to_owned() or "...".into() -> "..."
1237
            if method_name == "to_string" || method_name == "to_owned" || method_name == "into" {
2✔
1238
                return expr_to_php_stub(&method_call.receiver);
2✔
1239
            }
×
1240

1241
            // Default: use raw representation
1242
            expr.to_token_stream().to_string()
×
1243
        }
1244

1245
        // String literals -> keep as-is (already valid PHP)
1246
        Expr::Lit(lit) => match &lit.lit {
28✔
1247
            syn::Lit::Str(s) => format!(
2✔
1248
                "\"{}\"",
1249
                s.value().replace('\\', "\\\\").replace('"', "\\\"")
2✔
1250
            ),
1251
            // Use base10_digits() to strip Rust type suffixes like _usize, _i32, etc.
1252
            syn::Lit::Int(i) => i.base10_digits().to_string(),
22✔
1253
            syn::Lit::Float(f) => f.base10_digits().to_string(),
4✔
1254
            syn::Lit::Bool(b) => if b.value { "true" } else { "false" }.to_string(),
×
1255
            syn::Lit::Char(c) => format!("\"{}\"", c.value()),
×
1256
            _ => expr.to_token_stream().to_string(),
×
1257
        },
1258

1259
        // Handle arrays: [] or vec![]
1260
        Expr::Array(arr) => {
×
1261
            if arr.elems.is_empty() {
×
1262
                "[]".to_string()
×
1263
            } else {
1264
                let elems: Vec<String> = arr.elems.iter().map(expr_to_php_stub).collect();
×
1265
                format!("[{}]", elems.join(", "))
×
1266
            }
1267
        }
1268

1269
        // Handle vec![] macro
1270
        Expr::Macro(m) => {
×
1271
            let macro_name = m.mac.path.to_token_stream().to_string();
×
1272
            if macro_name == "vec" {
×
1273
                let tokens = m.mac.tokens.to_string();
×
1274
                if tokens.trim().is_empty() {
×
1275
                    return "[]".to_string();
×
1276
                }
×
1277
            }
×
1278
            // Default: use raw representation
1279
            expr.to_token_stream().to_string()
×
1280
        }
1281

1282
        // Handle unary expressions like -42
1283
        Expr::Unary(unary) => {
2✔
1284
            let inner = expr_to_php_stub(&unary.expr);
2✔
1285
            format!("{}{}", unary.op.to_token_stream(), inner)
2✔
1286
        }
1287

1288
        // Default: use raw representation
1289
        _ => expr.to_token_stream().to_string(),
×
1290
    }
1291
}
42✔
1292

1293
/// Returns true if the given type is nullable in PHP (i.e., it's an
1294
/// `Option<T>`).
1295
///
1296
/// Note: Having a default value does NOT make a type nullable. A parameter with
1297
/// a default value is optional (can be omitted), but passing `null` explicitly
1298
/// should still be rejected unless the type is `Option<T>`.
1299
// TODO(david): Eventually move to compile-time constants for this (similar to
1300
// FromZval::NULLABLE).
1301
pub fn type_is_nullable(ty: &Type) -> Result<bool> {
209✔
1302
    Ok(match ty {
209✔
1303
        Type::Path(path) => path
147✔
1304
            .path
147✔
1305
            .segments
147✔
1306
            .iter()
147✔
1307
            .next_back()
147✔
1308
            .is_some_and(|seg| seg.ident == "Option"),
147✔
1309
        Type::Reference(_) => false, /* Reference cannot be nullable unless */
62✔
1310
        // wrapped in `Option` (in that case it'd be a Path).
1311
        _ => bail!(ty => "Unsupported argument type."),
×
1312
    })
1313
}
209✔
1314

1315
#[cfg(test)]
1316
mod tests {
1317
    use super::*;
1318

1319
    #[test]
1320
    fn test_only_reference_receivers_are_accepted() {
1✔
1321
        let by_ref: FnArg = syn::parse_quote!(&self);
1✔
1322
        let by_mut: FnArg = syn::parse_quote!(&mut self);
1✔
1323
        let by_value: FnArg = syn::parse_quote!(self);
1✔
1324
        let boxed: FnArg = syn::parse_quote!(self: Box<Self>);
1✔
1325

1326
        assert!(Args::parse_from_fnargs([&by_ref].into_iter(), HashMap::new()).is_ok());
1✔
1327
        assert!(Args::parse_from_fnargs([&by_mut].into_iter(), HashMap::new()).is_ok());
1✔
1328
        assert!(Args::parse_from_fnargs([&by_value].into_iter(), HashMap::new()).is_err());
1✔
1329
        assert!(Args::parse_from_fnargs([&boxed].into_iter(), HashMap::new()).is_err());
1✔
1330
    }
1✔
1331

1332
    #[test]
1333
    fn test_expr_to_php_stub_strips_numeric_suffixes() {
1✔
1334
        // Test integer suffixes are stripped (issue #492)
1335
        let expr: Expr = syn::parse_quote!(42_usize);
1✔
1336
        assert_eq!(expr_to_php_stub(&expr), "42");
1✔
1337

1338
        let expr: Expr = syn::parse_quote!(42_i32);
1✔
1339
        assert_eq!(expr_to_php_stub(&expr), "42");
1✔
1340

1341
        let expr: Expr = syn::parse_quote!(42_u64);
1✔
1342
        assert_eq!(expr_to_php_stub(&expr), "42");
1✔
1343

1344
        // Test float suffixes are stripped
1345
        let expr: Expr = syn::parse_quote!(3.14_f64);
1✔
1346
        assert_eq!(expr_to_php_stub(&expr), "3.14");
1✔
1347

1348
        let expr: Expr = syn::parse_quote!(3.14_f32);
1✔
1349
        assert_eq!(expr_to_php_stub(&expr), "3.14");
1✔
1350

1351
        // Test literals without suffixes still work
1352
        let expr: Expr = syn::parse_quote!(42);
1✔
1353
        assert_eq!(expr_to_php_stub(&expr), "42");
1✔
1354

1355
        let expr: Expr = syn::parse_quote!(3.14);
1✔
1356
        assert_eq!(expr_to_php_stub(&expr), "3.14");
1✔
1357
    }
1✔
1358

1359
    #[test]
1360
    fn test_expr_to_php_stub_negative_numbers() {
1✔
1361
        let expr: Expr = syn::parse_quote!(-42_i32);
1✔
1362
        assert_eq!(expr_to_php_stub(&expr), "-42");
1✔
1363

1364
        let expr: Expr = syn::parse_quote!(-3.14_f64);
1✔
1365
        assert_eq!(expr_to_php_stub(&expr), "-3.14");
1✔
1366
    }
1✔
1367

1368
    #[test]
1369
    fn test_expr_to_php_stub_none_and_some() {
1✔
1370
        let expr: Expr = syn::parse_quote!(None);
1✔
1371
        assert_eq!(expr_to_php_stub(&expr), "null");
1✔
1372

1373
        let expr: Expr = syn::parse_quote!(Some(42_usize));
1✔
1374
        assert_eq!(expr_to_php_stub(&expr), "42");
1✔
1375
    }
1✔
1376
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc