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

extphprs / ext-php-rs / 20433243175

22 Dec 2025 01:25PM UTC coverage: 35.47% (-0.6%) from 36.059%
20433243175

push

github

web-flow
fix(macro): nullable parameters #538 (#617)

30 of 149 new or added lines in 6 files covered. (20.13%)

3 existing lines in 1 file now uncovered.

1693 of 4773 relevant lines covered (35.47%)

12.43 hits per line

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

20.62
/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;
×
210

211
                        #(#arg_declarations)*
×
212
                        let result = {
×
213
                            #result
×
214
                        };
215

216
                        if let Err(e) = result.set_zval(retval, false) {
×
217
                            let e: ::ext_php_rs::exception::PhpException = e.into();
×
218
                            e.throw().expect("Failed to throw PHP exception.");
×
219
                        }
220
                    }
221
                }
222
                handler
×
223
            })
224
            #(.arg(#required_args))*
×
225
            .not_required()
×
226
            #(.arg(#not_required_args))*
×
227
            #returns
×
228
            #docs
×
229
        }
230
    }
231

232
    fn build_returns(&self) -> Option<TokenStream> {
3✔
233
        self.output.cloned().map(|mut output| {
12✔
234
            output.drop_lifetimes();
6✔
235
            quote! {
3✔
236
                .returns(
×
237
                    <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
×
238
                    false,
×
239
                    <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
240
                )
241
            }
242
        })
243
    }
244

245
    fn build_result(
×
246
        &self,
247
        call_type: CallType,
248
        required: &[TypedArg<'_>],
249
        not_required: &[TypedArg<'_>],
250
    ) -> TokenStream {
251
        let ident = self.ident;
×
252
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
253
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
254

255
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
×
256
            if arg.variadic {
×
257
                let name = arg.name;
×
258
                let variadic_name = format_ident!("__variadic_{}", name);
×
259
                let clean_ty = arg.clean_ty();
×
260
                Some(quote! {
×
261
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
262
                })
263
            } else {
264
                None
×
265
            }
266
        });
267

268
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
269
            arg.accessor(|e| {
×
270
                quote! {
×
271
                    #e.throw().expect("Failed to throw PHP exception.");
×
272
                    return;
×
273
                }
274
            })
275
        });
276

277
        match call_type {
×
278
            CallType::Function => quote! {
×
279
                let parse = ex.parser()
×
280
                    #(.arg(&mut #required_arg_names))*
×
281
                    .not_required()
×
282
                    #(.arg(&mut #not_required_arg_names))*
×
283
                    .parse();
×
284
                if parse.is_err() {
×
285
                    return;
×
286
                }
287
                #(#variadic_bindings)*
×
288

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

330
                    #call
×
331
                }
332
            }
333
        }
334
    }
335

336
    /// Generates a struct and impl for the `PhpFunction` trait.
337
    pub fn php_function_impl(&self) -> TokenStream {
×
338
        let internal_ident = self.internal_ident();
×
339
        let builder = self.function_builder(CallType::Function);
×
340

341
        quote! {
×
342
            #[doc(hidden)]
×
343
            #[allow(non_camel_case_types)]
×
344
            struct #internal_ident;
×
345

346
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
×
347
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
×
348
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
×
349
                    {
350
                        #builder
×
351
                    }
352
                    entry
×
353
                };
354
            }
355
        }
356
    }
357

358
    /// Returns a constructor metadata object for this function. This doesn't
359
    /// check if the function is a constructor, however.
360
    pub fn constructor_meta(
×
361
        &self,
362
        class: &syn::Path,
363
        visibility: Option<&Visibility>,
364
    ) -> TokenStream {
365
        let ident = self.ident;
×
366
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
367
        let required_args = required
×
368
            .iter()
369
            .map(TypedArg::arg_builder)
×
370
            .collect::<Vec<_>>();
371
        let not_required_args = not_required
×
372
            .iter()
373
            .map(TypedArg::arg_builder)
×
374
            .collect::<Vec<_>>();
375

376
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
377
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
378
        let arg_declarations = self
×
379
            .args
×
380
            .typed
×
381
            .iter()
382
            .map(TypedArg::arg_declaration)
×
383
            .collect::<Vec<_>>();
384
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
×
385
            if arg.variadic {
×
386
                let name = arg.name;
×
387
                let variadic_name = format_ident!("__variadic_{}", name);
×
388
                let clean_ty = arg.clean_ty();
×
389
                Some(quote! {
×
390
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
391
                })
392
            } else {
393
                None
×
394
            }
395
        });
