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

extphprs / ext-php-rs / 20456683792

23 Dec 2025 09:19AM UTC coverage: 35.984% (+0.6%) from 35.384%
20456683792

push

github

web-flow
fix(macro): identifier-related bugs #536 (#616)

61 of 92 new or added lines in 9 files covered. (66.3%)

1 existing line in 1 file now uncovered.

1765 of 4905 relevant lines covered (35.98%)

13.94 hits per line

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

20.46
/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 {
3✔
19
    let Some(ty) = output else {
6✔
20
        return false;
×
21
    };
22
    if let Type::Reference(ref_) = ty
3✔
23
        && let Type::Path(path) = &*ref_.elem
×
24
        && path.path.segments.len() == 1
×
25
        && let Some(segment) = path.path.segments.last()
×
26
    {
27
        return segment.ident == "Self";
×
28
    }
29
    false
3✔
30
}
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 {
3✔
35
    let Some(ty) = output else {
6✔
36
        return false;
×
37
    };
38
    if let Type::Path(path) = ty
6✔
39
        && path.path.segments.len() == 1
3✔
40
        && let Some(segment) = path.path.segments.last()
6✔
41
    {
42
        return segment.ident == "Self";
3✔
43
    }
44
    false
×
45
}
46

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

53
    Ok(quote! {{
×
54
        (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)()
×
55
    }})
56
}
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> {
×
70
    let php_attr = PhpFunctionAttribute::from_attributes(&input.attrs)?;
×
71
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
×
72

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

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

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

87
    Ok(quote! {
×
88
        #input
×
89
        #function_impl
×
90
    })
91
}
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(
3✔
139
        sig: &'a syn::Signature,
140
        name: String,
141
        args: Args<'a>,
142
        optional: Option<Ident>,
143
        docs: Vec<String>,
144
    ) -> Self {
145
        Self {
146
            ident: &sig.ident,
3✔
147
            name,
148
            args,
149
            output: match &sig.output {
3✔
150
                syn::ReturnType::Default => None,
151
                syn::ReturnType::Type(_, ty) => Some(&**ty),
152
            },
153
            optional,
154
            docs,
155
        }
156
    }
157

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

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

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

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

187
        quote! {
3✔
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
    }
196

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

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

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

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

231
        // Static methods cannot return &Self or &mut Self
232
        if returns_self_ref(self.output)
×
233
            && let CallType::Method {
×
234
                receiver: MethodReceiver::Static,
×
235
                ..
×
236
            } = call_type
×
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
        }
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)
×
250
            && matches!(
×
251
                call_type,
×
252
                CallType::Method {
×
253
                    receiver: MethodReceiver::Class | MethodReceiver::ZendClassObject,
×
254
                    ..
×
255
                }
256
            );
257

258
        let handler_body = if returns_this {
×
259
            quote! {
×
260
                use ::ext_php_rs::convert::IntoZval;
×
261

262
                #(#arg_declarations)*
×
263
                #result
×
264

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

275
                #(#arg_declarations)*
×
276
                let result = {
×
277
                    #result
×
278
                };
279

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

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

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

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

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

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

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

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

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

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

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

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

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

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

455
                    #call
×
456
                }
457
            }
458
        }
459
    }
460

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

741
        ty
2✔
742
    }
743

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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