@@ -613,10 +613,173 @@ interface Factory {
613613 assert_eq ! ( classes[ 0 ] . name, "Factory" ) ;
614614 assert_eq ! ( classes[ 0 ] . methods. len( ) , 2 ) ;
615615
616- let create = classes[ 0 ] . methods . iter ( ) . find ( |m| m. name == "create" ) . unwrap ( ) ;
616+ let create = classes[ 0 ]
617+ . methods
618+ . iter ( )
619+ . find ( |m| m. name == "create" )
620+ . unwrap ( ) ;
617621 assert ! ( create. is_static) ;
618622 assert_eq ! ( create. return_type. as_deref( ) , Some ( "static" ) ) ;
619623
620- let build = classes[ 0 ] . methods . iter ( ) . find ( |m| m. name == "build" ) . unwrap ( ) ;
624+ let build = classes[ 0 ]
625+ . methods
626+ . iter ( )
627+ . find ( |m| m. name == "build" )
628+ . unwrap ( ) ;
621629 assert ! ( !build. is_static) ;
622630}
631+
632+ // ─── Promoted Property Tests ────────────────────────────────────────────────
633+
634+ #[ tokio:: test]
635+ async fn test_parse_php_promoted_properties_basic ( ) {
636+ let backend = create_test_backend ( ) ;
637+ let php = r#"<?php
638+ class Service {
639+ public function __construct(
640+ private IShoppingCart $cart,
641+ protected Logger $logger,
642+ ) {}
643+ }
644+ "# ;
645+
646+ let classes = backend. parse_php ( php) ;
647+ assert_eq ! ( classes. len( ) , 1 ) ;
648+
649+ let cls = & classes[ 0 ] ;
650+ assert_eq ! (
651+ cls. properties. len( ) ,
652+ 2 ,
653+ "Should extract 2 promoted properties"
654+ ) ;
655+
656+ let cart = cls. properties . iter ( ) . find ( |p| p. name == "cart" ) . unwrap ( ) ;
657+ assert_eq ! ( cart. type_hint. as_deref( ) , Some ( "IShoppingCart" ) ) ;
658+ assert_eq ! ( cart. visibility, Visibility :: Private ) ;
659+ assert ! ( !cart. is_static) ;
660+
661+ let logger = cls. properties . iter ( ) . find ( |p| p. name == "logger" ) . unwrap ( ) ;
662+ assert_eq ! ( logger. type_hint. as_deref( ) , Some ( "Logger" ) ) ;
663+ assert_eq ! ( logger. visibility, Visibility :: Protected ) ;
664+ assert ! ( !logger. is_static) ;
665+ }
666+
667+ #[ tokio:: test]
668+ async fn test_parse_php_promoted_properties_mixed_with_regular ( ) {
669+ let backend = create_test_backend ( ) ;
670+ let php = r#"<?php
671+ class ShoppingCartService {
672+ private IShoppingCart $regular;
673+
674+ public function __construct(
675+ private IShoppingCart $promoted,
676+ ) {}
677+ }
678+ "# ;
679+
680+ let classes = backend. parse_php ( php) ;
681+ assert_eq ! ( classes. len( ) , 1 ) ;
682+
683+ let cls = & classes[ 0 ] ;
684+ assert_eq ! (
685+ cls. properties. len( ) ,
686+ 2 ,
687+ "Should have regular + promoted property"
688+ ) ;
689+
690+ let regular = cls. properties . iter ( ) . find ( |p| p. name == "regular" ) . unwrap ( ) ;
691+ assert_eq ! ( regular. type_hint. as_deref( ) , Some ( "IShoppingCart" ) ) ;
692+ assert_eq ! ( regular. visibility, Visibility :: Private ) ;
693+
694+ let promoted = cls
695+ . properties
696+ . iter ( )
697+ . find ( |p| p. name == "promoted" )
698+ . unwrap ( ) ;
699+ assert_eq ! ( promoted. type_hint. as_deref( ) , Some ( "IShoppingCart" ) ) ;
700+ assert_eq ! ( promoted. visibility, Visibility :: Private ) ;
701+ }
702+
703+ #[ tokio:: test]
704+ async fn test_parse_php_promoted_property_public_visibility ( ) {
705+ let backend = create_test_backend ( ) ;
706+ let php = r#"<?php
707+ class Config {
708+ public function __construct(
709+ public string $name,
710+ public int $value,
711+ ) {}
712+ }
713+ "# ;
714+
715+ let classes = backend. parse_php ( php) ;
716+ assert_eq ! ( classes. len( ) , 1 ) ;
717+
718+ let cls = & classes[ 0 ] ;
719+ assert_eq ! ( cls. properties. len( ) , 2 ) ;
720+
721+ for prop in & cls. properties {
722+ assert_eq ! ( prop. visibility, Visibility :: Public ) ;
723+ }
724+
725+ let name = cls. properties . iter ( ) . find ( |p| p. name == "name" ) . unwrap ( ) ;
726+ assert_eq ! ( name. type_hint. as_deref( ) , Some ( "string" ) ) ;
727+
728+ let value = cls. properties . iter ( ) . find ( |p| p. name == "value" ) . unwrap ( ) ;
729+ assert_eq ! ( value. type_hint. as_deref( ) , Some ( "int" ) ) ;
730+ }
731+
732+ #[ tokio:: test]
733+ async fn test_parse_php_non_promoted_constructor_params_ignored ( ) {
734+ let backend = create_test_backend ( ) ;
735+ let php = r#"<?php
736+ class Service {
737+ public function __construct(
738+ private string $promoted,
739+ string $regularParam,
740+ ) {}
741+ }
742+ "# ;
743+
744+ let classes = backend. parse_php ( php) ;
745+ assert_eq ! ( classes. len( ) , 1 ) ;
746+
747+ let cls = & classes[ 0 ] ;
748+ assert_eq ! (
749+ cls. properties. len( ) ,
750+ 1 ,
751+ "Only promoted params (with visibility) should become properties"
752+ ) ;
753+ assert_eq ! ( cls. properties[ 0 ] . name, "promoted" ) ;
754+ }
755+
756+ #[ tokio:: test]
757+ async fn test_parse_php_promoted_property_readonly ( ) {
758+ let backend = create_test_backend ( ) ;
759+ let php = r#"<?php
760+ class User {
761+ public function __construct(
762+ public readonly string $name,
763+ private readonly int $id,
764+ ) {}
765+ }
766+ "# ;
767+
768+ let classes = backend. parse_php ( php) ;
769+ assert_eq ! ( classes. len( ) , 1 ) ;
770+
771+ let cls = & classes[ 0 ] ;
772+ assert_eq ! (
773+ cls. properties. len( ) ,
774+ 2 ,
775+ "readonly promoted params are still promoted"
776+ ) ;
777+
778+ let name = cls. properties . iter ( ) . find ( |p| p. name == "name" ) . unwrap ( ) ;
779+ assert_eq ! ( name. visibility, Visibility :: Public ) ;
780+ assert_eq ! ( name. type_hint. as_deref( ) , Some ( "string" ) ) ;
781+
782+ let id = cls. properties . iter ( ) . find ( |p| p. name == "id" ) . unwrap ( ) ;
783+ assert_eq ! ( id. visibility, Visibility :: Private ) ;
784+ assert_eq ! ( id. type_hint. as_deref( ) , Some ( "int" ) ) ;
785+ }
0 commit comments