396
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
397
            arg.accessor(
×
398
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
×
399
            )
400
        });
401
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
×
402
            quote! {
×
403
                .variadic()
×
404
            }
405
        });
406
        let docs = &self.docs;
×
407
        let flags = visibility.option_tokens();
×
408

409
        quote! {
×
410
            ::ext_php_rs::class::ConstructorMeta {
×
411
                constructor: {
×
412
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
×
413
                        #(#arg_declarations)*
×
414
                        let parse = ex.parser()
×
415
                            #(.arg(&mut #required_arg_names))*
×
416
                            .not_required()
×
417
                            #(.arg(&mut #not_required_arg_names))*
×
418
                            #variadic
×
419
                            .parse();
×
420
                        if parse.is_err() {
×
421
                            return ::ext_php_rs::class::ConstructorResult::ArgError;
×
422
                        }
423
                        #(#variadic_bindings)*
×
424
                        #class::#ident(#({#arg_accessors}),*).into()
×
425
                    }
426
                    inner
×
427
                },
428
                build_fn: {
×
429
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
×
430
                        func
×
431
                            .docs(&[#(#docs),*])
×
432
                            #(.arg(#required_args))*
×
433
                            .not_required()
×
434
                            #(.arg(#not_required_args))*
×
435
                            #variadic
×
436
                    }
437
                    inner
×
438
                },
439
                flags: #flags
×
440
            }
441
        }
442
    }
443
}
444

445
#[derive(Debug)]
446
pub struct ReceiverArg {
447
    pub _mutable: bool,
448
    pub span: Span,
449
}
450

451
#[derive(Debug)]
452
pub struct TypedArg<'a> {
453
    pub name: &'a Ident,
454
    pub ty: Type,
455
    pub nullable: bool,
456
    pub default: Option<Expr>,
457
    pub as_ref: bool,
458
    pub variadic: bool,
459
}
460

461
#[derive(Debug)]
462
pub struct Args<'a> {
463
    pub receiver: Option<ReceiverArg>,
464
    pub typed: Vec<TypedArg<'a>>,
465
}
466

467
impl<'a> Args<'a> {
468
    pub fn parse_from_fnargs(
3✔
469
        args: impl Iterator<Item = &'a FnArg>,
470
        mut defaults: HashMap<Ident, Expr>,
471
    ) -> Result<Self> {
472
        let mut result = Self {
473
            receiver: None,
474
            typed: vec![],
3✔
475
        };
476
        for arg in args {
13✔
477
            match arg {
5✔
478
                FnArg::Receiver(receiver) => {
3✔
479
                    if receiver.reference.is_none() {
6✔
480
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
×
481
                    } else if result.receiver.is_some() {
6✔
482
                        bail!(receiver => "Too many receivers specified.")
×
483
                    }
484
                    result.receiver.replace(ReceiverArg {
9✔
485
                        _mutable: receiver.mutability.is_some(),
9✔
486
                        span: receiver.span(),
3✔
487
                    });
488
                }
489
                FnArg::Typed(PatType { pat, ty, .. }) => {
4✔
490
                    let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else {
4✔
491
                        bail!(pat => "Unsupported argument.");
×
492
                    };
493

494
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
495
                    let default = defaults.remove(ident);
8✔
496
                    let nullable = type_is_nullable(ty.as_ref())?;
6✔
497
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
8✔
498
                    result.typed.push(TypedArg {
6✔
499
                        name: ident,
4✔
500
                        ty,
4✔
501
                        nullable,
4✔
502
                        default,
4✔
503
                        as_ref,
2✔
504
                        variadic,
2✔
505
                    });
506
                }
507
            }
508
        }
509
        Ok(result)
3✔
510
    }
511

512
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
2✔
513
        match ty {
2✔
514
            Type::Reference(ref_) => {
×
515
                let as_ref = ref_.mutability.is_some();
×
516
                match ref_.elem.as_ref() {
×
517
                    Type::Slice(slice) => (
×
518
                        // TODO: Allow specifying the variadic type.
519
                        slice.elem.to_token_stream().to_string() == "& Zval",
×
520
                        as_ref,
×
521
                        ty.clone(),
×
522
                    ),
523
                    _ => (false, as_ref, ty.clone()),
×
524
                }
525
            }
526
            Type::Path(TypePath { path, .. }) => {
2✔
527
                let mut as_ref = false;
4✔
528

529
                // For for types that are `Option<&mut T>` to turn them into
530
                // `Option<&T>`, marking the Arg as as "passed by reference".
531
                let ty = path
4✔
532
                    .segments
2✔
533
                    .last()
534
                    .filter(|seg| seg.ident == "Option")
6✔
535
                    .and_then(|seg| {
2✔
536
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
×
537
                            args.args
×
538
                                .iter()
×
539
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
×
540
                                .and_then(|ga| match ga {
×
541
                                    GenericArgument::Type(ty) => Some(match ty {
×
542
                                        Type::Reference(r) => {
×
543
                                            // Only mark as_ref for mutable references
544
                                            // (Option<&mut T>), not immutable ones (Option<&T>)
545
                                            as_ref = r.mutability.is_some();
×
546
                                            let mut new_ref = r.clone();
×
547
                                            new_ref.mutability = None;
×
548
                                            Type::Reference(new_ref)
×
549
                                        }
550
                                        _ => ty.clone(),
×
551
                                    }),
552
                                    _ => None,
×
553
                                })
554
                        } else {
555
                            None
×
556
                        }
557
                    })
558
                    .unwrap_or_else(|| ty.clone());
6✔
559
                (false, as_ref, ty.clone())
4✔
560
            }
561
            _ => (false, false, ty.clone()),
×
562
        }
