@@ -117,9 +117,12 @@ const PHP_RESERVED_KEYWORDS: &[&str] = &[
117117
118118/// Type keywords that are reserved for class/interface/enum names but CAN be
119119/// used as method, function, constant, or property names in PHP.
120+ ///
121+ /// Note: `resource` and `numeric` are NOT in this list because PHP allows them
122+ /// as class names. See: <https://github.com/php/php-src/blob/master/Zend/zend_compile.c>
120123const PHP_TYPE_KEYWORDS : & [ & str ] = & [
121- "bool" , "false" , "float" , "int" , "iterable" , "mixed" , "never" , "null" , "numeric " , "object " ,
122- "resource" , "string" , " true", "void" ,
124+ "bool" , "false" , "float" , "int" , "iterable" , "mixed" , "never" , "null" , "object " , "string " ,
125+ "true" , "void" ,
123126] ;
124127
125128/// The context in which a PHP name is being used.
@@ -131,6 +134,8 @@ pub enum PhpNameContext {
131134 Interface ,
132135 /// An enum name (e.g., `enum Foo {}`)
133136 Enum ,
137+ /// An enum case name (e.g., `case Foo;`)
138+ EnumCase ,
134139 /// A function name (e.g., `function foo() {}`)
135140 Function ,
136141 /// A method name (e.g., `public function foo() {}`)
@@ -147,6 +152,7 @@ impl PhpNameContext {
147152 Self :: Class => "class" ,
148153 Self :: Interface => "interface" ,
149154 Self :: Enum => "enum" ,
155+ Self :: EnumCase => "enum case" ,
150156 Self :: Function => "function" ,
151157 Self :: Method => "method" ,
152158 Self :: Constant => "constant" ,
@@ -177,36 +183,47 @@ pub fn is_php_reserved_keyword(name: &str) -> bool {
177183/// Validates that a PHP name is not a reserved keyword.
178184///
179185/// The validation is context-aware:
180- /// - For class, interface, and enum names: both reserved keywords AND type keywords are checked
186+ /// - For class, interface, enum, and enum case names: both reserved keywords AND type keywords are checked
181187/// - For method, function, constant, and property names: only reserved keywords are checked
182188/// (type keywords like `void`, `bool`, etc. are allowed)
183189///
184- /// # Panics
190+ /// # Errors
185191///
186- /// Panics with a descriptive error message if the name is a reserved keyword in the given context.
187- pub fn validate_php_name ( name : & str , context : PhpNameContext ) {
192+ /// Returns a `syn::Error` if the name is a reserved keyword in the given context.
193+ pub fn validate_php_name (
194+ name : & str ,
195+ context : PhpNameContext ,
196+ span : proc_macro2:: Span ,
197+ ) -> Result < ( ) , syn:: Error > {
188198 let is_reserved = is_php_reserved_keyword ( name) ;
189199 let is_type = is_php_type_keyword ( name) ;
190200
191- // Type keywords are only forbidden for class/interface/enum names
201+ // Type keywords are forbidden for class/interface/enum/enum case names
192202 let is_forbidden = match context {
193- PhpNameContext :: Class | PhpNameContext :: Interface | PhpNameContext :: Enum => {
194- is_reserved || is_type
195- }
203+ PhpNameContext :: Class
204+ | PhpNameContext :: Interface
205+ | PhpNameContext :: Enum
206+ | PhpNameContext :: EnumCase => is_reserved || is_type,
196207 PhpNameContext :: Function
197208 | PhpNameContext :: Method
198209 | PhpNameContext :: Constant
199210 | PhpNameContext :: Property => is_reserved,
200211 } ;
201212
202- assert ! (
203- !is_forbidden,
204- "Cannot use '{}' as a PHP {} name: '{}' is a reserved keyword in PHP. \
205- Consider using a different name or the #[php(name = \" ...\" )] attribute to specify an alternative PHP name.",
206- name,
207- context. description( ) ,
208- name
209- ) ;
213+ if is_forbidden {
214+ return Err ( syn:: Error :: new (
215+ span,
216+ format ! (
217+ "cannot use '{}' as a PHP {} name: '{}' is a reserved keyword in PHP. \
218+ Consider using a different name or the #[php(name = \" ...\" )] attribute to specify an alternative PHP name.",
219+ name,
220+ context. description( ) ,
221+ name
222+ ) ,
223+ ) ) ;
224+ }
225+
226+ Ok ( ( ) )
210227}
211228
212229const MAGIC_METHOD : [ & str ; 17 ] = [
@@ -588,41 +605,80 @@ mod tests {
588605 }
589606
590607 #[ test]
591- #[ should_panic( expected = "is a reserved keyword in PHP" ) ]
592608 fn test_validate_php_name_rejects_reserved_keyword ( ) {
593609 use super :: { PhpNameContext , validate_php_name} ;
594- validate_php_name ( "class" , PhpNameContext :: Class ) ;
610+ use proc_macro2:: Span ;
611+
612+ let result = validate_php_name ( "class" , PhpNameContext :: Class , Span :: call_site ( ) ) ;
613+ assert ! ( result. is_err( ) ) ;
614+ let err = result. unwrap_err ( ) ;
615+ assert ! ( err. to_string( ) . contains( "is a reserved keyword in PHP" ) ) ;
595616 }
596617
597618 #[ test]
598- #[ should_panic( expected = "is a reserved keyword in PHP" ) ]
599619 fn test_validate_php_name_rejects_type_keyword_for_class ( ) {
600620 use super :: { PhpNameContext , validate_php_name} ;
621+ use proc_macro2:: Span ;
622+
601623 // Type keywords like 'void' cannot be used as class names
602- validate_php_name ( "void" , PhpNameContext :: Class ) ;
624+ let result = validate_php_name ( "void" , PhpNameContext :: Class , Span :: call_site ( ) ) ;
625+ assert ! ( result. is_err( ) ) ;
626+ let err = result. unwrap_err ( ) ;
627+ assert ! ( err. to_string( ) . contains( "is a reserved keyword in PHP" ) ) ;
628+ }
629+
630+ #[ test]
631+ fn test_validate_php_name_rejects_type_keyword_for_enum_case ( ) {
632+ use super :: { PhpNameContext , validate_php_name} ;
633+ use proc_macro2:: Span ;
634+
635+ // Type keywords like 'true' cannot be used as enum case names
636+ let result = validate_php_name ( "true" , PhpNameContext :: EnumCase , Span :: call_site ( ) ) ;
637+ assert ! ( result. is_err( ) ) ;
638+ let err = result. unwrap_err ( ) ;
639+ assert ! ( err. to_string( ) . contains( "is a reserved keyword in PHP" ) ) ;
640+
641+ let result = validate_php_name ( "false" , PhpNameContext :: EnumCase , Span :: call_site ( ) ) ;
642+ assert ! ( result. is_err( ) ) ;
603643 }
604644
605645 #[ test]
606646 fn test_validate_php_name_allows_type_keyword_for_method ( ) {
607647 use super :: { PhpNameContext , validate_php_name} ;
648+ use proc_macro2:: Span ;
649+
608650 // Type keywords like 'void' CAN be used as method names in PHP
609- validate_php_name ( "void" , PhpNameContext :: Method ) ;
610- validate_php_name ( "true" , PhpNameContext :: Method ) ;
611- validate_php_name ( "bool" , PhpNameContext :: Method ) ;
612- validate_php_name ( "int" , PhpNameContext :: Method ) ;
651+ validate_php_name ( "void" , PhpNameContext :: Method , Span :: call_site ( ) ) . unwrap ( ) ;
652+ validate_php_name ( "true" , PhpNameContext :: Method , Span :: call_site ( ) ) . unwrap ( ) ;
653+ validate_php_name ( "bool" , PhpNameContext :: Method , Span :: call_site ( ) ) . unwrap ( ) ;
654+ validate_php_name ( "int" , PhpNameContext :: Method , Span :: call_site ( ) ) . unwrap ( ) ;
613655 }
614656
615657 #[ test]
616658 fn test_validate_php_name_allows_type_keyword_for_function ( ) {
617659 use super :: { PhpNameContext , validate_php_name} ;
660+ use proc_macro2:: Span ;
661+
618662 // Type keywords CAN be used as function names in PHP
619- validate_php_name ( "void" , PhpNameContext :: Function ) ;
663+ validate_php_name ( "void" , PhpNameContext :: Function , Span :: call_site ( ) ) . unwrap ( ) ;
620664 }
621665
622666 #[ test]
623667 fn test_validate_php_name_allows_type_keyword_for_constant ( ) {
624668 use super :: { PhpNameContext , validate_php_name} ;
669+ use proc_macro2:: Span ;
670+
625671 // Type keywords CAN be used as constant names in PHP
626- validate_php_name ( "void" , PhpNameContext :: Constant ) ;
672+ validate_php_name ( "void" , PhpNameContext :: Constant , Span :: call_site ( ) ) . unwrap ( ) ;
673+ }
674+
675+ #[ test]
676+ fn test_validate_php_name_allows_resource_and_numeric_for_class ( ) {
677+ use super :: { PhpNameContext , validate_php_name} ;
678+ use proc_macro2:: Span ;
679+
680+ // 'resource' and 'numeric' are NOT reserved for class names in PHP
681+ validate_php_name ( "resource" , PhpNameContext :: Class , Span :: call_site ( ) ) . unwrap ( ) ;
682+ validate_php_name ( "numeric" , PhpNameContext :: Class , Span :: call_site ( ) ) . unwrap ( ) ;
627683 }
628684}
0 commit comments