diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a6d38c503c..fd1a720cb4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -411,12 +411,6 @@ parameters: count: 1 path: src/Reflection/ClassReflection.php - - - rawMessage: 'Method PHPStan\Reflection\ClassReflection::getCacheKey() should return string but returns string|null.' - identifier: return.type - count: 1 - path: src/Reflection/ClassReflection.php - - rawMessage: Binary operation "&" between bool|float|int|string|null and bool|float|int|string|null results in an error. identifier: binaryOp.invalid diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 0ee1be1651..bb361d5615 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -173,6 +173,7 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; @@ -6108,6 +6109,41 @@ private function processAssignVar( $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); } + foreach ([null, false, 0, 0.0, '', '0', []] as $falseyScalar) { + $falseyType = ConstantTypeHelper::getTypeFromValue($falseyScalar); + $withoutFalseyType = TypeCombinator::remove($type, $falseyType); + if ( + $withoutFalseyType->equals($type) + || $withoutFalseyType->equals($truthyType) + ) { + continue; + } + + if ($falseyScalar === null) { + $astNode = new ConstFetch(new Name('null')); + } elseif ($falseyScalar === false) { + $astNode = new ConstFetch(new Name('false')); + } elseif ($falseyScalar === 0) { + $astNode = new Node\Scalar\Int_($falseyScalar); + } elseif ($falseyScalar === 0.0) { + $astNode = new Node\Scalar\Float_($falseyScalar); + } elseif (in_array($falseyScalar, ['', '0'], true)) { + $astNode = new Node\Scalar\String_($falseyScalar); + } elseif ($falseyScalar === []) { + $astNode = new Node\Expr\Array_($falseyScalar); + } + + $notIdenticalConditionExpr = new Expr\BinaryOp\NotIdentical($assignedExpr, $astNode); + $notIdenticalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $notIdenticalConditionExpr, TypeSpecifierContext::createTrue()); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType); + + $identicalConditionExpr = new Expr\BinaryOp\Identical($assignedExpr, $astNode); + $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $identicalConditionExpr, TypeSpecifierContext::createTrue()); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); + } + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-10482.php b/tests/PHPStan/Analyser/nsrt/bug-10482.php new file mode 100644 index 0000000000..d9ef43dc15 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10482.php @@ -0,0 +1,24 @@ +id; +if (null !== $testId) { + assertType('Bug10482\Test', $test); +} + +if ($testId) { + assertType('Bug10482\Test', $test); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 465689b211..994039a6c1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -66,10 +66,10 @@ function mixedLast($mixed): void function firstInCondition(array $array) { if (($key = array_key_first($array)) !== null) { - assertType('list', $array); // could be 'non-empty-list' + assertType('non-empty-list', $array); return $array[$key]; } - assertType('list', $array); + assertType('array{}', $array); return null; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13709.php b/tests/PHPStan/Analyser/nsrt/bug-13709.php new file mode 100644 index 0000000000..0b87324414 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13709.php @@ -0,0 +1,17 @@ +', $key); - assertType('list', $array); // could be non-empty-list + assertType('non-empty-list', $array); assertType('string', $array[$key]); return $array[$key]; } @@ -60,7 +60,7 @@ function lastNotNull(array $array): mixed { if (($key = array_key_last($array)) !== null) { assertType('int<0, max>', $key); - assertType('list', $array); // could be non-empty-list + assertType('non-empty-list', $array); assertType('string', $array[$key]); return $array[$key]; } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index a641163fe1..73d4f028e0 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -840,7 +840,7 @@ public function testArrayDimFetchAfterArraySearch(): void $this->analyse([__DIR__ . '/data/array-dim-after-array-search.php'], [ [ - 'Offset int|string might not exist on array.', + 'Offset int|string might not exist on non-empty-array.', 20, ], ]); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index ac8e1ad170..b7e36dbd96 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3891,4 +3891,13 @@ public function testBug9820(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug6120(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-6120.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-6120.php b/tests/PHPStan/Rules/Methods/data/bug-6120.php new file mode 100644 index 0000000000..a498ae73b3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6120.php @@ -0,0 +1,20 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug6120; + +class Clazz +{ + + public int $foo = 0; + + public function bar(?Clazz $clazz): void + { + $result = $clazz?->foo; + if ($result !== null) { + $clazz->bar(null); + } + } + +}