563
    }
564

565
    /// Splits the typed arguments into two slices:
566
    ///
567
    /// 1. Required arguments.
568
    /// 2. Non-required arguments.
569
    ///
570
    /// # Parameters
571
    ///
572
    /// * `optional` - The first optional argument. If [`None`], the optional
573
    ///   arguments will be from the first optional argument (nullable or has
574
    ///   default) after the last required argument to the end of the arguments.
575
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
3✔
576
        let mut mid = None;
6✔
577
        for (i, arg) in self.typed.iter().enumerate() {
10✔
578
            // An argument is optional if it's nullable (Option<T>) or has a default value.
579
            let is_optional = arg.nullable || arg.default.is_some();
8✔
580
            if let Some(optional) = optional {
2✔
581
                if optional == arg.name {
×
582
                    mid.replace(i);
×
583
                }
584
            } else if mid.is_none() && is_optional {
6✔
585
                mid.replace(i);
×
586
            } else if !is_optional {
4✔
587
                mid.take();
2✔
588
            }
589
        }
590
        match mid {
3✔
591
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
×
592
            None => (&self.typed[..], &self.typed[0..0]),
6✔
593
        }
594
    }
595
}
596

597
impl TypedArg<'_> {
598
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
599
    /// to be used outside of the original function context.
600
    fn clean_ty(&self) -> Type {
2✔
601
        let mut ty = self.ty.clone();
6✔
602
        ty.drop_lifetimes();
4✔
603

604
        // Variadic arguments are passed as &[&Zval], so we need to extract the
605
        // inner type.
606
        if self.variadic {
2✔
607
            let Type::Reference(reference) = &ty else {
×
608
                return ty;
×
609
            };
610

611
            if let Type::Slice(inner) = &*reference.elem {
×
612
                return *inner.elem.clone();
×
613
            }
614
        }
615

616
        ty
2✔
617
    }
618

619
    /// Returns a token stream containing an argument declaration, where the
620
    /// name of the variable holding the arg is the name of the argument.
621
    fn arg_declaration(&self) -> TokenStream {
×
622
        let name = self.name;
×
623
        let val = self.arg_builder();
×
624
        quote! {
×
625
            let mut #name = #val;
626
        }
627
    }
628

629
    /// Returns a token stream containing the `Arg` definition to be passed to
630
    /// `ext-php-rs`.
