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

extphprs / ext-php-rs / 20436024906

22 Dec 2025 03:17PM UTC coverage: 35.249% (-0.2%) from 35.47%
20436024906

push

github

web-flow
fix(class): Return Self ($this) #502 (#626)

14 of 81 new or added lines in 2 files covered. (17.28%)

2 existing lines in 1 file now uncovered.

1705 of 4837 relevant lines covered (35.25%)

12.28 hits per line

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

20.37
/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::{PhpRename, RenameRule, Visibility};
11
use crate::prelude::*;
12
use crate::syn_ext::DropLifetimes;
13

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

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

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

51
    Ok(quote! {{
×
52
        (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)()
×
53
    }})
54
}
55

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

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

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

76
    let docs = get_docs(&php_attr.attrs)?;
×
77

78
    let func = Function::new(
79
        &input.sig,
×
80
        php_attr
×
81
            .rename
×
82
            .rename(input.sig.ident.to_string(), RenameRule::Snake),
×
83
        args,
×
84
        php_attr.optional,
×
85
        docs,
×
86
    );
87
    let function_impl = func.php_function_impl();
×
88

89
    Ok(quote! {
×
90
        #input
×
91
        #function_impl
×
92
    })
93
}
94

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

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

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

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

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

165
    pub fn abstract_function_builder(&self) -> TokenStream {
3✔
166
        let name = &self.name;
6✔
167
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
15✔
168

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

179
        let returns = self.build_returns(None);
12✔
180
        let docs = if self.docs.is_empty() {
9✔
181
            quote! {}
2✔
182
        } else {
183
            let docs = &self.docs;
2✔
184
            quote! {
1✔
185
                .docs(&[#(#docs),*])
×
186
            }
187
        };
188

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

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

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

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

NEW
222
        let returns = self.build_returns(Some(call_type));
×
223
        let result = self.build_result(call_type, required, not_required);
×
224
        let docs = if self.docs.is_empty() {
×
225
            quote! {}
×
226
        } else {
227
            let docs = &self.docs;
×
228
            quote! {
×
229
                .docs(&[#(#docs),*])
×
230
            }
231
        };
232

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

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

NEW
260
        let handler_body = if returns_this {
×
NEW
261
            quote! {
×
NEW
262
                use ::ext_php_rs::convert::IntoZval;
×
263

NEW
264
                #(#arg_declarations)*
×
NEW
265
                #result
×
266

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

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

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

289
        quote! {
×
290
            ::ext_php_rs::builders::FunctionBuilder::new(#name, {
×
291
                ::ext_php_rs::zend_fastcall! {
×
292
                    extern fn handler(
×
293
                        ex: &mut ::ext_php_rs::zend::ExecuteData,
×
294
                        retval: &mut ::ext_php_rs::types::Zval,
×
295
                    ) {
NEW
296
                        #handler_body
×
297
                    }
298
                }
299
                handler
×
300
            })
301
            #(.arg(#required_args))*
×
302
            .not_required()
×
303
            #(.arg(#not_required_args))*
×
304
            #returns
×
305
            #docs
×
306
        }
307
    }
308

309
    fn build_returns(&self, call_type: Option<&CallType>) -> Option<TokenStream> {
3✔
310
        self.output.cloned().map(|mut output| {
12✔
311
            output.drop_lifetimes();
6✔
312

313
            // If returning &Self or &mut Self from a method, use the class type
314
            // for return type information since we return `this` (ZendClassObject)
315
            if returns_self_ref(self.output)
6✔
NEW
316
                && let Some(CallType::Method { class, .. }) = call_type
×
317
            {
NEW
318
                return quote! {
×
NEW
319
                    .returns(
×
NEW
320
                        <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE,
×
NEW
321
                        false,
×
NEW
322
                        <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
323
                    )
324
                };
325
            }
326

327
            // If returning Self (new instance) from a method, replace Self with
328
            // the actual class type since Self won't resolve in generated code
329
            if returns_self(self.output)
6✔
NEW
330
                && let Some(CallType::Method { class, .. }) = call_type
×
331
            {
NEW
332
                return quote! {
×
NEW
333
                    .returns(
×
NEW
334
                        <#class as ::ext_php_rs::convert::IntoZval>::TYPE,
×
NEW
335
                        false,
×
NEW
336
                        <#class as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
337
                    )
338
                };
339
            }
340

341
            quote! {
3✔
342
                .returns(
×
343
                    <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
×
344
                    false,
×
345
                    <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
346
                )
347
            }
348
        })
349
    }
350

351
    fn build_result(
×
352
        &self,
353
        call_type: &CallType,
354
        required: &[TypedArg<'_>],
355
        not_required: &[TypedArg<'_>],
356
    ) -> TokenStream {
357
        let ident = self.ident;
×
358
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
359
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
360

361
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
×
362
            if arg.variadic {
×
363
                let name = arg.name;
×
364
                let variadic_name = format_ident!("__variadic_{}", name);
×
365
                let clean_ty = arg.clean_ty();
×
366
                Some(quote! {
×
367
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
368
                })
369
            } else {
370
                None
×
371
            }
372
        });
373

374
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
375
            arg.accessor(|e| {
×
376
                quote! {
×
377
                    #e.throw().expect("Failed to throw PHP exception.");
×
378
                    return;
×
379
                }
380
            })
381
        });
382

383
        // Check if this method returns &Self or &mut Self
NEW
384
        let returns_this = returns_self_ref(self.output);
×
385

386
        match call_type {
×
387
            CallType::Function => quote! {
×
388
                let parse = ex.parser()
×
389
                    #(.arg(&mut #required_arg_names))*
×
390
                    .not_required()
×
391
                    #(.arg(&mut #not_required_arg_names))*
×
392
                    .parse();
×
393
                if parse.is_err() {
×
394
                    return;
×
395
                }
396
                #(#variadic_bindings)*
×
397

398
                #ident(#({#arg_accessors}),*)
×
399
            },
400
            CallType::Method { class, receiver } => {
×
401
                let this = match receiver {
×
402
                    MethodReceiver::Static => quote! {
×
403
                        let parse = ex.parser();
×
404
                    },
405
                    MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! {
×
406
                        let (parse, this) = ex.parser_method::<#class>();
×
407
                        let this = match this {
×
408
                            Some(this) => this,
×
409
                            None => {
×
410
                                ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into())
×
411
                                    .throw()
×
412
                                    .unwrap();
×
413
                                return;
×
414
                            }
415
                        };
416
                    },
417
                };
418

419
                // When returning &Self or &mut Self, discard the return value
420
                // (we'll use `this` directly in the handler)
NEW
421
                let call = match (receiver, returns_this) {
×
NEW
422
                    (MethodReceiver::Static, _) => {
×
UNCOV
423
                        quote! { #class::#ident(#({#arg_accessors}),*) }
×
424
                    }
NEW
425
                    (MethodReceiver::Class, true) => {
×
NEW
426
                        quote! { let _ = this.#ident(#({#arg_accessors}),*); }
×
427
                    }
NEW
428
                    (MethodReceiver::Class, false) => {
×
NEW
429
                        quote! { this.#ident(#({#arg_accessors}),*) }
×
430
                    }
NEW
431
                    (MethodReceiver::ZendClassObject, true) => {
×
432
                        // Explicit scope helps with mutable borrow lifetime when
433
                        // the method returns `&mut Self`
NEW
434
                        quote! {
×
435
                            {
NEW
436
                                let _ = #class::#ident(this, #({#arg_accessors}),*);
×
437
                            }
438
                        }
439
                    }
NEW
440
                    (MethodReceiver::ZendClassObject, false) => {
×
UNCOV
441
                        quote! { #class::#ident(this, #({#arg_accessors}),*) }
×
442
                    }
443
                };
444

445
                quote! {
×
446
                    #this
×
447
                    let parse_result = parse
×
448
                        #(.arg(&mut #required_arg_names))*
×
449
                        .not_required()
×
450
                        #(.arg(&mut #not_required_arg_names))*
×
451
                        .parse();
×
452
                    if parse_result.is_err() {
×
453
                        return;
×
454
                    }
455
                    #(#variadic_bindings)*
×
456

457
                    #call
×
458
                }
459
            }
460
        }
461
    }
462

463
    /// Generates a struct and impl for the `PhpFunction` trait.
464
    pub fn php_function_impl(&self) -> TokenStream {
×
465
        let internal_ident = self.internal_ident();
×
NEW
466
        let builder = self.function_builder(&CallType::Function);
×
467

468
        quote! {
×
469
            #[doc(hidden)]
×
470
            #[allow(non_camel_case_types)]
×
471
            struct #internal_ident;
×
472

473
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
×
474
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
×
475
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
×
476
                    {
477
                        #builder
×
478
                    }
479
                    entry
×
480
                };
481
            }
482
        }
483
    }
484

485
    /// Returns a constructor metadata object for this function. This doesn't
486
    /// check if the function is a constructor, however.
487
    pub fn constructor_meta(
×
488
        &self,
489
        class: &syn::Path,
490
        visibility: Option<&Visibility>,
491
    ) -> TokenStream {
492
        let ident = self.ident;
×
493
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
494
        let required_args = required
×
495
            .iter()
496
            .map(TypedArg::arg_builder)
×
497
            .collect::<Vec<_>>();
498
        let not_required_args = not_required
×
499
            .iter()
500
            .map(TypedArg::arg_builder)
×
501
            .collect::<Vec<_>>();
502

503
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
504
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
505
        let arg_declarations = self
×
506
            .args
×
507
            .typed
×
508
            .iter()
509
            .map(TypedArg::arg_declaration)
×
510
            .collect::<Vec<_>>();
511
        let variadic_bindings = self.args.typed.iter().filter_map(|arg| {
×
512
            if arg.variadic {
×
513
                let name = arg.name;
×
514
                let variadic_name = format_ident!("__variadic_{}", name);
×
515
                let clean_ty = arg.clean_ty();
×
516
                Some(quote! {
×
517
                    let #variadic_name = #name.variadic_vals::<#clean_ty>();
×
518
                })
519
            } else {
520
                None
×
521
            }
522
        });
523
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
524
            arg.accessor(
×
525
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
×
526
            )
527
        });
528
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
×
529
            quote! {
×
530
                .variadic()
×
531
            }
532
        });
533
        let docs = &self.docs;
×
534
        let flags = visibility.option_tokens();
×
535

536
        quote! {
×
537
            ::ext_php_rs::class::ConstructorMeta {
×
538
                constructor: {
×
539
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
×
540
                        #(#arg_declarations)*
×
541
                        let parse = ex.parser()
×
542
                            #(.arg(&mut #required_arg_names))*
×
543
                            .not_required()
×
544
                            #(.arg(&mut #not_required_arg_names))*
×
545
                            #variadic
×
546
                            .parse();
×
547
                        if parse.is_err() {
×
548
                            return ::ext_php_rs::class::ConstructorResult::ArgError;
×
549
                        }
550
                        #(#variadic_bindings)*
×
551
                        #class::#ident(#({#arg_accessors}),*).into()
×
552
                    }
553
                    inner
×
554
                },
555
                build_fn: {
×
556
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
×
557
                        func
×
558
                            .docs(&[#(#docs),*])
×
559
                            #(.arg(#required_args))*
×
560
                            .not_required()
×
561
                            #(.arg(#not_required_args))*
×
562
                            #variadic
×
563
                    }
564
                    inner
×
565
                },
566
                flags: #flags
×
567
            }
568
        }
569
    }
