diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 1c389f9fb9a..a6c2ba01744 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -47,6 +47,13 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type } $offsetAccessibleType = $scope->getType($expr->var); + if ($offsetAccessibleType instanceof NeverType) { + // never is a subtype of everything (including ArrayAccess), so without + // this short-circuit the fetch would be resolved through offsetGet() and + // produce an *ERROR* type instead of the expected never. + return $offsetAccessibleType; + } + if ( !$offsetAccessibleType->isArray()->yes() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes() diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 5599286bb8a..0ffd17c9ee8 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1947,7 +1947,10 @@ private function optimizeScalarType(Type $type): Type public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResult { if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new TypeResult(new ConstantBooleanType(false), []); + // A never-typed operand has no value to compare, so the result is + // undecided. This mirrors how never behaves as a boolean condition and + // keeps always-true/false rules from piling onto already-unreachable code. + return new TypeResult(new BooleanType(), []); } if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { diff --git a/tests/PHPStan/Analyser/data/bug-9307.php b/tests/PHPStan/Analyser/data/bug-9307.php index 69158aa7591..0b6ce3becc5 100644 --- a/tests/PHPStan/Analyser/data/bug-9307.php +++ b/tests/PHPStan/Analyser/data/bug-9307.php @@ -31,7 +31,7 @@ public function test(): void } } - assertType('array<*ERROR*>', $objects); // could be array + assertType('array', $objects); $this->acceptObjects($objects); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14281.php b/tests/PHPStan/Analyser/nsrt/bug-14281.php new file mode 100644 index 00000000000..4040b51a55a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14281.php @@ -0,0 +1,37 @@ + 'value'], + ]; + + assert($array[0] === null); + assertType("array{null, 0, 'some-string', stdClass, array{some: 'value'}}", $array); + + // $array[1] is 0, so this assertion can never hold and collapses the array + assert($array[1] === null); + assertType('*NEVER*', $array); + + // offset access on a never array must stay never instead of becoming *ERROR* + assertType('*NEVER*', $array[2]); + assert($array[2] === null); + assertType('*NEVER*', $array); +} + +function neverVariable(int $i): void +{ + if ($i !== $i) { + assertType('*NEVER*', $i); + assertType('bool', $i === null); + assertType('bool', $i !== null); + } +} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 5122f366f8a..d0123938388 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -686,10 +686,6 @@ public static function dataLastMatchArm(): iterable 36, 'Remove remaining cases below this one and this error will disappear too.', ], - [ - "Strict comparison using === between *NEVER* and 'ccc' will always evaluate to false.", - 38, - ], [ "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", 46, @@ -719,10 +715,6 @@ public static function dataLastMatchArm(): iterable "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", 36, ], - [ - "Strict comparison using === between *NEVER* and 'ccc' will always evaluate to false.", - 38, - ], [ "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", 46, @@ -1244,4 +1236,22 @@ public function testBug14791(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14791.php'], []); } + public function testBug14281(): void + { + $this->analyse([__DIR__ . '/data/bug-14281.php'], [ + [ + 'Strict comparison using === between null and null will always evaluate to true.', + 15, + ], + [ + 'Strict comparison using === between 0 and null will always evaluate to false.', + 16, + ], + [ + 'Strict comparison using !== between int and int will always evaluate to false.', + 25, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14281.php b/tests/PHPStan/Rules/Comparison/data/bug-14281.php new file mode 100644 index 00000000000..f45ca373f88 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14281.php @@ -0,0 +1,30 @@ + 'value'], + ]; + + assert($array[0] === null); + assert($array[1] === null); + // everything below is unreachable, the comparisons must not be reported + assert($array[2] === null); + assert($array[3] === null); + assert($array[4] === null); +} + +function neverOperand(int $i): void +{ + if ($i !== $i) { + // $i is never here + $a = ($i === null); + $b = ($i !== null); + } +}