631
    fn arg_builder(&self) -> TokenStream {
2✔
632
        let name = self.name.to_string();
6✔
633
        let ty = self.clean_ty();
6✔
634
        let null = if self.nullable {
4✔
635
            Some(quote! { .allow_null() })
×
636
        } else {
637
            None
2✔
638
        };
639
        let default = self.default.as_ref().map(|val| {
8✔
NEW
640
            let val = expr_to_php_stub(val);
×
641
            quote! {
×
642
                .default(#val)
643
            }
644
        });
645
        let as_ref = if self.as_ref {
4✔
646
            Some(quote! { .as_ref() })
×
647
        } else {
648
            None
2✔
649
        };
650
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
6✔
651
        quote! {
2✔
652
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
653
                #null
654
                #default
655
                #as_ref
656
                #variadic
657
        }
658
    }
659

660
    /// Get the accessor used to access the value of the argument.
661
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
×
662
        let name = self.name;
×
663
        if let Some(default) = &self.default {
×
NEW
664
            if self.nullable {
×
665
                // For nullable types with defaults, null is acceptable
NEW
666
                quote! {
×
NEW
667
                    #name.val().unwrap_or(#default.into())
×
668
                }
669
            } else {
670
                // For non-nullable types with defaults:
671
                // - If argument was omitted: use default
672
                // - If null was explicitly passed: throw TypeError
673
                // - If a value was passed: try to convert it
NEW
674
                let bail_null = bail_fn(quote! {
×
NEW
675
                    ::ext_php_rs::exception::PhpException::new(
×
NEW
676
                        concat!("Argument `$", stringify!(#name), "` must not be null").into(),
×
NEW
677
                        0,
×
NEW
678
                        ::ext_php_rs::zend::ce::type_error(),
×
679
                    )
680
                });
NEW
681
                let bail_invalid = bail_fn(quote! {
×
NEW
682
                    ::ext_php_rs::exception::PhpException::default(
×
NEW
683
                        concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
684
                    )
685
                });
NEW
686
                quote! {
×
NEW
687
                    match #name.zval() {
×
NEW
688
                        Some(zval) if zval.is_null() => {
×
689
                            // Null was explicitly passed to a non-nullable parameter
NEW
690
                            #bail_null
×
691
                        }
NEW
692
                        Some(_) => {
×
693
                            // A value was passed, try to convert it
NEW
694
                            match #name.val() {
×
NEW
695
                                Some(val) => val,
×
NEW
696
                                None => {
×
NEW
697
                                    #bail_invalid
×
698
                                }
699
                            }
700
                        }
NEW
701
                        None => {
×
702
                            // Argument was omitted, use default
NEW
703
                            #default.into()
×
704
                        }
705
                    }
706
                }
707
            }
708
        } else if self.variadic {
×
709
            let variadic_name = format_ident!("__variadic_{}", name);
×
710
            quote! {
×
711
                #variadic_name.as_slice()
×
712
            }
713
        } else if self.nullable {
×
714
            // Originally I thought we could just use the below case for `null` options, as
715
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
716
            // the argument isn't given, as the underlying zval is null.
717
            quote! {
×
718
                #name.val()
×
719
            }
720
        } else {
721
            let bail = bail_fn(quote! {
×
722
                ::ext_php_rs::exception::PhpException::default(
×
723
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
724
                )
725
            });
726
            quote! {
×
727
                match #name.val() {
×
728
                    Some(val) => val,
×
729
                    None => {
×
730
                        #bail;
×
731
                    }
732
                }
733
            }
734
        }
735
    }
736
}
737

