diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 276d219dce..9af30f6191 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -575,10 +575,26 @@ public function specifyTypesInCondition( if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context, $rootExpr); $rightScope = $scope->filterByTruthyValue($expr->left); $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context, $rootExpr); - $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + + if ($context->falsey()) { + $leftIsAlwaysTrue = $scope->getType($expr->left)->isTrue()->yes(); + $rightIsAlwaysTrue = $scope->getType($expr->right)->isTrue()->yes(); + + if ($leftIsAlwaysTrue && !$rightIsAlwaysTrue) { + return $context->true() ? $rightTypes : $rightTypes->normalize($rightScope); + } + if ($rightIsAlwaysTrue && !$leftIsAlwaysTrue) { + return $context->true() ? $leftTypes : $leftTypes->normalize($scope); + } + } + + $types = $context->true() + ? $leftTypes->unionWith($rightTypes) + : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); if ($context->false()) { return new SpecifiedTypes( $types->getSureTypes(), @@ -599,10 +615,26 @@ public function specifyTypesInCondition( if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context, $rootExpr); $rightScope = $scope->filterByFalseyValue($expr->left); $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context, $rootExpr); - $types = $context->true() ? $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)) : $leftTypes->unionWith($rightTypes); + + if ($context->truthy()) { + $leftIsAlwaysFalse = $scope->getType($expr->left)->isFalse()->yes(); + $rightIsAlwaysFalse = $scope->getType($expr->right)->isFalse()->yes(); + + if ($leftIsAlwaysFalse && !$rightIsAlwaysFalse) { + return $context->true() ? $rightTypes->normalize($rightScope) : $rightTypes; + } + if ($rightIsAlwaysFalse && !$leftIsAlwaysFalse) { + return $context->true() ? $leftTypes->normalize($scope) : $leftTypes; + } + } + + $types = $context->true() + ? $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)) + : $leftTypes->unionWith($rightTypes); if ($context->true()) { return new SpecifiedTypes( $types->getSureTypes(), diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index c2b21e92c4..492bc16ef7 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -70,7 +70,9 @@ protected function setUp(): void $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)])); $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)]), new UnionType([new StringType(), new ConstantBooleanType(false)])); $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType())); + $this->scope = $this->scope->assignVariable('arrayOrNull', new UnionType([new ArrayType(new MixedType(), new MixedType()), new NullType()]), new UnionType([new ArrayType(new MixedType(), new MixedType()), new NullType()])); $this->scope = $this->scope->assignVariable('foo', new MixedType(), new MixedType()); + $this->scope = $this->scope->assignVariable('mixed', new MixedType(), new MixedType()); $this->scope = $this->scope->assignVariable('classString', new ClassStringType(), new ClassStringType()); $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar')), new GenericClassStringType(new ObjectType('Bar'))); $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType(), new ObjectWithoutClassType()); @@ -349,9 +351,17 @@ public function dataCondition(): iterable $this->createFunctionCall('is_int', 'foo'), $this->createFunctionCall('is_string', 'bar'), ), - [], + ['$foo' => 'int'], ['$foo' => '~int', '$bar' => '~string'], ], + [ + new Expr\BinaryOp\BooleanOr( + $this->createFunctionCall('is_int', 'foo'), + $this->createFunctionCall('is_string', 'mixed'), + ), + [], + ['$foo' => '~int', '$mixed' => '~string'], + ], [ new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\BooleanOr( @@ -648,19 +658,37 @@ public function dataCondition(): iterable [ new Expr\Empty_(new Variable('array')), [ - '$array' => 'array{}|null', + '$array' => 'array{}', ], [ '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], ], + [ + new Expr\Empty_(new Variable('arrayOrNull')), + [ + '$arrayOrNull' => 'array{}|null', + ], + [ + '$arrayOrNull' => '~0|0.0|\'\'|\'0\'|array{}|false|null', + ], + ], [ new BooleanNot(new Expr\Empty_(new Variable('array'))), [ '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ - '$array' => 'array{}|null', + '$array' => 'array{}', + ], + ], + [ + new BooleanNot(new Expr\Empty_(new Variable('arrayOrNull'))), + [ + '$arrayOrNull' => '~0|0.0|\'\'|\'0\'|array{}|false|null', + ], + [ + '$arrayOrNull' => 'array{}|null', ], ], [ diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index deacff9371..d4c70900b4 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -864,4 +864,10 @@ public function testBug10997(): void ]); } + public function testBug11276(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->analyse([__DIR__ . '/data/bug-11276.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11276.php b/tests/PHPStan/Rules/Arrays/data/bug-11276.php new file mode 100644 index 0000000000..7594f72659 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11276.php @@ -0,0 +1,40 @@ +[a-z]{2})-(?[a-z]{1}[a-z0-9]{1})\/#', $url, $matches); + + foreach ($expected as $key => $value) { + if ($matches instanceof ArrayAccess || \array_key_exists($key, $matches)) { + $matches[$key]; + } + } + + foreach ($expected as $key => $value) { + if (\array_key_exists($key, $matches) || $matches instanceof ArrayAccess) { + $matches[$key]; + } + } + + foreach ($expected as $key => $value) { + if (!$matches instanceof ArrayAccess && !\array_key_exists($key, $matches)) { + } else { + $matches[$key]; + } + } + + foreach ($expected as $key => $value) { + if (!\array_key_exists($key, $matches) && !$matches instanceof ArrayAccess) { + } else { + $matches[$key]; + } + } + } +}