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 (
@@ -816,6 +831,40 @@ private function specifyEqualAgainstConstantSide(
816831 return false ;
817832 }
818833
834+ /**
835+ * When the subject variable is provenance-recorded as holding the result
836+ * of a pure call (`$v = count($x)`), the comparison against the constant
837+ * also runs the call's own family narrowing - `$v == 3` narrows $x like
838+ * `count($x) == 3` would. The recorded call comes from an earlier
839+ * statement and has no stored results in this walk, so the families read
840+ * the argument's current type from the evaluation scope's tracked state.
841+ */
842+ private function specifyThroughResultProvenance (
843+ Expr $ subject ,
844+ Expr $ constantExpr ,
845+ Type $ constantType ,
846+ TypeSpecifierContext $ context ,
847+ MutatingScope $ evaluationScope ,
848+ ): ?SpecifiedTypes
849+ {
850+ $ unwrappedSubject = $ subject instanceof AlwaysRememberedExpr ? $ subject ->getExpr () : $ subject ;
851+ if (!$ unwrappedSubject instanceof Expr \Variable || !is_string ($ unwrappedSubject ->name )) {
852+ return null ;
853+ }
854+
855+ $ call = $ evaluationScope ->getResultProvenanceCall ('$ ' . $ unwrappedSubject ->name );
856+ if ($ call === null ) {
857+ return null ;
858+ }
859+
860+ $ familyTypes = $ this ->specifyFuncCallFamilies ($ call , null , $ call , $ constantExpr , $ constantType , $ context , $ evaluationScope , null , true );
861+ if (!$ familyTypes instanceof SpecifiedTypes) {
862+ return null ;
863+ }
864+
865+ return $ familyTypes ;
866+ }
867+
819868 /**
820869 * The function-family compositions, shared by the literal and the
821870 * TYPE-based constant sides: a family answer, null to fall back to the
@@ -826,13 +875,14 @@ private function specifyEqualAgainstConstantSide(
826875 */
827876 private function specifyFuncCallFamilies (
828877 Expr $ subject ,
829- ExpressionResult $ subjectResult ,
878+ ? ExpressionResult $ subjectResult ,
830879 Expr \FuncCall $ call ,
831880 Expr $ constantExpr ,
832881 Type $ constantType ,
833882 TypeSpecifierContext $ context ,
834883 MutatingScope $ evaluationScope ,
835884 ?ExpressionResult $ argResult ,
885+ bool $ argTypesFromScopeState = false ,
836886 ): SpecifiedTypes |false |null
837887 {
838888 if (!($ call ->name instanceof Name) || $ call ->isFirstClassCallable () || !isset ($ call ->getArgs ()[0 ])) {
@@ -845,6 +895,10 @@ private function specifyFuncCallFamilies(
845895 $ call ->name ->toLowerString () === 'preg_match '
846896 ) {
847897 if ($ context ->true () && (new ConstantIntegerType (1 ))->isSuperTypeOf ($ constantType )->yes ()) {
898+ if ($ subjectResult === null ) {
899+ return null ;
900+ }
901+
848902 return $ subjectResult ->getSpecifiedTypesForScope ($ evaluationScope , $ context );
849903 }
850904
@@ -859,10 +913,11 @@ private function specifyFuncCallFamilies(
859913 $ constantStrings = $ constantType ->getConstantStrings ();
860914 if (count ($ constantStrings ) === 1 && $ constantStrings [0 ]->getValue () === '' ) {
861915 $ argExpr = $ call ->getArgs ()[0 ]->value ;
862- if ($ argResult === null ) {
916+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
917+ if ($ argType === null ) {
863918 return null ;
864919 }
865- if ($ argResult -> getTypeOnScope ( $ evaluationScope , $ evaluationScope -> nativeTypesPromoted ) ->isString ()->yes ()) {
920+ if ($ argType ->isString ()->yes ()) {
866921 return $ this ->defaultNarrowingHelper ->createForSubject (
867922 $ argExpr ,
868923 new IntersectionType ([new StringType (), new AccessoryNonEmptyStringType ()]),
@@ -882,10 +937,10 @@ private function specifyFuncCallFamilies(
882937 $ constantStrings = $ constantType ->getConstantStrings ();
883938 if (count ($ constantStrings ) === 1 && $ constantStrings [0 ]->getValue () !== '' ) {
884939 $ argExpr = $ call ->getArgs ()[0 ]->value ;
885- if ($ argResult === null ) {
940+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
941+ if ($ argType === null ) {
886942 return null ;
887943 }
888- $ argType = $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
889944 $ objectType = new ObjectType ($ constantStrings [0 ]->getValue ());
890945 $ classStringType = new GenericClassStringType ($ objectType );
891946
@@ -916,10 +971,10 @@ private function specifyFuncCallFamilies(
916971 ) {
917972 if ($ context ->truthy () && $ constantType ->isNonEmptyString ()->yes ()) {
918973 $ argExpr = $ call ->getArgs ()[0 ]->value ;
919- if ($ argResult === null ) {
974+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
975+ if ($ argType === null ) {
920976 return null ;
921977 }
922- $ argType = $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
923978
924979 if ($ argType ->isString ()->yes ()) {
925980 $ types = new SpecifiedTypes ();
@@ -961,10 +1016,10 @@ private function specifyFuncCallFamilies(
9611016 return $ this ->defaultNarrowingHelper ->createForSubject ($ argExpr , new NeverType (), $ context , $ evaluationScope );
9621017 }
9631018
964- if ($ argResult === null ) {
1019+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
1020+ if ($ argType === null ) {
9651021 return null ;
9661022 }
967- $ argType = $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
9681023
9691024 if ((new ConstantIntegerType (0 ))->isSuperTypeOf ($ constantType )->yes ()) {
9701025 $ newArgType = $ context ->truthy () && !$ argType ->isArray ()->yes ()
@@ -1022,10 +1077,11 @@ private function specifyFuncCallFamilies(
10221077 }
10231078
10241079 if ($ context ->truthy () && IntegerRangeType ::fromInterval (1 , null )->isSuperTypeOf ($ constantType )->yes ()) {
1025- if ($ argResult === null ) {
1080+ $ argType = $ this ->resolveFamilyArgType ($ call , $ argResult , $ argTypesFromScopeState , $ evaluationScope );
1081+ if ($ argType === null ) {
10261082 return null ;
10271083 }
1028- if ($ argResult -> getTypeOnScope ( $ evaluationScope , $ evaluationScope -> nativeTypesPromoted ) ->isString ()->yes ()) {
1084+ if ($ argType ->isString ()->yes ()) {
10291085 $ accessory = IntegerRangeType ::fromInterval (2 , null )->isSuperTypeOf ($ constantType )->yes ()
10301086 ? new AccessoryNonFalsyStringType ()
10311087 : new AccessoryNonEmptyStringType ();
@@ -1102,6 +1158,25 @@ private function specifyFuncCallFamilies(
11021158 return false ;
11031159 }
11041160
1161+ /**
1162+ * The first argument's current type for the family compositions: from the
1163+ * captured operand result or - for a provenance-recorded call from an
1164+ * earlier statement, which has no captured results in this comparison -
1165+ * from the evaluation scope's tracked state. Null means unknown and the
1166+ * family falls back to the old-world path.
1167+ */
1168+ private function resolveFamilyArgType (Expr \FuncCall $ call , ?ExpressionResult $ argResult , bool $ argTypesFromScopeState , MutatingScope $ evaluationScope ): ?Type
1169+ {
1170+ if ($ argTypesFromScopeState ) {
1171+ return $ evaluationScope ->getStateType ($ call ->getArgs ()[0 ]->value );
1172+ }
1173+ if ($ argResult === null ) {
1174+ return null ;
1175+ }
1176+
1177+ return $ argResult ->getTypeOnScope ($ evaluationScope , $ evaluationScope ->nativeTypesPromoted );
1178+ }
1179+
11051180 /**
11061181 * The first argument's stored result of a (possibly remembered) call
11071182 * operand, captured by the seams at create time - the composed function
0 commit comments