4646use PHPStan \Type \UnionType ;
4747use function count ;
4848use function in_array ;
49+ use function is_string ;
4950
5051/**
5152 * New-world narrowing for `===` (and, via a negated context, `!==`): composed
@@ -239,6 +240,20 @@ private function specifyAgainstScalarLiteral(
239240 return null ;
240241 }
241242
243+ // a provenance-recorded subject variable narrows through its defining
244+ // call's families, exactly like the direct call comparison - the
245+ // variable itself still pins to the constant alongside
246+ $ provenanceTypes = $ this ->specifyThroughResultProvenance ($ subject , $ constantExpr , $ constantType , $ context , $ evaluationScope );
247+ if ($ provenanceTypes !== null ) {
248+ return $ provenanceTypes ->unionWith ($ this ->defaultNarrowingHelper ->createSubjectTypes (
249+ $ evaluationScope ,
250+ $ subject ,
251+ $ subjectResult ,
252+ $ constantType ,
253+ $ context ,
254+ ));
255+ }
256+
242257 // $a::class === Foo::class narrows $a to a final Foo when true;
243258 // other contexts and plain-string sides only pin the fetch
244259 if (
@@ -817,6 +832,40 @@ private function specifyEqualAgainstConstantSide(
817832 return false ;
818833 }
819834
835+ /**
836+ * When the subject variable is provenance-recorded as holding the result
837+ * of a pure call (`$v = count($x)`), the comparison against the constant
838+ * also runs the call's own family narrowing - `$v == 3` narrows $x like
839+ * `count($x) == 3` would. The recorded call comes from an earlier
840+ * statement and has no stored results in this walk, so the families read
841+ * the argument's current type from the evaluation scope's tracked state.
842+ */
843+ private function specifyThroughResultProvenance (
844+ Expr $ subject ,
845+ Expr $ constantExpr ,
846+ Type $ constantType ,
847+ TypeSpecifierContext $ context ,
848+ MutatingScope $ evaluationScope ,
849+ ): ?SpecifiedTypes
850+ {
851+ $ unwrappedSubject = $ subject instanceof AlwaysRememberedExpr ? $ subject ->getExpr () : $ subject ;
852+ if (!$ unwrappedSubject instanceof Expr \Variable || !is_string ($ unwrappedSubject ->name )) {
853+ return null ;
854+ }
855+
856+ $ call = $ evaluationScope ->getResultProvenanceCall ('$ ' . $ unwrappedSubject ->name );
857+ if ($ call === null ) {
858+ return null ;
859+ }
860+
861+ $ familyTypes = $ this ->specifyFuncCallFamilies ($ call , null , $ call , $ constantExpr , $ constantType , $ context , $ evaluationScope , null , true );
862+ if (!$ familyTypes instanceof SpecifiedTypes) {
863+ return null ;
864+ }
865+
866+ return $ familyTypes ;
867+ }
868+
820869 /**
821870 * The function-family compositions, shared by the literal and the
822871 * TYPE-based constant sides: a family answer, null to fall back to the
@@ -827,13 +876,14 @@ private function specifyEqualAgainstConstantSide(
827876 */
828877 private function specifyFuncCallFamilies (
829878 Expr $ subject ,
830- ExpressionResult $ subjectResult ,
879+ ? ExpressionResult $ subjectResult ,
831880 Expr \FuncCall $ call ,
832881 Expr $ constantExpr ,
833882 Type $ constantType ,
834883 TypeSpecifierContext $ context ,
835884 MutatingScope $ evaluationScope ,
836885 ?ExpressionResult $ argResult ,
886+ bool $ argTypesFromScopeState = false ,
837887 ): SpecifiedTypes |false |null
838888 {
839889 if (!($ call ->name instanceof Name) || $ call ->isFirstClassCallable () || !isset ($ call ->getArgs ()[0 ])) {
@@ -846,6 +896,10 @@ private function specifyFuncCallFamilies(
846896 $ call ->name ->toLowerString () === 'preg_match '
847897 ) {
848898 if ($ context ->true () && (new ConstantIntegerType (1 ))->isSuperTypeOf ($ constantType )->yes ()) {
899+ if ($ subjectResult === null ) {
900+ return null ;
901+ }
902+
849903 return $ subjectResult ->getSpecifiedTypesForScope ($ evaluationScope , $ context );
850904 }
851905
@@ -860,10 +914,11 @@ private function specifyFuncCallFamilies(
860914 $ constantStrings = $ constantType ->getConstantStrings ();
861915 if (count ($ constantStrings ) === 1 && $ constantStrings [0 ]->getValue () === '' ) {
862916 $ argExpr = $ call ->getArgs ()[0 ]->value ;
863- if ($ argResult === null ) {
917+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
918+ if ($ argType === null ) {
864919 return null ;
865920 }
866- if ($ argResult -> getTypeOnScope ( $ evaluationScope , $ evaluationScope -> nativeTypesPromoted ) ->isString ()->yes ()) {
921+ if ($ argType ->isString ()->yes ()) {
867922 return $ this ->defaultNarrowingHelper ->createForSubject (
868923 $ argExpr ,
869924 new IntersectionType ([new StringType (), new AccessoryNonEmptyStringType ()]),
@@ -883,10 +938,10 @@ private function specifyFuncCallFamilies(
883938 $ constantStrings = $ constantType ->getConstantStrings ();
884939 if (count ($ constantStrings ) === 1 && $ constantStrings [0 ]->getValue () !== '' ) {
885940 $ argExpr = $ call ->getArgs ()[0 ]->value ;
886- if ($ argResult === null ) {
941+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
942+ if ($ argType === null ) {
887943 return null ;
888944 }
889- $ argType = $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
890945 $ objectType = new ObjectType ($ constantStrings [0 ]->getValue ());
891946 $ classStringType = new GenericClassStringType ($ objectType );
892947
@@ -917,10 +972,10 @@ private function specifyFuncCallFamilies(
917972 ) {
918973 if ($ context ->truthy () && $ constantType ->isNonEmptyString ()->yes ()) {
919974 $ argExpr = $ call ->getArgs ()[0 ]->value ;
920- if ($ argResult === null ) {
975+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
976+ if ($ argType === null ) {
921977 return null ;
922978 }
923- $ argType = $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
924979
925980 if ($ argType ->isString ()->yes ()) {
926981 $ types = new SpecifiedTypes ();
@@ -962,10 +1017,10 @@ private function specifyFuncCallFamilies(
9621017 return $ this ->defaultNarrowingHelper ->createForSubject ($ argExpr , new NeverType (), $ context , $ evaluationScope );
9631018 }
9641019
965- if ($ argResult === null ) {
1020+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
1021+ if ($ argType === null ) {
9661022 return null ;
9671023 }
968- $ argType = $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
9691024
9701025 if ((new ConstantIntegerType (0 ))->isSuperTypeOf ($ constantType )->yes ()) {
9711026 $ newArgType = $ context ->truthy () && !$ argType ->isArray ()->yes ()
@@ -1023,10 +1078,11 @@ private function specifyFuncCallFamilies(
10231078 }
10241079
10251080 if ($ context ->truthy () && IntegerRangeType ::fromInterval (1 , null )->isSuperTypeOf ($ constantType )->yes ()) {
1026- if ($ argResult === null ) {
1081+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
1082+ if ($ argType === null ) {
10271083 return null ;
10281084 }
1029- if ($ argResult -> getTypeOnScope ( $ evaluationScope , $ evaluationScope -> nativeTypesPromoted ) ->isString ()->yes ()) {
1085+ if ($ argType ->isString ()->yes ()) {
10301086 $ accessory = IntegerRangeType ::fromInterval (2 , null )->isSuperTypeOf ($ constantType )->yes ()
10311087 ? new AccessoryNonFalsyStringType ()
10321088 : new AccessoryNonEmptyStringType ();
@@ -1103,6 +1159,25 @@ private function specifyFuncCallFamilies(
11031159 return false ;
11041160 }
11051161
1162+ /**
1163+ * The first argument's current type for the family compositions: from the
1164+ * captured operand result or - for a provenance-recorded call from an
1165+ * earlier statement, which has no captured results in this comparison -
1166+ * from the evaluation scope's tracked state. Null means unknown and the
1167+ * family falls back to the old-world path.
1168+ */
1169+ private function resolveFamilyArgType (Expr \FuncCall $ call , ?ExpressionResult $ argResult , bool $ argTypesFromScopeState , MutatingScope $ evaluationScope ): ?Type
1170+ {
1171+ if ($ argTypesFromScopeState ) {
1172+ return $ evaluationScope ->getStateType ($ call ->getArgs ()[0 ]->value );
1173+ }
1174+ if ($ argResult === null ) {
1175+ return null ;
1176+ }
1177+
1178+ return $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
1179+ }
1180+
11061181 /**
11071182 * The first argument's stored result of a (possibly remembered) call
11081183 * operand, captured by the seams at create time - the composed function
0 commit comments