5858use PHPStan \Type \FloatType ;
5959use PHPStan \Type \FunctionTypeSpecifyingExtension ;
6060use PHPStan \Type \Generic \GenericClassStringType ;
61+ use PHPStan \Type \Generic \GenericObjectType ;
6162use PHPStan \Type \Generic \TemplateType ;
6263use PHPStan \Type \Generic \TemplateTypeHelper ;
6364use PHPStan \Type \Generic \TemplateTypeVariance ;
8788use function array_merge ;
8889use function array_reverse ;
8990use function array_shift ;
91+ use function array_values ;
9092use function count ;
9193use function in_array ;
9294use function is_string ;
@@ -2665,6 +2667,74 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
26652667 return $ specifiedTypes ;
26662668 }
26672669
2670+ private function resolveClassStringComparison (
2671+ ClassConstFetch $ classExpr ,
2672+ ConstantStringType $ constType ,
2673+ Expr $ originalClassExpr ,
2674+ TypeSpecifierContext $ context ,
2675+ Scope $ scope ,
2676+ Expr $ rootExpr ,
2677+ ): SpecifiedTypes
2678+ {
2679+ if (!$ classExpr ->class instanceof Expr) {
2680+ throw new ShouldNotHappenException ();
2681+ }
2682+
2683+ $ className = $ constType ->getValue ();
2684+ if ($ className === '' ) {
2685+ throw new ShouldNotHappenException ();
2686+ }
2687+
2688+ if (!$ this ->reflectionProvider ->hasClass ($ className )) {
2689+ return $ this ->specifyTypesInCondition (
2690+ $ scope ,
2691+ new Instanceof_ (
2692+ $ classExpr ->class ,
2693+ new Name ($ className ),
2694+ ),
2695+ $ context ,
2696+ )->unionWith (
2697+ $ this ->create ($ originalClassExpr , $ constType , $ context , $ scope ),
2698+ )->setRootExpr ($ rootExpr );
2699+ }
2700+
2701+ $ classReflection = $ this ->reflectionProvider ->getClass ($ className );
2702+ $ narrowedType = new ObjectType ($ className , classReflection: $ classReflection ->asFinal ());
2703+
2704+ // Infer generic type parameters from the current type when narrowing to a child class.
2705+ // For union types (e.g. Cat<string>|Dog<string>), scope application via
2706+ // addTypeToExpression already preserves generics through TypeCombinator::intersect.
2707+ // This inference handles the parent-to-child case (e.g. Animal<string> → Cat<string>).
2708+ $ currentVarType = $ scope ->getType ($ classExpr ->class );
2709+ $ currentReflections = $ currentVarType ->getObjectClassReflections ();
2710+ $ childTemplateTypes = $ classReflection ->getTemplateTypeMap ()->getTypes ();
2711+ if (
2712+ count ($ childTemplateTypes ) > 0
2713+ && count ($ currentReflections ) === 1
2714+ && count ($ currentReflections [0 ]->getTemplateTypeMap ()->getTypes ()) > 0
2715+ ) {
2716+ $ freshChild = new GenericObjectType ($ className , array_values ($ childTemplateTypes ));
2717+ $ ancestor = $ freshChild ->getAncestorWithClassName ($ currentReflections [0 ]->getName ());
2718+ if ($ ancestor !== null ) {
2719+ $ inferredMap = $ ancestor ->inferTemplateTypes ($ currentVarType );
2720+ $ resolved = [];
2721+ foreach ($ childTemplateTypes as $ name => $ tType ) {
2722+ $ resolved [] = $ inferredMap ->getType ($ name ) ?? $ tType ;
2723+ }
2724+ $ narrowedType = new GenericObjectType ($ className , $ resolved );
2725+ }
2726+ }
2727+
2728+ return $ this ->create (
2729+ $ classExpr ->class ,
2730+ $ narrowedType ,
2731+ $ context ,
2732+ $ scope ,
2733+ )->unionWith (
2734+ $ this ->create ($ originalClassExpr , $ constType , $ context , $ scope ),
2735+ )->setRootExpr ($ rootExpr );
2736+ }
2737+
26682738 private function resolveNormalizedIdentical (Expr \BinaryOp \Identical $ expr , Scope $ scope , TypeSpecifierContext $ context ): SpecifiedTypes
26692739 {
26702740 $ leftExpr = $ expr ->left ;
@@ -2966,22 +3036,14 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29663036 $ rightType ->getValue () !== '' &&
29673037 strtolower ($ unwrappedLeftExpr ->name ->toString ()) === 'class '
29683038 ) {
2969- if ($ this ->reflectionProvider ->hasClass ($ rightType ->getValue ())) {
2970- return $ this ->create (
2971- $ unwrappedLeftExpr ->class ,
2972- new ObjectType ($ rightType ->getValue (), classReflection: $ this ->reflectionProvider ->getClass ($ rightType ->getValue ())->asFinal ()),
2973- $ context ,
2974- $ scope ,
2975- )->unionWith ($ this ->create ($ leftExpr , $ rightType , $ context , $ scope ))->setRootExpr ($ expr );
2976- }
2977- return $ this ->specifyTypesInCondition (
2978- $ scope ,
2979- new Instanceof_ (
2980- $ unwrappedLeftExpr ->class ,
2981- new Name ($ rightType ->getValue ()),
2982- ),
3039+ return $ this ->resolveClassStringComparison (
3040+ $ unwrappedLeftExpr ,
3041+ $ rightType ,
3042+ $ leftExpr ,
29833043 $ context ,
2984- )->unionWith ($ this ->create ($ leftExpr , $ rightType , $ context , $ scope ))->setRootExpr ($ expr );
3044+ $ scope ,
3045+ $ expr ,
3046+ );
29853047 }
29863048
29873049 $ leftType = $ scope ->getType ($ leftExpr );
@@ -2997,23 +3059,14 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29973059 $ leftType ->getValue () !== '' &&
29983060 strtolower ($ unwrappedRightExpr ->name ->toString ()) === 'class '
29993061 ) {
3000- if ($ this ->reflectionProvider ->hasClass ($ leftType ->getValue ())) {
3001- return $ this ->create (
3002- $ unwrappedRightExpr ->class ,
3003- new ObjectType ($ leftType ->getValue (), classReflection: $ this ->reflectionProvider ->getClass ($ leftType ->getValue ())->asFinal ()),
3004- $ context ,
3005- $ scope ,
3006- )->unionWith ($ this ->create ($ rightExpr , $ leftType , $ context , $ scope )->setRootExpr ($ expr ));
3007- }
3008-
3009- return $ this ->specifyTypesInCondition (
3010- $ scope ,
3011- new Instanceof_ (
3012- $ unwrappedRightExpr ->class ,
3013- new Name ($ leftType ->getValue ()),
3014- ),
3062+ return $ this ->resolveClassStringComparison (
3063+ $ unwrappedRightExpr ,
3064+ $ leftType ,
3065+ $ rightExpr ,
30153066 $ context ,
3016- )->unionWith ($ this ->create ($ rightExpr , $ leftType , $ context , $ scope )->setRootExpr ($ expr ));
3067+ $ scope ,
3068+ $ expr ,
3069+ );
30173070 }
30183071
30193072 if ($ context ->false ()) {
0 commit comments