diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef0b6d559..12b5356de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Setup Mago uses: nhedger/setup-mago@v2 with: - version: 1.49.0 + version: 1.57.0 - name: Check PHP formatting run: mago format --check - name: Lint PHP diff --git a/benches/benches/array_interned_keys.php b/benches/benches/array_interned_keys.php index c7139b6a7..35bbc48d4 100644 --- a/benches/benches/array_interned_keys.php +++ b/benches/benches/array_interned_keys.php @@ -1,5 +1,5 @@ $i * 2, (int) $argv[1]); diff --git a/benches/benches/exception_throw.php b/benches/benches/exception_throw.php index 9260acca6..335c43cb6 100644 --- a/benches/benches/exception_throw.php +++ b/benches/benches/exception_throw.php @@ -1,6 +1,6 @@ Function<'a> { let ident = self.ident; let arg_names: Vec<_> = self.args.typed.iter().map(|arg| arg.name).collect(); - let variadic_bindings = self.args.typed.iter().filter_map(|arg| { - if arg.variadic { - let name = arg.name; - let variadic_name = format_ident!("__variadic_{}", name); - let clean_ty = arg.clean_ty(); - Some(quote! { - let #variadic_name = #name.variadic_vals::<#clean_ty>(); - }) - } else { - None - } - }); + let variadic_bindings = self + .args + .typed + .iter() + .filter_map(TypedArg::variadic_binding); let arg_accessors = self.args.typed.iter().map(|arg| { arg.accessor(|e| { @@ -508,10 +501,9 @@ impl<'a> Function<'a> { ) }; + let invalid = invalid_argument(name); let throw_invalid = quote! { - ::ext_php_rs::exception::PhpException::from_message( - concat!("Invalid value given for argument `", stringify!(#name), "`.").into() - ).throw(); + #invalid.throw(); return; }; @@ -710,18 +702,11 @@ impl<'a> Function<'a> { .iter() .map(TypedArg::arg_declaration) .collect::>(); - let variadic_bindings = self.args.typed.iter().filter_map(|arg| { - if arg.variadic { - let name = arg.name; - let variadic_name = format_ident!("__variadic_{}", name); - let clean_ty = arg.clean_ty(); - Some(quote! { - let #variadic_name = #name.variadic_vals::<#clean_ty>(); - }) - } else { - None - } - }); + let variadic_bindings = self + .args + .typed + .iter() + .filter_map(TypedArg::variadic_binding); let arg_accessors = self.args.typed.iter().map(|arg| { arg.accessor( |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); }, @@ -912,6 +897,20 @@ fn is_slice_ref(ty: &Type) -> bool { matches!(ty, Type::Reference(reference) if matches!(*reference.elem, Type::Slice(_))) } +pub(crate) fn invalid_argument(name: &Ident) -> TokenStream { + let message = format!( + "Invalid value given for argument `{}`.", + ident_to_php_name(name) + ); + quote! { + ::ext_php_rs::exception::PhpException::new( + #message.into(), + 0, + ::ext_php_rs::zend::ce::type_error(), + ) + } +} + impl TypedArg<'_> { /// Returns a 'clean type' with the lifetimes removed. This allows the type /// to be used outside of the original function context. @@ -965,6 +964,18 @@ impl TypedArg<'_> { } } + fn variadic_binding(&self) -> Option { + if !self.variadic { + return None; + } + let name = self.name; + let variadic_name = format_ident!("__variadic_{}", name); + let ty = self.clean_ty(); + Some(quote! { + let #variadic_name = #name.try_variadic_vals::<#ty>(); + }) + } + /// The value of the argument when the caller omitted it: the default when /// one is declared, otherwise what the type says (`Some(None)` for /// `Option`, `None` for a required type). @@ -979,19 +990,20 @@ impl TypedArg<'_> { /// Get the accessor used to access the value of the argument. fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream { let name = self.name; + let bail_invalid = bail_fn(invalid_argument(name)); if self.variadic { let variadic_name = format_ident!("__variadic_{}", name); return quote! { - #variadic_name.as_slice() + match #variadic_name.as_deref() { + Some(values) => values, + None => { + #bail_invalid + } + } }; } let ty = self.clean_ty(); - let bail_invalid = bail_fn(quote! { - ::ext_php_rs::exception::PhpException::from_message( - concat!("Invalid value given for argument `", stringify!(#name), "`.").into() - ) - }); let reject_null = self.default.as_ref().map(|_| { let bail_null = bail_fn(quote! { ::ext_php_rs::exception::PhpException::new( diff --git a/crates/macros/src/impl_interface.rs b/crates/macros/src/impl_interface.rs index 1aa1e5fb9..b8a792949 100644 --- a/crates/macros/src/impl_interface.rs +++ b/crates/macros/src/impl_interface.rs @@ -13,6 +13,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{FnArg, ImplItem, ItemImpl, Pat, ReturnType}; +use crate::function::invalid_argument; use crate::interface::method_name_const; use crate::parsing::{RenameRule, ident_to_php_name, reject_php_attrs}; use crate::prelude::*; @@ -200,7 +201,7 @@ fn generate_method_builder( let arg_value_accessors: Vec<_> = args .iter() .map(|(name, ty)| { - let php_name = ident_to_php_name(name); + let invalid = invalid_argument(name); quote! { let #name: #ty = match match #name.zval() { Some(zval) => <#ty as ::ext_php_rs::convert::FromZvalMut>::from_zval_mut(zval.dereference_mut()), @@ -208,9 +209,7 @@ fn generate_method_builder( } { Some(value) => value, None => { - let msg = format!("Invalid value for argument `{}`", #php_name); - ::ext_php_rs::exception::PhpException::from_message(msg.into()) - .throw(); + #invalid.throw(); return; } }; diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index f096d046a..c85c30edc 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1425,7 +1425,8 @@ fn php_interface_internal(args: TokenStream2, input: TokenStream2) -> TokenStrea /// should not be used on class methods - see the `#[php_impl]` macro for that. /// /// See the [list of types](../types/index.md) that are valid as parameter and -/// return types. +/// return types. If the caller passes a value that the parameter type does not +/// accept, the call throws a `TypeError`. /// /// A function accepts the `name`, `change_case`, `defaults` and `optional` /// options. PHP functions have no visibility, so `#[php(vis = "...")]` is a @@ -1562,9 +1563,10 @@ fn php_interface_internal(args: TokenStream2, input: TokenStream2) -> TokenStrea /// /// A slice parameter `&[T]` in last position is variadic, the equivalent of the /// PHP `...$args` syntax. `T` is any argument type: `&[&Zval]` receives the raw -/// values, `&[i64]` receives converted integers. A variadic parameter that is -/// not the last parameter is a compile error. The slice must be spelled in the -/// signature; a type alias that hides the slice is not detected. +/// values, `&[i64]` receives converted integers. If one of the values cannot be +/// converted into `T`, the call throws a `TypeError`. A variadic parameter that +/// is not the last parameter is a compile error. The slice must be spelled in +/// the signature; a type alias that hides the slice is not detected. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -2457,7 +2459,9 @@ fn php_extern_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 /// The ordering of the variants in the enum is important, as the `FromZval` /// implementation will attempt to parse the zval data in order. For example, if /// you put a `String` variant before an integer variant, the integer would be -/// converted to a string and passed as the string variant. +/// converted to a string and passed as the string variant. Because an `f64` or +/// `f32` variant also accepts an integer, the integer variants must come before +/// it. /// /// ### Examples /// diff --git a/crates/macros/tests/expand/function.expanded.rs b/crates/macros/tests/expand/function.expanded.rs index 3fc3a5e6e..3a879884a 100644 --- a/crates/macros/tests/expand/function.expanded.rs +++ b/crates/macros/tests/expand/function.expanded.rs @@ -57,8 +57,10 @@ impl ::ext_php_rs::internal::function::PhpFunction for _internal_greet { match __value { Some(value) => value, None => { - ::ext_php_rs::exception::PhpException::from_message( + ::ext_php_rs::exception::PhpException::new( "Invalid value given for argument `name`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), ) .throw(); return; @@ -85,8 +87,10 @@ impl ::ext_php_rs::internal::function::PhpFunction for _internal_greet { match __value { Some(value) => value, None => { - ::ext_php_rs::exception::PhpException::from_message( + ::ext_php_rs::exception::PhpException::new( "Invalid value given for argument `age`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), ) .throw(); return; @@ -120,8 +124,10 @@ impl ::ext_php_rs::internal::function::PhpFunction for _internal_greet { match __value { Some(value) => value, None => { - ::ext_php_rs::exception::PhpException::from_message( + ::ext_php_rs::exception::PhpException::new( "Invalid value given for argument `times`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), ) .throw(); return; @@ -200,7 +206,7 @@ impl ::ext_php_rs::internal::function::PhpFunction for _internal_sum { if parse.is_err() { return; } - let __variadic_rest = rest.variadic_vals::(); + let __variadic_rest = rest.try_variadic_vals::(); sum( { match match first.zval() { @@ -215,15 +221,30 @@ impl ::ext_php_rs::internal::function::PhpFunction for _internal_sum { } { Some(value) => value, None => { - ::ext_php_rs::exception::PhpException::from_message( + ::ext_php_rs::exception::PhpException::new( "Invalid value given for argument `first`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), + ) + .throw(); + return; + } + } + }, + { + match __variadic_rest.as_deref() { + Some(values) => values, + None => { + ::ext_php_rs::exception::PhpException::new( + "Invalid value given for argument `rest`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), ) .throw(); return; } } }, - { __variadic_rest.as_slice() }, ) }; if let Err(e) = result.set_zval(retval, false) { @@ -300,8 +321,10 @@ impl ::ext_php_rs::internal::function::PhpFunction for _internal_bump { match __value { Some(value) => value, None => { - ::ext_php_rs::exception::PhpException::from_message( + ::ext_php_rs::exception::PhpException::new( "Invalid value given for argument `target`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), ) .throw(); return; diff --git a/crates/macros/tests/expand/interface.expanded.rs b/crates/macros/tests/expand/interface.expanded.rs index 2b00a4d11..b474760a5 100644 --- a/crates/macros/tests/expand/interface.expanded.rs +++ b/crates/macros/tests/expand/interface.expanded.rs @@ -636,13 +636,10 @@ for ::ext_php_rs::internal::class::PhpClassImplCollector { } { Some(value) => value, None => { - let msg = ::alloc::__export::must_use({ - ::alloc::fmt::format( - format_args!("Invalid value for argument `{0}`", "arg"), - ) - }); - ::ext_php_rs::exception::PhpException::from_message( - msg.into(), + ::ext_php_rs::exception::PhpException::new( + "Invalid value given for argument `arg`.".into(), + 0, + ::ext_php_rs::zend::ce::type_error(), ) .throw(); return; diff --git a/flake.nix b/flake.nix index e2fa1d892..83bebc03d 100644 --- a/flake.nix +++ b/flake.nix @@ -56,10 +56,10 @@ # local dev and CI (nhedger/setup-mago) run the exact same version. mago = pkgs.stdenvNoCC.mkDerivation rec { pname = "mago"; - version = "1.49.0"; + version = "1.57.0"; src = pkgs.fetchurl { url = "https://github.com/carthage-software/mago/releases/download/${version}/mago-${version}-x86_64-unknown-linux-musl.tar.gz"; - hash = "sha256-jigDGfg3o75qeqy0o+LS5J56yJkPioH+6sK0CQOVegw="; + hash = "sha256-65riwMATzaEYWCIfn5OfXmDcm+XGb2/5BYFvXbbw0zY="; }; installPhase = '' runHook preInstall diff --git a/guide/src/macros/function.md b/guide/src/macros/function.md index a04de5e32..a901617e8 100644 --- a/guide/src/macros/function.md +++ b/guide/src/macros/function.md @@ -4,7 +4,8 @@ Used to annotate functions which should be exported to PHP. Note that this should not be used on class methods - see the `#[php_impl]` macro for that. See the [list of types](../types/index.md) that are valid as parameter and -return types. +return types. If the caller passes a value that the parameter type does not +accept, the call throws a `TypeError`. A function accepts the `name`, `change_case`, `defaults` and `optional` options. PHP functions have no visibility, so `#[php(vis = "...")]` is a @@ -140,7 +141,8 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { A slice parameter `&[T]` in last position is variadic, the equivalent of the PHP `...$args` syntax. `T` is any argument type: `&[&Zval]` receives the raw values, -`&[i64]` receives converted integers. A variadic parameter that is not the last +`&[i64]` receives converted integers. If one of the values cannot be converted +into `T`, the call throws a `TypeError`. A variadic parameter that is not the last parameter is a compile error. The slice must be spelled in the signature; a type alias that hides the slice is not detected. diff --git a/guide/src/macros/zval_convert.md b/guide/src/macros/zval_convert.md index 7586138d9..f36d0ca48 100644 --- a/guide/src/macros/zval_convert.md +++ b/guide/src/macros/zval_convert.md @@ -103,7 +103,8 @@ variants could not be extracted from the zval. The ordering of the variants in the enum is important, as the `FromZval` implementation will attempt to parse the zval data in order. For example, if you put a `String` variant before an integer variant, the integer would be converted -to a string and passed as the string variant. +to a string and passed as the string variant. Because an `f64` or `f32` variant +also accepts an integer, the integer variants must come before it. ### Examples diff --git a/guide/src/types/numbers.md b/guide/src/types/numbers.md index 68755a1db..19fcecabd 100644 --- a/guide/src/types/numbers.md +++ b/guide/src/types/numbers.md @@ -13,7 +13,8 @@ long is always signed, and the size will be 32-bits on 32-bit platforms and 64-bits on 64-bit platforms. Floating point numbers are always stored in a `double` type (`f64`), regardless -of platform. Note that converting a zval into a `f32` will lose accuracy. +of platform. Note that converting a zval into a `f32` will lose accuracy. An `f32` +or `f64` parameter also accepts a PHP `int`, the same as a PHP `float` parameter. This means that converting `i64`, `u32`, `u64`, `isize` and `usize` _can_ fail depending on the value and the platform, which is why all zval conversions are diff --git a/guide/src/types/option.md b/guide/src/types/option.md index a50201afd..c2b0267f4 100644 --- a/guide/src/types/option.md +++ b/guide/src/types/option.md @@ -9,10 +9,14 @@ is also able to be converted to/from a zval. | Yes | No | Yes | No | Depends on `T`, `null` for `None`. | An `Option` parameter is nullable. If the caller passes `null`, the function -receives `None`. A trailing `Option` parameter is also optional. If the caller -omits it, the function receives `None`, and PHP reflection reports a default of -`null`. The macro reads this from `FromZvalMut::NULLABLE`, so a type alias or a -qualified path behaves the same way as `Option`. +receives `None`. If the caller passes a value that `T` does not accept, the call +throws a `TypeError`, the same as for a `T` parameter. If `T` accepts `null`, as +`&Zval` does, a `null` argument gives `Some`. + +A trailing `Option` parameter is also optional. If the caller omits it, the +function receives `None`, and PHP reflection reports a default of `null`. The +macro reads this from `FromZvalMut::NULLABLE`, so a type alias or a qualified path +behaves the same way as `Option`. Returning `Option` is a nullable return type. Returning `None` will return null to PHP. diff --git a/src/args.rs b/src/args.rs index e7046261a..aff657dcd 100644 --- a/src/args.rs +++ b/src/args.rs @@ -128,7 +128,9 @@ impl<'a> Arg<'a> { .and_then(|zv| T::from_zval_mut(zv.dereference_mut())) } - /// Retrice all the variadic values for this Rust argument. + /// Gets all the variadic values for this Rust argument. The result does not + /// contain a value that cannot be converted into `T`. + /// [`try_variadic_vals`](Self::try_variadic_vals) rejects it. pub fn variadic_vals(&'a mut self) -> Vec where T: FromZvalMut<'a>, @@ -140,6 +142,19 @@ impl<'a> Arg<'a> { .collect() } + /// Gets all the variadic values for this Rust argument. Returns `None` if + /// one of the values cannot be converted into `T`. + pub fn try_variadic_vals(&'a mut self) -> Option> + where + T: FromZvalMut<'a>, + { + self.variadic_zvals + .iter_mut() + .filter_map(|zv| zv.as_mut()) + .map(|zv| T::from_zval_mut(zv.dereference_mut())) + .collect() + } + /// Attempts to return a reference to the arguments internal Zval. /// /// # Returns @@ -476,6 +491,18 @@ mod tests { assert_eq!(result, vec![42, 43]); } + #[test] + #[cfg(feature = "embed")] + fn try_variadic_vals_fails_when_a_value_does_not_convert() { + let mut arg = Arg::new("test", DataType::Long).is_variadic(); + let mut long = Zval::from(42); + let mut double = Zval::from(1.5); + arg.variadic_zvals.push(Some(&mut long)); + arg.variadic_zvals.push(Some(&mut double)); + + assert_eq!(arg.try_variadic_vals::(), None); + } + #[test] fn test_zval_no_value() { let mut arg = Arg::new("test", DataType::Long); diff --git a/src/closure.rs b/src/closure.rs index 58b8ffe20..0842cef7a 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -9,7 +9,7 @@ use crate::{ exception::PhpException, flags::{DataType, MethodFlags}, types::Zval, - zend::ExecuteData, + zend::{ExecuteData, ce}, zend_fastcall, }; @@ -310,7 +310,7 @@ macro_rules! php_closure_impl { match $gen.consume() { Ok(val) => val, _ => { - PhpException::from_message(concat!("Invalid parameter type for `", stringify!($gen), "`.").into()).throw(); + PhpException::new(concat!("Invalid parameter type for `", stringify!($gen), "`.").into(), 0, ce::type_error()).throw(); return; } } diff --git a/src/convert.rs b/src/convert.rs index 8c5945af1..19f922b4c 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -51,7 +51,9 @@ where const NULLABLE: bool = true; fn from_zval(zval: &'a Zval) -> Option { - Some(T::from_zval(zval)) + T::from_zval(zval) + .map(Some) + .or_else(|| zval.dereference().is_null().then_some(None)) } fn from_missing() -> Option { @@ -593,4 +595,23 @@ mod tests { ); assert_eq!(::from_missing(), None); } + + #[test] + #[cfg(feature = "embed")] + fn option_rejects_a_value_the_inner_type_rejects() { + assert_eq!( as FromZval>::from_zval(&Zval::from(1.5)), None); + } + + #[test] + #[cfg(feature = "embed")] + fn option_reads_null_as_none_unless_the_inner_type_accepts_null() { + assert_eq!( + as FromZval>::from_zval(&Zval::new()), + Some(None) + ); + assert_eq!( + > as FromZval>::from_zval(&Zval::new()), + Some(Some(None)) + ); + } } diff --git a/src/types/array/mod.rs b/src/types/array/mod.rs index a39b19978..359998049 100644 --- a/src/types/array/mod.rs +++ b/src/types/array/mod.rs @@ -893,7 +893,11 @@ impl<'a> FromZvalMut<'a> for Option<&'a mut ZendHashTable> { const BY_REF: bool = true; fn from_zval_mut(zval: &'a mut Zval) -> Option { - Some(zval.array_mut()) + if zval.dereference().is_null() { + Some(None) + } else { + zval.array_mut().map(Some) + } } fn from_missing() -> Option { diff --git a/src/types/mod.rs b/src/types/mod.rs index f60f9c951..2cbd20002 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -27,20 +27,55 @@ pub use separated::Separated; pub use string::ZendStr; pub use zval::Zval; -use crate::{convert::FromZval, flags::DataType}; +use crate::{ + convert::FromZval, + error::{Error, Result}, + flags::DataType, +}; into_zval!(f32, set_double, Double); into_zval!(f64, set_double, Double); into_zval!(bool, set_bool, Bool); -try_from_zval!(f64, double, Double); try_from_zval!(bool, bool, Bool); +impl FromZval<'_> for f64 { + const TYPE: DataType = DataType::Double; + + #[allow( + clippy::cast_precision_loss, + reason = "PHP casts an int given for a float parameter the same way" + )] + fn from_zval(zval: &Zval) -> Option { + zval.double().or_else(|| zval.long().map(|v| v as f64)) + } +} + +impl TryFrom for f64 { + type Error = Error; + + fn try_from(value: Zval) -> Result { + Self::from_zval(&value).ok_or(Error::ZvalConversion(value.get_type())) + } +} + impl FromZval<'_> for f32 { const TYPE: DataType = DataType::Double; fn from_zval(zval: &Zval) -> Option { #[allow(clippy::cast_possible_truncation)] - zval.double().map(|v| v as f32) + f64::from_zval(zval).map(|v| v as f32) + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +mod tests { + use super::*; + + #[test] + fn floats_accept_a_php_int() { + assert_eq!(f64::from_zval(&Zval::from(2)), Some(2.0)); + assert_eq!(f32::from_zval(&Zval::from(2)), Some(2.0)); } } diff --git a/tests/src/integration/_utils.php b/tests/src/integration/_utils.php index 7027a97e2..c9c1683cc 100644 --- a/tests/src/integration/_utils.php +++ b/tests/src/integration/_utils.php @@ -9,3 +9,13 @@ function assert_exception_thrown(callable $callback): void } throw new Exception('Exception was not thrown', 255); } + +function assert_type_error(callable $callback): void +{ + try { + call_user_func($callback); + } catch (\TypeError) { + return; + } + throw new Exception('TypeError was not thrown', 255); +} diff --git a/tests/src/integration/array/array.php b/tests/src/integration/array/array.php index 5cfc5e739..949ace712 100644 --- a/tests/src/integration/array/array.php +++ b/tests/src/integration/array/array.php @@ -1,5 +1,7 @@ should accept null'); +$not_an_array = 'text'; +assert_type_error(fn() => test_optional_array_mut_ref($not_an_array)); // Test ZendEmptyArray - returns an empty immutable shared array $empty = test_empty_array(); diff --git a/tests/src/integration/binary_slice/binary_slice.php b/tests/src/integration/binary_slice/binary_slice.php index f4746ab98..4a40a9cdd 100644 --- a/tests/src/integration/binary_slice/binary_slice.php +++ b/tests/src/integration/binary_slice/binary_slice.php @@ -10,5 +10,5 @@ try { test_binary_slice_len('1234567'); assert(false, 'a 7-byte string must be rejected, not truncated'); -} catch (Exception $e) { +} catch (TypeError $e) { } diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index 00178dd92..bd0db8552 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -261,6 +261,7 @@ class SelfLinked extends TestClassExtendsWithProp assert($arr["\0TestPropertyVisibility\0privateStr"] === 'new_private'); $optional = new TestOptionalProp('set'); +assert_type_error(fn() => new TestOptionalProp(42)); assert((array) $optional === ['opt' => 'set']); assert(isset($optional->opt)); $optional->clear(); diff --git a/tests/src/integration/closure/closure.php b/tests/src/integration/closure/closure.php index 940b5daee..09e998c97 100644 --- a/tests/src/integration/closure/closure.php +++ b/tests/src/integration/closure/closure.php @@ -6,6 +6,7 @@ // Closure assert($v('works') === 'works'); +assert_type_error(fn() => $v(42)); // Closure once $closure = test_closure_once('test'); diff --git a/tests/src/integration/constant/constant.php b/tests/src/integration/constant/constant.php index aab25fa1e..32fd41778 100644 --- a/tests/src/integration/constant/constant.php +++ b/tests/src/integration/constant/constant.php @@ -1,6 +1,6 @@ getReflectionConstant('PUBLIC_CONST')->isPublic()); diff --git a/tests/src/integration/enum_/enum.php b/tests/src/integration/enum_/enum.php index 3d2c90dc8..56adf0772 100644 --- a/tests/src/integration/enum_/enum.php +++ b/tests/src/integration/enum_/enum.php @@ -1,6 +1,6 @@ greetOptional() === 'Hi World'); assert($greeter->greetOptional(null) === 'Hi World'); assert($greeter->greetOptional('Hello') === 'Hello World'); +assert_type_error(fn() => $greeter->greetOptional(42)); $param = ( new ReflectionMethod($greeter, 'greetOptional') )->getParameters()[0]; assert($param->allowsNull()); assert($param->isOptional()); diff --git a/tests/src/integration/nullable/mod.rs b/tests/src/integration/nullable/mod.rs index b5529e3e9..416b0e3e1 100644 --- a/tests/src/integration/nullable/mod.rs +++ b/tests/src/integration/nullable/mod.rs @@ -5,8 +5,27 @@ pub fn test_nullable(a: Option) -> Option { a } +#[php_function] +pub fn test_nullable_int(a: Option) -> Option { + a +} + +#[php_function] +pub fn test_nullable_float(a: Option) -> Option { + a +} + +#[php_function] +pub fn test_nullable_ints(values: Vec>) -> Vec> { + values +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { - builder.function(wrap_function!(test_nullable)) + builder + .function(wrap_function!(test_nullable)) + .function(wrap_function!(test_nullable_int)) + .function(wrap_function!(test_nullable_float)) + .function(wrap_function!(test_nullable_ints)) } #[cfg(test)] diff --git a/tests/src/integration/nullable/nullable.php b/tests/src/integration/nullable/nullable.php index f12577098..d3d405e29 100644 --- a/tests/src/integration/nullable/nullable.php +++ b/tests/src/integration/nullable/nullable.php @@ -1,5 +1,7 @@ isOptional()); assert($param->isDefaultValueAvailable()); assert($param->getDefaultValue() === null); + +assert(test_nullable_int(5) === 5); +assert(test_nullable_int(null) === null); +assert(test_nullable_int() === null); +assert_type_error(fn() => test_nullable_int('5')); +assert_type_error(fn() => test_nullable_int([])); +assert(test_nullable_float(1) === 1.0); + +$null = null; +assert(test_nullable_ints([1, null, &$null]) === [1, null, null]); +assert_type_error(fn() => test_nullable_ints([1, 'two'])); diff --git a/tests/src/integration/number/number.php b/tests/src/integration/number/number.php index efa83e6ed..2eb431464 100644 --- a/tests/src/integration/number/number.php +++ b/tests/src/integration/number/number.php @@ -16,3 +16,4 @@ assert(round(test_number_float(-1.2), 2) === round(-1.2, 2)); assert(round(test_number_float(0.0), 2) === round(0.0, 2)); assert(round(test_number_float(1.2), 2) === round(1.2, 2)); +assert(test_number_float(1) === 1.0); diff --git a/tests/src/integration/variadic_args/mod.rs b/tests/src/integration/variadic_args/mod.rs index bdbc0305c..f2a7a25b8 100644 --- a/tests/src/integration/variadic_args/mod.rs +++ b/tests/src/integration/variadic_args/mod.rs @@ -62,6 +62,11 @@ pub fn test_variadic_sum_all(nums: &[&Zval]) -> i64 { nums.iter().filter_map(|v| v.long()).sum() } +#[php_function] +pub fn test_variadic_ints(first: i64, rest: &[i64]) -> i64 { + first + rest.iter().sum::() +} + #[php_function] pub fn test_variadic_optional(required: String, optional: Option, extras: &[&Zval]) -> String { let opt_str = optional.map_or_else(|| "none".to_string(), |v| v.to_string()); @@ -95,6 +100,7 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { .function(wrap_function!(test_variadic_types)) .function(wrap_function!(test_variadic_strings)) .function(wrap_function!(test_variadic_sum_all)) + .function(wrap_function!(test_variadic_ints)) .function(wrap_function!(test_variadic_optional)) .function(wrap_function!(test_variadic_empty_check)) .function(wrap_function!(test_variadic_first_last)) diff --git a/tests/src/integration/variadic_args/variadic_args.php b/tests/src/integration/variadic_args/variadic_args.php index f2674a4eb..4a0d9f8ea 100644 --- a/tests/src/integration/variadic_args/variadic_args.php +++ b/tests/src/integration/variadic_args/variadic_args.php @@ -68,6 +68,15 @@ assert(test_variadic_sum_all(-5, 5) === 0, 'Negative and positive should cancel'); assert(test_variadic_sum_all(100, 'not a number', 200) === 300, 'Should skip non-numeric values'); +assert(test_variadic_ints(1, 2, 3) === 6); +assert_type_error(fn() => test_variadic_ints(1, 'x', 3)); +try { + test_variadic_ints('x', 2, 'y'); + assert(false, 'a value of the wrong type must throw'); +} catch (TypeError $e) { + assert(str_contains($e->getMessage(), '`first`'), $e->getMessage()); +} + // Test variadic with optional parameter assert(test_variadic_optional('req', null) === 'req-none-0', 'Optional null, no extras'); assert(test_variadic_optional('req', 42) === 'req-42-0', 'Optional provided, no extras');