570
}
571

572
#[derive(Debug)]
573
pub struct ReceiverArg {
574
    pub _mutable: bool,
575
    pub span: Span,
576
}
577

578
#[derive(Debug)]
579
pub struct TypedArg<'a> {
580
    pub name: &'a Ident,
581
    pub ty: Type,
582
    pub nullable: bool,
583
    pub default: Option<Expr>,
584
    pub as_ref: bool,
585
    pub variadic: bool,
586
}
587

588
#[derive(Debug)]
589
pub struct Args<'a> {
590
    pub receiver: Option<ReceiverArg>,
591
    pub typed: Vec<TypedArg<'a>>,
592
}
593

594
impl<'a> Args<'a> {
595
    pub fn parse_from_fnargs(
3✔
596
        args: impl Iterator<Item = &'a FnArg>,
597
        mut defaults: HashMap<Ident, Expr>,
598
    ) -> Result<Self> {
599
        let mut result = Self {
600
            receiver: None,
601
            typed: vec![],
3✔
602
        };
603
        for arg in args {
13✔
604
            match arg {
5✔
605
                FnArg::Receiver(receiver) => {
3✔
606
                    if receiver.reference.is_none() {
6✔
607
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
×
608
                    } else if result.receiver.is_some() {
6✔
609
                        bail!(receiver => "Too many receivers specified.")
×
610
                    }
611
                    result.receiver.replace(ReceiverArg {
9✔
612
                        _mutable: receiver.mutability.is_some(),
9✔
613
                        span: receiver.span(),
3✔
614
                    });
615
                }
616
                FnArg::Typed(PatType { pat, ty, .. }) => {
4✔
617
                    let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else {
4✔
618
                        bail!(pat => "Unsupported argument.");
×
619
                    };
620

621
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
622
                    let default = defaults.remove(ident);
8✔
623
                    let nullable = type_is_nullable(ty.as_ref())?;
6✔
624
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
8✔
625
                    result.typed.push(TypedArg {
6✔
626
                        name: ident,
4✔
627
                        ty,
4✔
628
                        nullable,
4✔
629
                        default,
4✔
630
                        as_ref,
2✔
631
                        variadic,
2✔
632
                    });
633
                }
634
            }
635
        }
636
        Ok(result)
3✔
637
    }
638

639
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
2✔
640
        match ty {
2✔
641
            Type::Reference(ref_) => {
×
642
                let as_ref = ref_.mutability.is_some();
×
643
                match ref_.elem.as_ref() {
×
644
                    Type::Slice(slice) => (
×
645
                        // TODO: Allow specifying the variadic type.
646
                        slice.elem.to_token_stream().to_string() == "& Zval",
×
647
                        as_ref,
×
648
                        ty.clone(),
×
649
                    ),
650
                    _ => (false, as_ref, ty.clone()),
×
651
                }
652
            }
653
            Type::Path(TypePath { path, .. }) => {
2✔
654
                let mut as_ref = false;
4✔
655

656
                // For for types that are `Option<&mut T>` to turn them into
657
                // `Option<&T>`, marking the Arg as as "passed by reference".
658
                let ty = path
4✔
659
                    .segments
2✔
660
                    .last()
661
                    .filter(|seg| seg.ident == "Option")
6✔
662
                    .and_then(|seg| {
2✔
663
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
×
664
                            args.args
×
665
                                .iter()
×
666
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
×
667
                                .and_then(|ga| match ga {
×
668
                                    GenericArgument::Type(ty) => Some(match ty {
×
669
                                        Type::Reference(r) => {
×
670
                                            // Only mark as_ref for mutable references
671
                                            // (Option<&mut T>), not immutable ones (Option<&T>)
672
                                            as_ref = r.mutability.is_some();
×
673
                                            let mut new_ref = r.clone();
×
674
                                            new_ref.mutability = None;
×
675
                                            Type::Reference(new_ref)
×
676
                                        }
677
                                        _ => ty.clone(),
×
678
                                    }),
679
                                    _ => None,
×
680
                                })
681
                        } else {
682
                            None
×
683
                        }
684
                    })
685
                    .unwrap_or_else(|| ty.clone());
6✔
686
                (false, as_ref, ty.clone())
4✔
687
            }
688
            _ => (false, false, ty.clone()),
×
689
        }
