From 3fec5aadbedaf1b8013a9926cdee8a512f30fc39 Mon Sep 17 00:00:00 2001 From: Vasily Zorin Date: Mon, 22 Dec 2025 02:57:34 +0700 Subject: [PATCH] fix(macro): identifier-related bugs #536 --- crates/macros/src/class.rs | 30 ++- crates/macros/src/constant.rs | 5 +- crates/macros/src/enum_.rs | 28 ++- crates/macros/src/extern_.rs | 3 +- crates/macros/src/function.rs | 20 +- crates/macros/src/impl_.rs | 15 +- crates/macros/src/interface.rs | 30 ++- crates/macros/src/parsing.rs | 348 ++++++++++++++++++++++++++ crates/macros/src/zval.rs | 5 +- tests/src/integration/class/class.php | 4 +- tests/src/integration/class/mod.rs | 4 +- 11 files changed, 434 insertions(+), 58 deletions(-) diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 1e4da0c324..a5d72e36f7 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -5,7 +5,7 @@ use quote::{TokenStreamExt, quote}; use syn::{Attribute, Expr, Fields, ItemStruct}; use crate::helpers::get_docs; -use crate::parsing::{PhpRename, RenameRule}; +use crate::parsing::{PhpNameContext, PhpRename, RenameRule, ident_to_php_name, validate_php_name}; use crate::prelude::*; #[derive(FromAttributes, Debug, Default)] @@ -44,7 +44,10 @@ impl ToTokens for ClassEntryAttribute { pub fn parser(mut input: ItemStruct) -> Result { let attr = StructAttributes::from_attributes(&input.attrs)?; let ident = &input.ident; - let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); + let name = attr + .rename + .rename(ident_to_php_name(ident), RenameRule::Pascal); + validate_php_name(&name, PhpNameContext::Class, ident.span())?; let docs = get_docs(&attr.attrs)?; input.attrs.retain(|attr| !attr.path().is_ident("php")); @@ -97,7 +100,17 @@ fn parse_fields<'a>(fields: impl Iterator) -> Result< let docs = get_docs(&attr.attrs)?; field.attrs.retain(|attr| !attr.path().is_ident("php")); - result.push(Property { ident, attr, docs }); + let name = attr + .rename + .rename(ident_to_php_name(ident), RenameRule::Camel); + validate_php_name(&name, PhpNameContext::Property, ident.span())?; + + result.push(Property { + ident, + name, + attr, + docs, + }); } } @@ -107,17 +120,12 @@ fn parse_fields<'a>(fields: impl Iterator) -> Result< #[derive(Debug)] struct Property<'a> { pub ident: &'a syn::Ident, + pub name: String, pub attr: PropAttributes, pub docs: Vec, } impl Property<'_> { - pub fn name(&self) -> String { - self.attr - .rename - .rename(self.ident.to_string(), RenameRule::Camel) - } - pub fn is_static(&self) -> bool { self.attr.static_.is_present() } @@ -143,7 +151,7 @@ fn generate_registered_class_impl( // Generate instance properties (with Rust handlers) let instance_fields = instance_props.iter().map(|prop| { - let name = prop.name(); + let name = &prop.name; let field_ident = prop.ident; let flags = prop .attr @@ -166,7 +174,7 @@ fn generate_registered_class_impl( // We combine the base flags with Static flag using from_bits_retain which is // const let static_fields = static_props.iter().map(|prop| { - let name = prop.name(); + let name = &prop.name; let base_flags = prop .attr .flags diff --git a/crates/macros/src/constant.rs b/crates/macros/src/constant.rs index bd7262f5e3..3ebbe92060 100644 --- a/crates/macros/src/constant.rs +++ b/crates/macros/src/constant.rs @@ -4,7 +4,7 @@ use quote::{format_ident, quote}; use syn::ItemConst; use crate::helpers::get_docs; -use crate::parsing::{PhpRename, RenameRule}; +use crate::parsing::{PhpNameContext, PhpRename, RenameRule, ident_to_php_name, validate_php_name}; use crate::prelude::*; const INTERNAL_CONST_DOC_PREFIX: &str = "_internal_const_docs_"; @@ -25,7 +25,8 @@ pub fn parser(mut item: ItemConst) -> Result { let name = attr .rename - .rename(item.ident.to_string(), RenameRule::ScreamingSnake); + .rename(ident_to_php_name(&item.ident), RenameRule::ScreamingSnake); + validate_php_name(&name, PhpNameContext::Constant, item.ident.span())?; let name_ident = format_ident!("{INTERNAL_CONST_NAME_PREFIX}{}", item.ident); let docs = get_docs(&attr.attrs)?; diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs index 910f05cf21..4f7aa1c168 100644 --- a/crates/macros/src/enum_.rs +++ b/crates/macros/src/enum_.rs @@ -8,7 +8,9 @@ use syn::{Fields, Ident, ItemEnum, Lit}; use crate::{ helpers::get_docs, - parsing::{PhpRename, RenameRule, Visibility}, + parsing::{ + PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name, validate_php_name, + }, prelude::*, }; @@ -81,12 +83,15 @@ pub fn parser(mut input: ItemEnum) -> Result { bail!(variant => "Discriminant must be specified for all enum cases, found: {:?}", variant); } + let case_name = variant_attr.rename.rename( + ident_to_php_name(&variant.ident), + php_attr.rename_cases.unwrap_or(RenameRule::Pascal), + ); + validate_php_name(&case_name, PhpNameContext::EnumCase, variant.ident.span())?; + cases.push(EnumCase { ident: variant.ident.clone(), - name: variant_attr.rename.rename( - variant.ident.to_string(), - php_attr.rename_cases.unwrap_or(RenameRule::Pascal), - ), + name: case_name, attrs: variant_attr, discriminant, docs, @@ -108,7 +113,7 @@ pub fn parser(mut input: ItemEnum) -> Result { cases, None, // TODO: Implement flags support discriminant_type, - ); + )?; Ok(quote! { #[allow(dead_code)] @@ -136,17 +141,20 @@ impl<'a> Enum<'a> { cases: Vec, flags: Option, discriminant_type: DiscriminantType, - ) -> Self { - let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal); + ) -> Result { + let name = attrs + .rename + .rename(ident_to_php_name(ident), RenameRule::Pascal); + validate_php_name(&name, PhpNameContext::Enum, ident.span())?; - Self { + Ok(Self { ident, name, discriminant_type, docs, cases, flags, - } + }) } fn registered_class(&self) -> TokenStream { diff --git a/crates/macros/src/extern_.rs b/crates/macros/src/extern_.rs index 8718b2a71e..b9d997322d 100644 --- a/crates/macros/src/extern_.rs +++ b/crates/macros/src/extern_.rs @@ -5,6 +5,7 @@ use syn::{ spanned::Spanned as _, token::Unsafe, }; +use crate::parsing::ident_to_php_name; use crate::prelude::*; pub fn parser(input: ItemForeignMod) -> Result { @@ -27,7 +28,7 @@ fn parse_function(mut func: ForeignItemFn) -> Result { let Signature { ident, .. } = &sig; - let name = ident.to_string(); + let name = ident_to_php_name(ident); let params = sig .inputs .iter() diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index bcc4865ccf..ee9cd841a9 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -7,7 +7,9 @@ use syn::spanned::Spanned as _; use syn::{Expr, FnArg, GenericArgument, ItemFn, PatType, PathArguments, Type, TypePath}; use crate::helpers::get_docs; -use crate::parsing::{PhpRename, RenameRule, Visibility}; +use crate::parsing::{ + PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name, validate_php_name, +}; use crate::prelude::*; use crate::syn_ext::DropLifetimes; @@ -44,15 +46,11 @@ pub fn parser(mut input: ItemFn) -> Result { let docs = get_docs(&php_attr.attrs)?; - let func = Function::new( - &input.sig, - php_attr - .rename - .rename(input.sig.ident.to_string(), RenameRule::Snake), - args, - php_attr.optional, - docs, - ); + let func_name = php_attr + .rename + .rename(ident_to_php_name(&input.sig.ident), RenameRule::Snake); + validate_php_name(&func_name, PhpNameContext::Function, input.sig.ident.span())?; + let func = Function::new(&input.sig, func_name, args, php_attr.optional, docs); let function_impl = func.php_function_impl(); Ok(quote! { @@ -625,7 +623,7 @@ impl TypedArg<'_> { /// Returns a token stream containing the `Arg` definition to be passed to /// `ext-php-rs`. fn arg_builder(&self) -> TokenStream { - let name = self.name.to_string(); + let name = ident_to_php_name(self.name); let ty = self.clean_ty(); let null = if self.nullable { Some(quote! { .allow_null() }) diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 1d96871493..52c21a062b 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -8,7 +8,9 @@ use syn::{Expr, Ident, ItemImpl}; 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::{ + PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name, validate_php_name, +}; use crate::prelude::*; /// Method types. @@ -187,7 +189,8 @@ impl<'a> ParsedImpl<'a> { let attr = PhpConstAttribute::from_attributes(&c.attrs)?; let name = attr .rename - .rename(c.ident.to_string(), self.change_constant_case); + .rename(ident_to_php_name(&c.ident), self.change_constant_case); + validate_php_name(&name, PhpNameContext::Constant, c.ident.span())?; let docs = get_docs(&attr.attrs)?; c.attrs.retain(|attr| !attr.path().is_ident("php")); @@ -199,9 +202,11 @@ impl<'a> ParsedImpl<'a> { } syn::ImplItem::Fn(method) => { let attr = PhpFunctionImplAttribute::from_attributes(&method.attrs)?; - let name = attr - .rename - .rename_method(method.sig.ident.to_string(), self.change_method_case); + let name = attr.rename.rename_method( + ident_to_php_name(&method.sig.ident), + self.change_method_case, + ); + validate_php_name(&name, PhpNameContext::Method, method.sig.ident.span())?; let docs = get_docs(&attr.attrs)?; method.attrs.retain(|attr| !attr.path().is_ident("php")); diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 6e9a54a4f9..756f40eec1 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -11,7 +11,9 @@ use quote::{ToTokens, format_ident, quote}; use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn}; use crate::impl_::{FnBuilder, MethodModifier}; -use crate::parsing::{PhpRename, RenameRule, Visibility}; +use crate::parsing::{ + PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name, validate_php_name, +}; use crate::prelude::*; const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface"; @@ -196,7 +198,10 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { fn parse(&'a mut self) -> Result> { let attrs = TraitAttributes::from_attributes(&self.attrs)?; let ident = &self.ident; - let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal); + let name = attrs + .rename + .rename(ident_to_php_name(ident), RenameRule::Pascal); + validate_php_name(&name, PhpNameContext::Interface, ident.span())?; let docs = get_docs(&attrs.attrs)?; self.attrs.clean_php(); let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}"); @@ -277,16 +282,16 @@ fn parse_trait_item_fn( modifiers.insert(MethodModifier::Static); } - let f = Function::new( - &fn_item.sig, - php_attr.rename.rename( - fn_item.sig.ident.to_string(), - change_case.unwrap_or(RenameRule::Camel), - ), - args, - php_attr.optional, - docs, + let method_name = php_attr.rename.rename( + ident_to_php_name(&fn_item.sig.ident), + change_case.unwrap_or(RenameRule::Camel), ); + validate_php_name( + &method_name, + PhpNameContext::Method, + fn_item.sig.ident.span(), + )?; + let f = Function::new(&fn_item.sig, method_name, args, php_attr.optional, docs); if php_attr.constructor.is_present() { Ok(MethodKind::Constructor(f)) @@ -336,9 +341,10 @@ fn parse_trait_item_const( let attr = PhpConstAttribute::from_attributes(&const_item.attrs)?; let name = attr.rename.rename( - const_item.ident.to_string(), + ident_to_php_name(&const_item.ident), change_case.unwrap_or(RenameRule::ScreamingSnake), ); + validate_php_name(&name, PhpNameContext::Constant, const_item.ident.span())?; let docs = get_docs(&attr.attrs)?; const_item.attrs.clean_php(); diff --git a/crates/macros/src/parsing.rs b/crates/macros/src/parsing.rs index 849654a8f1..573a280dbb 100644 --- a/crates/macros/src/parsing.rs +++ b/crates/macros/src/parsing.rs @@ -1,6 +1,230 @@ use convert_case::{Case, Casing}; use darling::FromMeta; use quote::{ToTokens, quote}; +use syn::Ident; + +/// Converts a Rust identifier to its PHP-compatible name. +/// +/// This function strips the `r#` prefix from raw identifiers, since that prefix +/// is Rust-specific syntax for using reserved keywords as identifiers. +/// +/// # Examples +/// +/// ```ignore +/// use syn::parse_quote; +/// let ident: Ident = parse_quote!(r#as); +/// assert_eq!(ident_to_php_name(&ident), "as"); +/// +/// let ident: Ident = parse_quote!(normal_name); +/// assert_eq!(ident_to_php_name(&ident), "normal_name"); +/// ``` +pub fn ident_to_php_name(ident: &Ident) -> String { + let name = ident.to_string(); + name.strip_prefix("r#").unwrap_or(&name).to_string() +} + +/// PHP reserved keywords that cannot be used as class, interface, trait, enum, +/// or function names. +/// +/// See: +const PHP_RESERVED_KEYWORDS: &[&str] = &[ + // Keywords + "__halt_compiler", + "abstract", + "and", + "array", + "as", + "break", + "callable", + "case", + "catch", + "class", + "clone", + "const", + "continue", + "declare", + "default", + "die", + "do", + "echo", + "else", + "elseif", + "empty", + "enum", + "enddeclare", + "endfor", + "endforeach", + "endif", + "endswitch", + "endwhile", + "eval", + "exit", + "extends", + "final", + "finally", + "fn", + "for", + "foreach", + "function", + "global", + "goto", + "if", + "implements", + "include", + "include_once", + "instanceof", + "insteadof", + "interface", + "isset", + "list", + "match", + "namespace", + "new", + "or", + "print", + "private", + "protected", + "public", + "readonly", + "require", + "require_once", + "return", + "static", + "switch", + "throw", + "trait", + "try", + "unset", + "use", + "var", + "while", + "xor", + "yield", + "yield from", + // Compile-time constants + "__CLASS__", + "__DIR__", + "__FILE__", + "__FUNCTION__", + "__LINE__", + "__METHOD__", + "__NAMESPACE__", + "__TRAIT__", + // Reserved classes (case-insensitive check needed) + "self", + "parent", +]; + +/// Type keywords that are reserved for class/interface/enum names but CAN be +/// used as method, function, constant, or property names in PHP. +/// +/// Note: `resource` and `numeric` are NOT in this list because PHP allows them +/// as class names. See: +const PHP_TYPE_KEYWORDS: &[&str] = &[ + "bool", "false", "float", "int", "iterable", "mixed", "never", "null", "object", "string", + "true", "void", +]; + +/// The context in which a PHP name is being used. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PhpNameContext { + /// A class name (e.g., `class Foo {}`) + Class, + /// An interface name (e.g., `interface Foo {}`) + Interface, + /// An enum name (e.g., `enum Foo {}`) + Enum, + /// An enum case name (e.g., `case Foo;`) + EnumCase, + /// A function name (e.g., `function foo() {}`) + Function, + /// A method name (e.g., `public function foo() {}`) + Method, + /// A constant name (e.g., `const FOO = 1;`) + Constant, + /// A property name (e.g., `public $foo;`) + Property, +} + +impl PhpNameContext { + fn description(self) -> &'static str { + match self { + Self::Class => "class", + Self::Interface => "interface", + Self::Enum => "enum", + Self::EnumCase => "enum case", + Self::Function => "function", + Self::Method => "method", + Self::Constant => "constant", + Self::Property => "property", + } + } +} + +/// Checks if a name is a PHP type keyword (case-insensitive). +/// +/// Type keywords like `void`, `bool`, `int`, etc. are reserved for type declarations +/// but CAN be used as method, function, constant, or property names in PHP. +fn is_php_type_keyword(name: &str) -> bool { + let lower = name.to_lowercase(); + PHP_TYPE_KEYWORDS + .iter() + .any(|&kw| kw.to_lowercase() == lower) +} + +/// Checks if a name is a PHP reserved keyword (case-insensitive). +pub fn is_php_reserved_keyword(name: &str) -> bool { + let lower = name.to_lowercase(); + PHP_RESERVED_KEYWORDS + .iter() + .any(|&kw| kw.to_lowercase() == lower) +} + +/// Validates that a PHP name is not a reserved keyword. +/// +/// The validation is context-aware: +/// - For class, interface, enum, and enum case names: both reserved keywords AND type keywords are checked +/// - For method, function, constant, and property names: only reserved keywords are checked +/// (type keywords like `void`, `bool`, etc. are allowed) +/// +/// # Errors +/// +/// Returns a `syn::Error` if the name is a reserved keyword in the given context. +pub fn validate_php_name( + name: &str, + context: PhpNameContext, + span: proc_macro2::Span, +) -> Result<(), syn::Error> { + let is_reserved = is_php_reserved_keyword(name); + let is_type = is_php_type_keyword(name); + + // Type keywords are forbidden for class/interface/enum/enum case names + let is_forbidden = match context { + PhpNameContext::Class + | PhpNameContext::Interface + | PhpNameContext::Enum + | PhpNameContext::EnumCase => is_reserved || is_type, + PhpNameContext::Function + | PhpNameContext::Method + | PhpNameContext::Constant + | PhpNameContext::Property => is_reserved, + }; + + if is_forbidden { + return Err(syn::Error::new( + span, + format!( + "cannot use '{}' as a PHP {} name: '{}' is a reserved keyword in PHP. \ + Consider using a different name or the #[php(name = \"...\")] attribute to specify an alternative PHP name.", + name, + context.description(), + name + ), + )); + } + + Ok(()) +} const MAGIC_METHOD: [&str; 17] = [ "__construct", @@ -333,4 +557,128 @@ mod tests { assert_eq!(snake, original.rename(RenameRule::Snake)); assert_eq!(screaming_snake, original.rename(RenameRule::ScreamingSnake)); } + + #[test] + fn ident_to_php_name_strips_raw_prefix() { + use super::ident_to_php_name; + use syn::parse_quote; + + // Raw identifier should have r# prefix stripped + let raw_ident: syn::Ident = parse_quote!(r#as); + assert_eq!(ident_to_php_name(&raw_ident), "as"); + + let raw_ident: syn::Ident = parse_quote!(r#match); + assert_eq!(ident_to_php_name(&raw_ident), "match"); + + let raw_ident: syn::Ident = parse_quote!(r#type); + assert_eq!(ident_to_php_name(&raw_ident), "type"); + + // Normal identifiers should be unchanged + let normal_ident: syn::Ident = parse_quote!(normal_name); + assert_eq!(ident_to_php_name(&normal_ident), "normal_name"); + + let normal_ident: syn::Ident = parse_quote!(foo); + assert_eq!(ident_to_php_name(&normal_ident), "foo"); + } + + #[test] + fn test_is_php_reserved_keyword() { + use super::is_php_reserved_keyword; + + // Hard keywords should be detected + assert!(is_php_reserved_keyword("class")); + assert!(is_php_reserved_keyword("function")); + assert!(is_php_reserved_keyword("match")); + + // Case-insensitive + assert!(is_php_reserved_keyword("CLASS")); + assert!(is_php_reserved_keyword("FUNCTION")); + + // Type keywords are NOT in the reserved list (they're in PHP_TYPE_KEYWORDS) + assert!(!is_php_reserved_keyword("void")); + assert!(!is_php_reserved_keyword("true")); + assert!(!is_php_reserved_keyword("bool")); + + // Non-keywords should pass + assert!(!is_php_reserved_keyword("MyClass")); + assert!(!is_php_reserved_keyword("foo")); + } + + #[test] + fn test_validate_php_name_rejects_reserved_keyword() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + let result = validate_php_name("class", PhpNameContext::Class, Span::call_site()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("is a reserved keyword in PHP")); + } + + #[test] + fn test_validate_php_name_rejects_type_keyword_for_class() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + // Type keywords like 'void' cannot be used as class names + let result = validate_php_name("void", PhpNameContext::Class, Span::call_site()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("is a reserved keyword in PHP")); + } + + #[test] + fn test_validate_php_name_rejects_type_keyword_for_enum_case() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + // Type keywords like 'true' cannot be used as enum case names + let result = validate_php_name("true", PhpNameContext::EnumCase, Span::call_site()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("is a reserved keyword in PHP")); + + let result = validate_php_name("false", PhpNameContext::EnumCase, Span::call_site()); + assert!(result.is_err()); + } + + #[test] + fn test_validate_php_name_allows_type_keyword_for_method() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + // Type keywords like 'void' CAN be used as method names in PHP + validate_php_name("void", PhpNameContext::Method, Span::call_site()).unwrap(); + validate_php_name("true", PhpNameContext::Method, Span::call_site()).unwrap(); + validate_php_name("bool", PhpNameContext::Method, Span::call_site()).unwrap(); + validate_php_name("int", PhpNameContext::Method, Span::call_site()).unwrap(); + } + + #[test] + fn test_validate_php_name_allows_type_keyword_for_function() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + // Type keywords CAN be used as function names in PHP + validate_php_name("void", PhpNameContext::Function, Span::call_site()).unwrap(); + } + + #[test] + fn test_validate_php_name_allows_type_keyword_for_constant() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + // Type keywords CAN be used as constant names in PHP + validate_php_name("void", PhpNameContext::Constant, Span::call_site()).unwrap(); + } + + #[test] + fn test_validate_php_name_allows_resource_and_numeric_for_class() { + use super::{PhpNameContext, validate_php_name}; + use proc_macro2::Span; + + // 'resource' and 'numeric' are NOT reserved for class names in PHP + validate_php_name("resource", PhpNameContext::Class, Span::call_site()).unwrap(); + validate_php_name("numeric", PhpNameContext::Class, Span::call_site()).unwrap(); + } } diff --git a/crates/macros/src/zval.rs b/crates/macros/src/zval.rs index ddfaf78009..46c34b0d2e 100644 --- a/crates/macros/src/zval.rs +++ b/crates/macros/src/zval.rs @@ -6,6 +6,7 @@ use syn::{ LifetimeParam, TypeGenerics, Variant, WhereClause, punctuated::Punctuated, token::Where, }; +use crate::parsing::ident_to_php_name; use crate::prelude::*; pub fn parser(input: DeriveInput) -> Result { @@ -106,7 +107,7 @@ fn parse_struct( let ident = field.ident.as_ref().ok_or_else(|| { err!(field => "Fields require names when using the `#[derive(ZvalConvert)]` macro on a struct.") })?; - let field_name = ident.to_string(); + let field_name = ident_to_php_name(ident); Ok(quote! { obj.set_property(#field_name, self.#ident)?; @@ -121,7 +122,7 @@ fn parse_struct( let ident = field.ident.as_ref().ok_or_else(|| { err!(field => "Fields require names when using the `#[derive(ZvalConvert)]` macro on a struct.") })?; - let field_name = ident.to_string(); + let field_name = ident_to_php_name(ident); Ok(quote! { #ident: obj.get_property(#field_name)?, diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index 5a0005a1d5..3ab64f025f 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -46,8 +46,8 @@ $classReflection = new ReflectionClass(TestClassMethodVisibility::class); assert($classReflection->getMethod('__construct')->isPrivate()); -assert($classReflection->getMethod('private')->isPrivate()); -assert($classReflection->getMethod('protected')->isProtected()); +assert($classReflection->getMethod('privateMethod')->isPrivate()); +assert($classReflection->getMethod('protectedMethod')->isProtected()); $classReflection = new ReflectionClass(TestClassProtectedConstruct::class); assert($classReflection->getMethod('__construct')->isProtected()); diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index 293b5f6f14..f78321d977 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -156,12 +156,12 @@ impl TestClassMethodVisibility { } #[php(vis = "private")] - fn private() -> u32 { + fn private_method() -> u32 { 3 } #[php(vis = "protected")] - fn protected() -> u32 { + fn protected_method() -> u32 { 3 } }