@@ -602,51 +602,68 @@ private function resolveType(Expr $node): Type
602602 if (
603603 $ node instanceof Expr \BinaryOp \Equal
604604 || $ node instanceof Expr \BinaryOp \NotEqual
605- || $ node instanceof Expr \Empty_
606605 ) {
607606 return new BooleanType ();
608607 }
609608
610- if ($ node instanceof Expr \Isset_) {
611- $ result = new ConstantBooleanType (true );
612- foreach ($ node ->vars as $ var ) {
613- if ($ var instanceof Expr \ArrayDimFetch && $ var ->dim !== null ) {
614- $ variableType = $ this ->getType ($ var ->var );
615- $ dimType = $ this ->getType ($ var ->dim );
616- $ hasOffset = $ variableType ->hasOffsetValueType ($ dimType );
617- $ offsetValueType = $ variableType ->getOffsetValueType ($ dimType );
618- $ offsetValueIsNotNull = (new NullType ())->isSuperTypeOf ($ offsetValueType )->negate ();
619- $ isset = $ hasOffset ->and ($ offsetValueIsNotNull )->toBooleanType ();
620- if ($ isset instanceof ConstantBooleanType) {
621- if (!$ isset ->getValue ()) {
622- return $ isset ;
623- }
609+ if ($ node instanceof Expr \Empty_) {
610+ $ result = $ this ->issetCheck ($ node ->expr , static function (Type $ type ): ?bool {
611+ $ isNull = (new NullType ())->isSuperTypeOf ($ type );
612+ $ isFalsey = (new ConstantBooleanType (false ))->isSuperTypeOf ($ type ->toBoolean ());
613+ if ($ isNull ->maybe ()) {
614+ return null ;
615+ }
616+ if ($ isFalsey ->maybe ()) {
617+ return null ;
618+ }
624619
625- continue ;
620+ if ($ isNull ->yes ()) {
621+ if ($ isFalsey ->yes ()) {
622+ return false ;
623+ }
624+ if ($ isFalsey ->no ()) {
625+ return true ;
626626 }
627627
628- $ result = $ isset ;
629- continue ;
628+ return false ;
630629 }
631630
632- if ($ var instanceof Expr \Variable && is_string ($ var ->name )) {
633- $ variableType = $ this ->getType ($ var );
634- $ isNullSuperType = (new NullType ())->isSuperTypeOf ($ variableType );
635- $ has = $ this ->hasVariableType ($ var ->name );
636- if ($ has ->no () || $ isNullSuperType ->yes ()) {
637- return new ConstantBooleanType (false );
631+ return !$ isFalsey ->yes ();
632+ });
633+ if ($ result === null ) {
634+ return new BooleanType ();
635+ }
636+
637+ return new ConstantBooleanType (!$ result );
638+ }
639+
640+ if ($ node instanceof Expr \Isset_) {
641+ $ issetResult = true ;
642+ foreach ($ node ->vars as $ var ) {
643+ $ result = $ this ->issetCheck ($ var , static function (Type $ type ): ?bool {
644+ $ isNull = (new NullType ())->isSuperTypeOf ($ type );
645+ if ($ isNull ->maybe ()) {
646+ return null ;
638647 }
639648
640- if ($ has ->maybe () || !$ isNullSuperType ->no ()) {
641- $ result = new BooleanType ();
649+ return !$ isNull ->yes ();
650+ });
651+ if ($ result !== null ) {
652+ if (!$ result ) {
653+ return new ConstantBooleanType ($ result );
642654 }
655+
643656 continue ;
644657 }
645658
659+ $ issetResult = $ result ;
660+ }
661+
662+ if ($ issetResult === null ) {
646663 return new BooleanType ();
647664 }
648665
649- return $ result ;
666+ return new ConstantBooleanType ( $ issetResult ) ;
650667 }
651668
652669 if ($ node instanceof Node \Expr \BooleanNot) {
@@ -1928,53 +1945,32 @@ private function resolveType(Expr $node): Type
19281945 }
19291946
19301947 if ($ node instanceof Expr \BinaryOp \Coalesce) {
1931- if ($ node ->left instanceof Expr \ArrayDimFetch && $ node ->left ->dim !== null ) {
1932- $ dimType = $ this ->getType ($ node ->left ->dim );
1933- $ varType = $ this ->getType ($ node ->left ->var );
1934- $ hasOffset = $ varType ->hasOffsetValueType ($ dimType );
1935- $ leftType = $ this ->getType ($ node ->left );
1936- $ rightType = $ this ->filterByFalseyValue (
1937- new BinaryOp \NotIdentical ($ node ->left , new ConstFetch (new Name ('null ' ))),
1938- )->getType ($ node ->right );
1939- if ($ hasOffset ->no ()) {
1940- return $ rightType ;
1941- } elseif ($ hasOffset ->yes ()) {
1942- $ offsetValueType = $ varType ->getOffsetValueType ($ dimType );
1943- if ($ offsetValueType ->isSuperTypeOf (new NullType ())->no ()) {
1944- return TypeCombinator::removeNull ($ leftType );
1945- }
1946- }
1947-
1948- return TypeCombinator::union (
1949- TypeCombinator::removeNull ($ leftType ),
1950- $ rightType ,
1951- );
1952- }
1953-
19541948 $ leftType = $ this ->getType ($ node ->left );
19551949 $ rightType = $ this ->filterByFalseyValue (
19561950 new BinaryOp \NotIdentical ($ node ->left , new ConstFetch (new Name ('null ' ))),
19571951 )->getType ($ node ->right );
1958- if ($ leftType instanceof ErrorType || $ leftType instanceof NullType) {
1959- return $ rightType ;
1960- }
19611952
1962- if (
1963- TypeCombinator::containsNull ($ leftType )
1964- || $ node ->left instanceof PropertyFetch
1965- || (
1966- $ node ->left instanceof Variable
1967- && is_string ($ node ->left ->name )
1968- && !$ this ->hasVariableType ($ node ->left ->name )->yes ()
1969- )
1970- ) {
1953+ $ result = $ this ->issetCheck ($ node ->left , static function (Type $ type ): ?bool {
1954+ $ isNull = (new NullType ())->isSuperTypeOf ($ type );
1955+ if ($ isNull ->maybe ()) {
1956+ return null ;
1957+ }
1958+
1959+ return !$ isNull ->yes ();
1960+ });
1961+
1962+ if ($ result === null ) {
19711963 return TypeCombinator::union (
19721964 TypeCombinator::removeNull ($ leftType ),
19731965 $ rightType ,
19741966 );
19751967 }
19761968
1977- return TypeCombinator::removeNull ($ leftType );
1969+ if ($ result ) {
1970+ return TypeCombinator::removeNull ($ leftType );
1971+ }
1972+
1973+ return $ rightType ;
19781974 }
19791975
19801976 if ($ node instanceof ConstFetch) {
@@ -2590,6 +2586,177 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type
25902586 return $ type ;
25912587 }
25922588
2589+ /**
2590+ * @param callable(Type): ?bool $typeCallback
2591+ */
2592+ private function issetCheck (Expr $ expr , callable $ typeCallback , ?bool $ result = null ): ?bool
2593+ {
2594+ // mirrored in PHPStan\Rules\IssetCheck
2595+ if ($ expr instanceof Node \Expr \Variable && is_string ($ expr ->name )) {
2596+ $ hasVariable = $ this ->hasVariableType ($ expr ->name );
2597+ if ($ hasVariable ->maybe ()) {
2598+ return null ;
2599+ }
2600+
2601+ if ($ result === null ) {
2602+ if ($ hasVariable ->yes ()) {
2603+ if ($ expr ->name === '_SESSION ' ) {
2604+ return null ;
2605+ }
2606+
2607+ return $ typeCallback ($ this ->getVariableType ($ expr ->name ));
2608+ }
2609+
2610+ return false ;
2611+ }
2612+
2613+ return $ result ;
2614+ } elseif ($ expr instanceof Node \Expr \ArrayDimFetch && $ expr ->dim !== null ) {
2615+ $ type = $ this ->treatPhpDocTypesAsCertain
2616+ ? $ this ->getType ($ expr ->var )
2617+ : $ this ->getNativeType ($ expr ->var );
2618+ $ dimType = $ this ->treatPhpDocTypesAsCertain
2619+ ? $ this ->getType ($ expr ->dim )
2620+ : $ this ->getNativeType ($ expr ->dim );
2621+ $ hasOffsetValue = $ type ->hasOffsetValueType ($ dimType );
2622+ if (!$ type ->isOffsetAccessible ()->yes ()) {
2623+ return $ result ?? $ this ->issetCheckUndefined ($ expr ->var );
2624+ }
2625+
2626+ if ($ hasOffsetValue ->no ()) {
2627+ if ($ result !== null ) {
2628+ return $ result ;
2629+ }
2630+
2631+ return false ;
2632+ }
2633+
2634+ if ($ hasOffsetValue ->maybe ()) {
2635+ return null ;
2636+ }
2637+
2638+ // If offset is cannot be null, store this error message and see if one of the earlier offsets is.
2639+ // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null.
2640+ if ($ hasOffsetValue ->yes ()) {
2641+ if ($ result !== null ) {
2642+ return $ result ;
2643+ }
2644+
2645+ $ result = $ typeCallback ($ type ->getOffsetValueType ($ dimType ));
2646+
2647+ if ($ result !== null ) {
2648+ return $ this ->issetCheck ($ expr ->var , $ typeCallback , $ result );
2649+ }
2650+ }
2651+
2652+ // Has offset, it is nullable
2653+ return null ;
2654+
2655+ } elseif ($ expr instanceof Node \Expr \PropertyFetch || $ expr instanceof Node \Expr \StaticPropertyFetch) {
2656+
2657+ $ propertyReflection = $ this ->propertyReflectionFinder ->findPropertyReflectionFromNode ($ expr , $ this );
2658+
2659+ if ($ propertyReflection === null ) {
2660+ if ($ expr instanceof Node \Expr \PropertyFetch) {
2661+ return $ this ->issetCheckUndefined ($ expr ->var );
2662+ }
2663+
2664+ if ($ expr ->class instanceof Expr) {
2665+ return $ this ->issetCheckUndefined ($ expr ->class );
2666+ }
2667+
2668+ return null ;
2669+ }
2670+
2671+ if (!$ propertyReflection ->isNative ()) {
2672+ if ($ expr instanceof Node \Expr \PropertyFetch) {
2673+ return $ this ->issetCheckUndefined ($ expr ->var );
2674+ }
2675+
2676+ if ($ expr ->class instanceof Expr) {
2677+ return $ this ->issetCheckUndefined ($ expr ->class );
2678+ }
2679+
2680+ return null ;
2681+ }
2682+
2683+ $ nativeType = $ propertyReflection ->getNativeType ();
2684+ if (!$ nativeType instanceof MixedType) {
2685+ if (!$ this ->isSpecified ($ expr )) {
2686+ if ($ expr instanceof Node \Expr \PropertyFetch) {
2687+ return $ this ->issetCheckUndefined ($ expr ->var );
2688+ }
2689+
2690+ if ($ expr ->class instanceof Expr) {
2691+ return $ this ->issetCheckUndefined ($ expr ->class );
2692+ }
2693+
2694+ return null ;
2695+ }
2696+ }
2697+
2698+ if ($ result !== null ) {
2699+ return $ result ;
2700+ }
2701+
2702+ $ result = $ typeCallback ($ propertyReflection ->getWritableType ());
2703+ if ($ result !== null ) {
2704+ if ($ expr instanceof Node \Expr \PropertyFetch) {
2705+ return $ this ->issetCheck ($ expr ->var , $ typeCallback , $ result );
2706+ }
2707+
2708+ if ($ expr ->class instanceof Expr) {
2709+ return $ this ->issetCheck ($ expr ->class , $ typeCallback , $ result );
2710+ }
2711+ }
2712+
2713+ return $ result ;
2714+ }
2715+
2716+ if ($ result !== null ) {
2717+ return $ result ;
2718+ }
2719+
2720+ return $ typeCallback ($ this ->getType ($ expr ));
2721+ }
2722+
2723+ private function issetCheckUndefined (Expr $ expr ): ?bool
2724+ {
2725+ if ($ expr instanceof Node \Expr \Variable && is_string ($ expr ->name )) {
2726+ $ hasVariable = $ this ->hasVariableType ($ expr ->name );
2727+ if (!$ hasVariable ->no ()) {
2728+ return null ;
2729+ }
2730+
2731+ return false ;
2732+ }
2733+
2734+ if ($ expr instanceof Node \Expr \ArrayDimFetch && $ expr ->dim !== null ) {
2735+ $ type = $ this ->getType ($ expr ->var );
2736+ $ dimType = $ this ->getType ($ expr ->dim );
2737+ $ hasOffsetValue = $ type ->hasOffsetValueType ($ dimType );
2738+ if (!$ type ->isOffsetAccessible ()->yes ()) {
2739+ return $ this ->issetCheckUndefined ($ expr ->var );
2740+ }
2741+
2742+ if (!$ hasOffsetValue ->no ()) {
2743+ return $ this ->issetCheckUndefined ($ expr ->var );
2744+ }
2745+
2746+ return false ;
2747+ }
2748+
2749+ if ($ expr instanceof Expr \PropertyFetch) {
2750+ return $ this ->issetCheckUndefined ($ expr ->var );
2751+ }
2752+
2753+ if ($ expr instanceof Expr \StaticPropertyFetch && $ expr ->class instanceof Expr) {
2754+ return $ this ->issetCheckUndefined ($ expr ->class );
2755+ }
2756+
2757+ return null ;
2758+ }
2759+
25932760 /**
25942761 * @param ParametersAcceptor[] $variants
25952762 */
0 commit comments