690
    }
691

692
    /// Splits the typed arguments into two slices:
693
    ///
694
    /// 1. Required arguments.
695
    /// 2. Non-required arguments.
696
    ///
697
    /// # Parameters
698
    ///
699
    /// * `optional` - The first optional argument. If [`None`], the optional
700
    ///   arguments will be from the first optional argument (nullable or has
701
    ///   default) after the last required argument to the end of the arguments.
702
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
3✔
703
        let mut mid = None;
6✔
704
        for (i, arg) in self.typed.iter().enumerate() {
10✔
705
            // An argument is optional if it's nullable (Option<T>) or has a default value.
706
            let is_optional = arg.nullable || arg.default.is_some();
8✔
707
            if let Some(optional) = optional {
2✔
708
                if optional == arg.name {
×
709
                    mid.replace(i);
×
710
                }
711
            } else if mid.is_none() && is_optional {
6✔
712
                mid.replace(i);
×
713
            } else if !is_optional {
4✔
714
                mid.take();
2✔
715
            }
716
        }
717
        match mid {
3✔
718
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
×
719
            None => (&self.typed[..], &self.typed[0..0]),
6✔
720
        }
721
    }
722
}
723

724
impl TypedArg<'_> {
725
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
726
    /// to be used outside of the original function context.
