diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 9fa5f16bdc..83303de29f 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -8,7 +8,7 @@ use syn::{Ident, ItemImpl, Lit}; use crate::constant::PhpConstAttribute; use crate::function::{Args, CallType, Function, MethodReceiver}; use crate::helpers::get_docs; -use crate::parsing::{PhpRename, RenameRule, Visibility}; +use crate::parsing::{MethodRename, PhpRename, Rename, RenameRule, Visibility}; use crate::prelude::*; /// Method types. @@ -184,7 +184,7 @@ impl<'a> ParsedImpl<'a> { match items { syn::ImplItem::Const(c) => { let attr = PhpConstAttribute::from_attributes(&c.attrs)?; - let name = self.rename_constants.rename(c.ident.to_string()); + let name = c.ident.rename(&self.rename_constants); let name = attr.rename.rename(name); let docs = get_docs(&attr.attrs)?; c.attrs.retain(|attr| !attr.path().is_ident("php")); @@ -197,7 +197,7 @@ impl<'a> ParsedImpl<'a> { } syn::ImplItem::Fn(method) => { let attr = PhpFunctionImplAttribute::from_attributes(&method.attrs)?; - let name = self.rename_methods.rename(method.sig.ident.to_string()); + let name = method.sig.ident.rename_method(&self.rename_methods); let name = attr.rename.rename(name); let docs = get_docs(&attr.attrs)?; method.attrs.retain(|attr| !attr.path().is_ident("php")); @@ -318,43 +318,3 @@ impl quote::ToTokens for FnBuilder { .to_tokens(tokens); } } - -#[cfg(test)] -mod tests { - use super::RenameRule; - - #[test] - fn test_rename_magic() { - for &(magic, expected) in &[ - ("__construct", "__construct"), - ("__destruct", "__destruct"), - ("__call", "__call"), - ("__call_static", "__callStatic"), - ("__get", "__get"), - ("__set", "__set"), - ("__isset", "__isset"), - ("__unset", "__unset"), - ("__sleep", "__sleep"), - ("__wakeup", "__wakeup"), - ("__serialize", "__serialize"), - ("__unserialize", "__unserialize"), - ("__to_string", "__toString"), - ("__invoke", "__invoke"), - ("__set_state", "__set_state"), - ("__clone", "__clone"), - ("__debug_info", "__debugInfo"), - ] { - assert_eq!(magic, RenameRule::None.rename(magic)); - assert_eq!(expected, RenameRule::Camel.rename(magic)); - assert_eq!(expected, RenameRule::Snake.rename(magic)); - } - } - - #[test] - fn test_rename_php_methods() { - let &(original, camel, snake) = &("get_name", "getName", "get_name"); - assert_eq!(original, RenameRule::None.rename(original)); - assert_eq!(camel, RenameRule::Camel.rename(original)); - assert_eq!(snake, RenameRule::Snake.rename(original)); - } -} diff --git a/crates/macros/src/parsing.rs b/crates/macros/src/parsing.rs index d72cdbdd9e..6b8bdfd881 100644 --- a/crates/macros/src/parsing.rs +++ b/crates/macros/src/parsing.rs @@ -1,5 +1,25 @@ use darling::FromMeta; +const MAGIC_METHOD: [&str; 17] = [ + "__construct", + "__destruct", + "__call", + "__call_static", + "__get", + "__set", + "__isset", + "__unset", + "__sleep", + "__wakeup", + "__serialize", + "__unserialize", + "__to_string", + "__invoke", + "__set_state", + "__clone", + "__debug_info", +]; + #[derive(Debug, FromMeta)] pub enum Visibility { #[darling(rename = "public")] @@ -10,6 +30,14 @@ pub enum Visibility { Protected, } +pub trait Rename { + fn rename(&self, rule: &RenameRule) -> String; +} + +pub trait MethodRename: Rename { + fn rename_method(&self, rule: &RenameRule) -> String; +} + #[derive(FromMeta, Debug, Default)] #[darling(default)] pub struct PhpRename { @@ -24,7 +52,7 @@ impl PhpRename { let name = name.as_ref(); self.rename .as_ref() - .map_or_else(|| name.to_string(), |r| r.rename(name)) + .map_or_else(|| name.to_string(), |r| name.rename(r)) }, ToString::to_string, ) @@ -52,52 +80,66 @@ pub enum RenameRule { } impl RenameRule { - /// Change case of an identifier. - /// - /// Magic methods are handled specially to make sure they're always cased - /// correctly. - pub fn rename(self, name: impl AsRef) -> String { - let name = name.as_ref(); - match self { - RenameRule::None => name.to_string(), - rule => match name { - "__construct" => "__construct".to_string(), - "__destruct" => "__destruct".to_string(), - "__call" => "__call".to_string(), - "__call_static" => "__callStatic".to_string(), - "__get" => "__get".to_string(), - "__set" => "__set".to_string(), - "__isset" => "__isset".to_string(), - "__unset" => "__unset".to_string(), - "__sleep" => "__sleep".to_string(), - "__wakeup" => "__wakeup".to_string(), - "__serialize" => "__serialize".to_string(), - "__unserialize" => "__unserialize".to_string(), - "__to_string" => "__toString".to_string(), - "__invoke" => "__invoke".to_string(), - "__set_state" => "__set_state".to_string(), - "__clone" => "__clone".to_string(), - "__debug_info" => "__debugInfo".to_string(), - field => match rule { - Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field), - Self::Pascal => ident_case::RenameRule::PascalCase.apply_to_field(field), - Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field), - Self::ScreamingSnakeCase => { - ident_case::RenameRule::ScreamingSnakeCase.apply_to_field(field) + fn rename(&self, value: impl AsRef) -> String { + match *self { + Self::None => value.as_ref().to_string(), + Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(value.as_ref()), + Self::Pascal => ident_case::RenameRule::PascalCase.apply_to_field(value.as_ref()), + Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(value.as_ref()), + Self::ScreamingSnakeCase => { + ident_case::RenameRule::ScreamingSnakeCase.apply_to_field(value.as_ref()) + } + } + } +} + +impl Rename for &str { + fn rename(&self, rule: &RenameRule) -> String { + rule.rename(self) + } +} + +impl Rename for syn::Ident { + fn rename(&self, rule: &RenameRule) -> String { + let s = self.to_string(); + rule.rename(s) + } +} + +impl MethodRename for syn::Ident { + fn rename_method(&self, rule: &RenameRule) -> String { + self.to_string().as_str().rename_method(rule) + } +} + +impl MethodRename for &str { + fn rename_method(&self, rule: &RenameRule) -> String { + match rule { + RenameRule::None => self.to_string(), + _ => { + if MAGIC_METHOD.contains(self) { + match *self { + "__to_string" => "__toString".to_string(), + "__debug_info" => "__debugInfo".to_string(), + "__call_static" => "__callStatic".to_string(), + _ => self.to_string(), } - Self::None => unreachable!(), - }, - }, + } else { + self.rename(rule) + } + } } } } #[cfg(test)] mod tests { - use super::{PhpRename, RenameRule}; + use crate::parsing::{MethodRename, Rename}; + + use super::{PhpRename, RenameRule, MAGIC_METHOD}; #[test] - fn test_php_rename() { + fn php_rename() { let rename = PhpRename { name: Some("test".to_string()), rename: None, @@ -132,7 +174,7 @@ mod tests { } #[test] - fn test_rename_magic() { + fn rename_magic_method() { for &(magic, expected) in &[ ("__construct", "__construct"), ("__destruct", "__destruct"), @@ -152,25 +194,42 @@ mod tests { ("__clone", "__clone"), ("__debug_info", "__debugInfo"), ] { - assert_eq!(magic, RenameRule::None.rename(magic)); - assert_eq!(expected, RenameRule::Camel.rename(magic)); - assert_eq!(expected, RenameRule::Pascal.rename(magic)); - assert_eq!(expected, RenameRule::Snake.rename(magic)); - assert_eq!(expected, RenameRule::ScreamingSnakeCase.rename(magic)); + assert_eq!(magic, magic.rename_method(&RenameRule::None)); + assert_eq!(expected, magic.rename_method(&RenameRule::Camel)); + assert_eq!(expected, magic.rename_method(&RenameRule::Pascal)); + assert_eq!(expected, magic.rename_method(&RenameRule::Snake)); + assert_eq!( + expected, + magic.rename_method(&RenameRule::ScreamingSnakeCase) + ); } } #[test] - fn test_rename_php_methods() { + fn rename_method() { + let &(original, camel, snake, pascal, screaming_snake) = + &("get_name", "getName", "get_name", "GetName", "GET_NAME"); + assert_eq!(original, original.rename_method(&RenameRule::None)); + assert_eq!(camel, original.rename_method(&RenameRule::Camel)); + assert_eq!(pascal, original.rename_method(&RenameRule::Pascal)); + assert_eq!(snake, original.rename_method(&RenameRule::Snake)); + assert_eq!( + screaming_snake, + original.rename_method(&RenameRule::ScreamingSnakeCase) + ); + } + + #[test] + fn rename() { let &(original, camel, snake, pascal, screaming_snake) = &("get_name", "getName", "get_name", "GetName", "GET_NAME"); - assert_eq!(original, RenameRule::None.rename(original)); - assert_eq!(camel, RenameRule::Camel.rename(original)); - assert_eq!(pascal, RenameRule::Pascal.rename(original)); - assert_eq!(snake, RenameRule::Snake.rename(original)); + assert_eq!(original, original.rename(&RenameRule::None)); + assert_eq!(camel, original.rename(&RenameRule::Camel)); + assert_eq!(pascal, original.rename(&RenameRule::Pascal)); + assert_eq!(snake, original.rename(&RenameRule::Snake)); assert_eq!( screaming_snake, - RenameRule::ScreamingSnakeCase.rename(original) + original.rename(&RenameRule::ScreamingSnakeCase) ); } } diff --git a/tests/src/integration/magic_method.php b/tests/src/integration/magic_method.php new file mode 100644 index 0000000000..069022249a --- /dev/null +++ b/tests/src/integration/magic_method.php @@ -0,0 +1,37 @@ +count = 10; +// __get +assert(10 === $magicMethod->count); +assert(null === $magicMethod->test); + +//__isset +assert(true === isset($magicMethod->count)); +assert(false === isset($magicMethod->noCount)); + +// __unset +unset($magicMethod->count); +assert(0 === $magicMethod->count); + +// __toString +assert("0" === $magicMethod->__toString()); +assert("0" === (string) $magicMethod); + +// __invoke +assert(34 === $magicMethod(34)); + +// __debugInfo +$debug = print_r($magicMethod, true); +$expectedDebug = "MagicMethod Object\n(\n [count] => 0\n)\n"; +assert($expectedDebug === $debug); + +// __call +assert("Hello" === $magicMethod->callMagicMethod(1, 2, 3)); +assert(null === $magicMethod->callUndefinedMagicMethod()); + +// __call_static +assert("Hello from static call 6" === MagicMethod::callStaticSomeMagic(1, 2, 3)); +assert(null === MagicMethod::callUndefinedStaticSomeMagic()); diff --git a/tests/src/integration/magic_method.rs b/tests/src/integration/magic_method.rs new file mode 100644 index 0000000000..145a8cf5e8 --- /dev/null +++ b/tests/src/integration/magic_method.rs @@ -0,0 +1,4 @@ +#[test] +fn magic_method() { + assert!(crate::integration::run_php("magic_method.php")); +} diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 78ce9ae6b7..ea783476f1 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -225,10 +225,101 @@ pub fn test_class(string: String, number: i32) -> TestClass { } } +#[php_class] +pub struct MagicMethod(i64); + +#[php_impl] +impl MagicMethod { + pub fn __construct() -> Self { + Self(0) + } + + pub fn __destruct(&self) {} + + pub fn __call(&self, name: String, _arguments: HashMap) -> Zval { + let mut z = Zval::new(); + if name == "callMagicMethod" { + let s = "Hello".to_string(); + + let _ = z.set_string(s.as_str(), false); + z + } else { + z.set_null(); + z + } + } + + pub fn __call_static(name: String, arguments: HashMap) -> Zval { + let mut zval = Zval::new(); + if name == "callStaticSomeMagic" { + let concat_args = format!( + "Hello from static call {}", + arguments + .iter() + .filter(|(_, v)| v.is_long()) + .map(|(_, s)| s.long().unwrap()) + .collect::>() + .iter() + .sum::() + ); + + let _ = zval.set_string(&concat_args, false); + zval + } else { + zval.set_null(); + zval + } + } + + pub fn __get(&self, name: String) -> Zval { + let mut v = Zval::new(); + v.set_null(); + if name == "count" { + v.set_long(self.0); + } + + v + } + + pub fn __set(&mut self, prop_name: String, val: &Zval) { + if val.is_long() && prop_name == "count" { + self.0 = val.long().unwrap() + } + } + + pub fn __isset(&self, prop_name: String) -> bool { + "count" == prop_name + } + + pub fn __unset(&mut self, prop_name: String) { + if prop_name == "count" { + self.0 = 0; + } + } + + pub fn __to_string(&self) -> String { + self.0.to_string() + } + + pub fn __invoke(&self, n: i64) -> i64 { + self.0 + n + } + + pub fn __debug_info(&self) -> HashMap { + let mut h: HashMap = HashMap::new(); + let mut z = Zval::new(); + z.set_long(self.0); + h.insert("count".to_string(), z); + + h + } +} + #[php_module] pub fn build_module(module: ModuleBuilder) -> ModuleBuilder { module .class::() + .class::() .function(wrap_function!(test_str)) .function(wrap_function!(test_string)) .function(wrap_function!(test_bool)) @@ -321,6 +412,7 @@ mod integration { mod closure; mod globals; mod iterator; + mod magic_method; mod nullable; mod number; mod object;