diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 75000f1956..fda02a899c 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -750,6 +750,8 @@ public function specifyTypesInCondition( $this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope), $this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope), $this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope), + $this->processBooleanTruthyConditionalTypes($scope, $expr->left, $leftTypesForHolders, $rightTypesForHolders, $rightScope), + $this->processBooleanTruthyConditionalTypes($scope, $expr->right, $rightTypesForHolders, $leftTypesForHolders, $scope), ))->setRootExpr($expr); } @@ -1115,6 +1117,7 @@ public function specifyTypesInCondition( } } } + } return new SpecifiedTypes(); @@ -2211,6 +2214,81 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes return []; } + /** + * @return array + */ + private function processBooleanTruthyConditionalTypes(Scope $scope, Expr $leftExpr, SpecifiedTypes $leftFalseyTypes, SpecifiedTypes $rightFalseyTypes, Scope $rightScope): array + { + if ($leftFalseyTypes->getSureTypes() !== [] || $leftFalseyTypes->getSureNotTypes() !== []) { + return []; + } + + $leftTruthyTypes = $this->specifyTypesInCondition($scope, $leftExpr, TypeSpecifierContext::createTrue()); + + $conditionExpressionTypes = []; + foreach ($leftTruthyTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if (!$this->isTrackableExpression($expr)) { + continue; + } + + $scopeType = $scope->getType($expr); + $conditionType = TypeCombinator::remove($scopeType, $type); + if ($scopeType->equals($conditionType)) { + continue; + } + + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + $conditionType, + ); + } + + foreach ($leftTruthyTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$this->isTrackableExpression($expr)) { + continue; + } + + if (isset($conditionExpressionTypes[$exprString])) { + continue; + } + + $scopeType = $scope->getType($expr); + $conditionType = TypeCombinator::intersect($scopeType, $type); + if ($scopeType->equals($conditionType)) { + continue; + } + + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + $conditionType, + ); + } + + if ($conditionExpressionTypes === []) { + return []; + } + + $holders = []; + foreach ($rightFalseyTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$this->isTrackableExpression($expr)) { + continue; + } + + if (!isset($holders[$exprString])) { + $holders[$exprString] = []; + } + + $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope; + $holder = new ConditionalExpressionHolder( + $conditionExpressionTypes, + ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($targetScope->getType($expr), $type)), + ); + $holders[$exprString][$holder->getKey()] = $holder; + } + + return $holders; + } + private function isTrackableExpression(Expr $expr): bool { if ($expr instanceof Expr\Variable) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-6202.php b/tests/PHPStan/Analyser/nsrt/bug-6202.php new file mode 100644 index 0000000000..e8bb545856 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6202.php @@ -0,0 +1,94 @@ + $array + */ + public function sayHello(array $array): void + { + if (isset($array['mightExist']) && !is_string($array['mightExist'])) { + throw new Exception('Has to be string if set'); + } + + assertType('string', $array['mightExist'] ?? ''); + + if (!isset($array['hasToExist']) || !is_string($array['hasToExist'])) { + throw new Exception('Has to exist as string'); + } + assertType('string', $array['hasToExist']); + } + + /** + * @param array $array + */ + public function otherIsTypeFunctions(array $array): void + { + if (isset($array['intKey']) && !is_int($array['intKey'])) { + throw new Exception(); + } + assertType('int', $array['intKey'] ?? 0); + + if (isset($array['arrayKey']) && !is_array($array['arrayKey'])) { + throw new Exception(); + } + assertType('array', $array['arrayKey'] ?? []); + + if (isset($array['boolKey']) && !is_bool($array['boolKey'])) { + throw new Exception(); + } + assertType('bool', $array['boolKey'] ?? false); + } + + /** + * @param array $array + */ + public function orPattern(array $array): void + { + if (!isset($array['key']) || !is_string($array['key'])) { + throw new Exception(); + } + assertType('string', $array['key']); + } + + /** + * @param array $array + */ + public function instanceofCheck(array $array): void + { + if (isset($array['obj']) && !$array['obj'] instanceof \stdClass) { + throw new Exception(); + } + assertType('stdClass', $array['obj'] ?? new \stdClass()); + } + + /** + * @param array $array + */ + public function nestedArrayDimFetch(array $array): void + { + if (isset($array['nested']['key']) && !is_string($array['nested']['key'])) { + throw new Exception(); + } + assertType('string', $array['nested']['key'] ?? ''); + } + + /** + * @param array $array + */ + public function directAccessAfterGuard(array $array): void + { + if (isset($array['mightExist']) && !is_string($array['mightExist'])) { + throw new Exception(); + } + + if (isset($array['mightExist'])) { + assertType('string', $array['mightExist']); + } + } +}