727
    fn clean_ty(&self) -> Type {
2✔
728
        let mut ty = self.ty.clone();
6✔
729
        ty.drop_lifetimes();
4✔
730

731
        // Variadic arguments are passed as &[&Zval], so we need to extract the
732
        // inner type.
733
        if self.variadic {
2✔
734
            let Type::Reference(reference) = &ty else {
×
735
                return ty;
×
736
            };
737

738
            if let Type::Slice(inner) = &*reference.elem {
×
739
                return *inner.elem.clone();
×
740
            }
741
        }
742

743
        ty
2✔
744
    }
745

746
    /// Returns a token stream containing an argument declaration, where the
747
    /// name of the variable holding the arg is the name of the argument.
748
    fn arg_declaration(&self) -> TokenStream {
×
749
        let name = self.name;
×
750
        let val = self.arg_builder();
×
751
        quote! {
×
752
            let mut #name = #val;
753
        }
754
    }
755

756
    /// Returns a token stream containing the `Arg` definition to be passed to
757
    /// `ext-php-rs`.
758
    fn arg_builder(&self) -> TokenStream {
2✔
759
        let name = self.name.to_string();
6✔
760
        let ty = self.clean_ty();
6✔
761
        let null = if self.nullable {
4✔
762
            Some(quote! { .allow_null() })
×
763
        } else {
764
            None
2✔
765
        };
766
        let default = self.default.as_ref().map(|val| {
8✔
767
            let val = expr_to_php_stub(val);
×
768
            quote! {
×
769
                .default(#val)
770
            }
771
        });
772
        let as_ref = if self.as_ref {
4✔
773
            Some(quote! { .as_ref() })
×
774
        } else {
775
            None
2✔
776
        };
777
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
6✔
778
        quote! {
2✔
779
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
780
                #null
781
                #default
782
                #as_ref
783
                #variadic
784
        }
785
    }
786

787
    /// Get the accessor used to access the value of the argument.
788
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
×
789
        let name = self.name;
×
790
        if let Some(default) = &self.default {
×
791
            if self.nullable {
×
792
                // For nullable types with defaults, null is acceptable
793
                quote! {
×
794
                    #name.val().unwrap_or(#default.into())
×
795
                }
796
            } else {
797
                // For non-nullable types with defaults:
798
                // - If argument was omitted: use default
799
                // - If null was explicitly passed: throw TypeError
800
                // - If a value was passed: try to convert it
801
                let bail_null = bail_fn(quote! {
×
802
                    ::ext_php_rs::exception::PhpException::new(
×
803
                        concat!("Argument `$", stringify!(#name), "` must not be null").into(),
×
804
                        0,
×
805
                        ::ext_php_rs::zend::ce::type_error(),
×
806
                    )
807
                });
808
                let bail_invalid = bail_fn(quote! {
×
809
                    ::ext_php_rs::exception::PhpException::default(
×
810
                        concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
811
                    )
812
                });
813
                quote! {
×
814
                    match #name.zval() {
×
815
                        Some(zval) if zval.is_null() => {
×
816
                            // Null was explicitly passed to a non-nullable parameter
817
                            #bail_null
×
818
                        }
819
                        Some(_) => {
×
820
                            // A value was passed, try to convert it
821
                            match #name.val() {
×
822
                                Some(val) => val,
×
823
                                None => {
×
824
                                    #bail_invalid
×
825
                                }
826
                            }
827
                        }
828
                        None => {
×
829
                            // Argument was omitted, use default
830
                            #default.into()
×
831
                        }
832
                    }
833
                }
834
            }
835
        } else if self.variadic {
×
836
            let variadic_name = format_ident!("__variadic_{}", name);
×
837
            quote! {
×
838
                #variadic_name.as_slice()
×
839
            }
840
        } else if self.nullable {
×
841
            // Originally I thought we could just use the below case for `null` options, as
842
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
843
            // the argument isn't given, as the underlying zval is null.
844
            quote! {
×
845
                #name.val()
×
846
            }
847
        } else {
848
            let bail = bail_fn(quote! {
×
849
                ::ext_php_rs::exception::PhpException::default(
×
850
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
851
                )
852
            });
853
            quote! {
×
854
                match #name.val() {
×
855
                    Some(val) => val,
×
856
                    None => {
×
857
                        #bail;
×
858
                    }
859
                }
860
            }
861
        }