738
/// Converts a Rust expression to a PHP stub-compatible default value string.
739
///
740
/// This function handles common Rust patterns and converts them to valid PHP
741
/// syntax for use in generated stub files:
742
///
743
/// - `None` → `"null"`
744
/// - `Some(expr)` → converts the inner expression
745
/// - `42`, `3.14` → numeric literals as-is
746
/// - `true`/`false` → as-is
747
/// - `"string"` → `"string"`
748
/// - `"string".to_string()` or `String::from("string")` → `"string"`
NEW
749
fn expr_to_php_stub(expr: &Expr) -> String {
×
NEW
750
    match expr {
×
751
        // Handle None -> null
NEW
752
        Expr::Path(path) => {
×
NEW
753
            let path_str = path.path.to_token_stream().to_string();
×
NEW
754
            if path_str == "None" {
×
NEW
755
                "null".to_string()
×
NEW
756
            } else if path_str == "true" || path_str == "false" {
×
NEW
757
                path_str
×
758
            } else {
759
                // For other paths (constants, etc.), use the raw representation
NEW
760
                path_str
×
761
            }
762
        }
763

764
        // Handle Some(expr) -> convert inner expression
NEW
765
        Expr::Call(call) => {
×
NEW
766
            if let Expr::Path(func_path) = &*call.func {
×
NEW
767
                let func_name = func_path.path.to_token_stream().to_string();
×
768

769
                // Some(value) -> convert inner value
NEW
770
                if func_name == "Some"
×
NEW
771
                    && let Some(arg) = call.args.first()
×
772
                {
NEW
773
                    return expr_to_php_stub(arg);
×
774
                }
775

776
                // String::from("...") -> "..."
NEW
777
                if (func_name == "String :: from" || func_name == "String::from")
×
NEW
778
                    && let Some(arg) = call.args.first()
×
779
                {
NEW
780
                    return expr_to_php_stub(arg);
×
781
                }
782
            }
783

784
            // Default: use raw representation
NEW
785
            expr.to_token_stream().to_string()
×
786
        }
787

788
        // Handle method calls like "string".to_string()
NEW
789
        Expr::MethodCall(method_call) => {
×
NEW
790
            let method_name = method_call.method.to_string();
×
791

792
            // "...".to_string() or "...".to_owned() or "...".into() -> "..."
NEW
793
            if method_name == "to_string" || method_name == "to_owned" || method_name == "into" {
×
NEW
794
                return expr_to_php_stub(&method_call.receiver);
×
795
            }
796

797
            // Default: use raw representation
NEW
798
            expr.to_token_stream().to_string()
×
799
        }
800

801
        // String literals -> keep as-is (already valid PHP)
NEW
802
        Expr::Lit(lit) => match &lit.lit {
×
NEW
803
            syn::Lit::Str(s) => format!(
×
804
                "\"{}\"",
NEW
805
                s.value().replace('\\', "\\\\").replace('"', "\\\"")
×
806
            ),
NEW
807
            syn::Lit::Int(i) => i.to_string(),
×
NEW
808
            syn::Lit::Float(f) => f.to_string(),
×
NEW
809
            syn::Lit::Bool(b) => if b.value { "true" } else { "false" }.to_string(),
×
NEW
810
            syn::Lit::Char(c) => format!("\"{}\"", c.value()),
×
NEW
811
            _ => expr.to_token_stream().to_string(),
×
812
        },
813

814
        // Handle arrays: [] or vec![]
NEW
815
        Expr::Array(arr) => {
×
NEW
816
            if arr.elems.is_empty() {
×
NEW
817
                "[]".to_string()
×
818
            } else {
NEW
819
                let elems: Vec<String> = arr.elems.iter().map(expr_to_php_stub).collect();
×
NEW
820
                format!("[{}]", elems.join(", "))
×
821
            }
822
        }
823

824
        // Handle vec![] macro
NEW
825
        Expr::Macro(m) => {
×
NEW
826
            let macro_name = m.mac.path.to_token_stream().to_string();
×
NEW
827
            if macro_name == "vec" {
×
NEW
828
                let tokens = m.mac.tokens.to_string();
×
NEW
829
                if tokens.trim().is_empty() {
×
NEW
830
                    return "[]".to_string();
×
831
                }
832
            }
833
            // Default: use raw representation
NEW
834
            expr.to_token_stream().to_string()
×
835
        }
836

837
        // Handle unary expressions like -42
NEW
838
        Expr::Unary(unary) => {
×
NEW
839
            let inner = expr_to_php_stub(&unary.expr);
×
NEW
840
            format!("{}{}", unary.op.to_token_stream(), inner)
×
841
        }
842

843
        // Default: use raw representation
NEW
844
        _ => expr.to_token_stream().to_string(),
×
845
    }
846
}
847

848
/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
849
///
850
/// Note: Having a default value does NOT make a type nullable. A parameter with
851
/// a default value is optional (can be omitted), but passing `null` explicitly
852
/// should still be rejected unless the type is `Option<T>`.
853
// TODO(david): Eventually move to compile-time constants for this (similar to
854
// FromZval::NULLABLE).
855
pub fn type_is_nullable(ty: &Type) -> Result<bool> {
2✔
856
    Ok(match ty {
2✔
857
        Type::Path(path) => path
4✔
858
            .path
2✔
859
            .segments
2✔
860
            .iter()
2✔
861
            .next_back()
2✔
862
            .is_some_and(|seg| seg.ident == "Option"),
6✔
NEW
863
        Type::Reference(_) => false, /* Reference cannot be nullable unless */
×
864
        // wrapped in `Option` (in that case it'd be a Path).
865
        _ => bail!(ty => "Unsupported argument type."),
×
866
    })
867
}
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