Skip to content

Commit d81eac0

Browse files
committed
fix(macro): nullable parameters #538
1 parent 6b192e8 commit d81eac0

File tree

2 files changed

+33
-17
lines changed

2 files changed

+33
-17
lines changed

crates/macros/src/function.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ impl<'a> Args<'a> {
493493

494494
// If the variable is `&[&Zval]` treat it as the variadic argument.
495495
let default = defaults.remove(ident);
496-
let nullable = type_is_nullable(ty.as_ref(), default.is_some())?;
496+
let nullable = type_is_nullable(ty.as_ref())?;
497497
let (variadic, as_ref, ty) = Self::parse_typed(ty);
498498
result.typed.push(TypedArg {
499499
name: ident,
@@ -568,18 +568,20 @@ impl<'a> Args<'a> {
568568
/// # Parameters
569569
///
570570
/// * `optional` - The first optional argument. If [`None`], the optional
571-
/// arguments will be from the first nullable argument after the last
572-
/// non-nullable argument to the end of the arguments.
571+
/// arguments will be from the first optional argument (nullable or has
572+
/// default) after the last required argument to the end of the arguments.
573573
pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
574574
let mut mid = None;
575575
for (i, arg) in self.typed.iter().enumerate() {
576+
// An argument is optional if it's nullable (Option<T>) or has a default value.
577+
let is_optional = arg.nullable || arg.default.is_some();
576578
if let Some(optional) = optional {
577579
if optional == arg.name {
578580
mid.replace(i);
579581
}
580-
} else if mid.is_none() && arg.nullable {
582+
} else if mid.is_none() && is_optional {
581583
mid.replace(i);
582-
} else if !arg.nullable {
584+
} else if !is_optional {
583585
mid.take();
584586
}
585587
}
@@ -690,21 +692,22 @@ impl TypedArg<'_> {
690692
}
691693
}
692694

693-
/// Returns true of the given type is nullable in PHP.
695+
/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
696+
///
697+
/// Note: Having a default value does NOT make a type nullable. A parameter with
698+
/// a default value is optional (can be omitted), but passing `null` explicitly
699+
/// should still be rejected unless the type is `Option<T>`.
694700
// TODO(david): Eventually move to compile-time constants for this (similar to
695701
// FromZval::NULLABLE).
696-
pub fn type_is_nullable(ty: &Type, has_default: bool) -> Result<bool> {
702+
pub fn type_is_nullable(ty: &Type) -> Result<bool> {
697703
Ok(match ty {
698-
syn::Type::Path(path) => {
699-
has_default
700-
|| path
701-
.path
702-
.segments
703-
.iter()
704-
.next_back()
705-
.is_some_and(|seg| seg.ident == "Option")
706-
}
707-
syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */
704+
Type::Path(path) => path
705+
.path
706+
.segments
707+
.iter()
708+
.next_back()
709+
.is_some_and(|seg| seg.ident == "Option"),
710+
Type::Reference(_) => false, /* Reference cannot be nullable unless */
708711
// wrapped in `Option` (in that case it'd be a Path).
709712
_ => bail!(ty => "Unsupported argument type."),
710713
})

tests/src/integration/defaults/defaults.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,16 @@
77
assert(test_defaults_multiple_option_arguments() === 'Default');
88
assert(test_defaults_multiple_option_arguments(a: 'a') === 'a');
99
assert(test_defaults_multiple_option_arguments(b: 'b') === 'b');
10+
11+
// Test that passing null to a non-nullable parameter with a default value throws TypeError
12+
// (fixes: https://github.com/extphprs/ext-php-rs/issues/538)
13+
$threw = false;
14+
try {
15+
test_defaults_integer(null);
16+
} catch (TypeError $e) {
17+
$threw = true;
18+
}
19+
assert($threw, 'Expected TypeError when passing null to non-nullable parameter with default');
20+
21+
// But passing null to a nullable parameter should still work
22+
assert(test_defaults_nullable_string(null) === null);

0 commit comments

Comments
 (0)