862
    }
863
}
864

865
/// Converts a Rust expression to a PHP stub-compatible default value string.
866
///
867
/// This function handles common Rust patterns and converts them to valid PHP
868
/// syntax for use in generated stub files:
869
///
870
/// - `None` → `"null"`
871
/// - `Some(expr)` → converts the inner expression
872
/// - `42`, `3.14` → numeric literals as-is
873
/// - `true`/`false` → as-is
874
/// - `"string"` → `"string"`
875
/// - `"string".to_string()` or `String::from("string")` → `"string"`
876
fn expr_to_php_stub(expr: &Expr) -> String {
×
877
    match expr {
×
878
        // Handle None -> null
879
        Expr::Path(path) => {
×
880
            let path_str = path.path.to_token_stream().to_string();
×
881
            if path_str == "None" {
×
882
                "null".to_string()
×
883
            } else if path_str == "true" || path_str == "false" {
×
884
                path_str
×
885
            } else {
886
                // For other paths (constants, etc.), use the raw representation
887
                path_str
×
888
            }
889
        }
890

891
        // Handle Some(expr) -> convert inner expression
892
        Expr::Call(call) => {
×
893
            if let Expr::Path(func_path) = &*call.func {
×
894
                let func_name = func_path.path.to_token_stream().to_string();
×
895

896
                // Some(value) -> convert inner value
897
                if func_name == "Some"
×
898
                    && let Some(arg) = call.args.first()
×
899
                {
900
                    return expr_to_php_stub(arg);
×
901
                }
902

903
                // String::from("...") -> "..."
904
                if (func_name == "String :: from" || func_name == "String::from")
×
905
                    && let Some(arg) = call.args.first()
×
906
                {
907
                    return expr_to_php_stub(arg);
×
908
                }
909
            }
910

911
            // Default: use raw representation
912
            expr.to_token_stream().to_string()
×
913
        }
914

915
        // Handle method calls like "string".to_string()
916
        Expr::MethodCall(method_call) => {
×
917
            let method_name = method_call.method.to_string();
×
918

919
            // "...".to_string() or "...".to_owned() or "...".into() -> "..."
920
            if method_name == "to_string" || method_name == "to_owned" || method_name == "into" {
×
921
                return expr_to_php_stub(&method_call.receiver);
×
922
            }
923

924
            // Default: use raw representation
925
            expr.to_token_stream().to_string()
×
926
        }
927

928
        // String literals -> keep as-is (already valid PHP)
929
        Expr::Lit(lit) => match &lit.lit {
×
930
            syn::Lit::Str(s) => format!(
×
931
                "\"{}\"",
932
                s.value().replace('\\', "\\\\").replace('"', "\\\"")
×
933
            ),
934
            syn::Lit::Int(i) => i.to_string(),
×
935
            syn::Lit::Float(f) => f.to_string(),
×
936
            syn::Lit::Bool(b) => if b.value { "true" } else { "false" }.to_string(),
×
937
            syn::Lit::Char(c) => format!("\"{}\"", c.value()),
×
938
            _ => expr.to_token_stream().to_string(),
×
939
        },
940

941
        // Handle arrays: [] or vec![]
942
        Expr::Array(arr) => {
×
943
            if arr.elems.is_empty() {
×
944
                "[]".to_string()
×
945
            } else {
946
                let elems: Vec<String> = arr.elems.iter().map(expr_to_php_stub).collect();
×
947
                format!("[{}]", elems.join(", "))
×
948
            }
949
        }
950

951
        // Handle vec![] macro
952
        Expr::Macro(m) => {
×
953
            let macro_name = m.mac.path.to_token_stream().to_string();
×
954
            if macro_name == "vec" {
×
955
                let tokens = m.mac.tokens.to_string();
×
956
                if tokens.trim().is_empty() {
×
957
                    return "[]".to_string();
×
958
                }
959
            }
960
            // Default: use raw representation
961
            expr.to_token_stream().to_string()
×
962
        }
963

964
        // Handle unary expressions like -42
965
        Expr::Unary(unary) => {
×
966
            let inner = expr_to_php_stub(&unary.expr);
×
967
            format!("{}{}", unary.op.to_token_stream(), inner)
×
968
        }
969

970
        // Default: use raw representation
971
        _ => expr.to_token_stream().to_string(),
×
972
    }
973
}
974

975
/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
976
///
977
/// Note: Having a default value does NOT make a type nullable. A parameter with
978
/// a default value is optional (can be omitted), but passing `null` explicitly
979
/// should still be rejected unless the type is `Option<T>`.
980
// TODO(david): Eventually move to compile-time constants for this (similar to
981
// FromZval::NULLABLE).
982
pub fn type_is_nullable(ty: &Type) -> Result<bool> {
2✔
983
    Ok(match ty {
2✔
984
        Type::Path(path) => path
4✔
985
            .path
2✔
986
            .segments
2✔
987
            .iter()
2✔
988
            .next_back()
2✔
989
            .is_some_and(|seg| seg.ident == "Option"),
6✔
990
        Type::Reference(_) => false, /* Reference cannot be nullable unless */
×
991
        // wrapped in `Option` (in that case it'd be a Path).
992
        _ => bail!(ty => "Unsupported argument type."),
×
993
    })
994
}
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