diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index c094bbd0c4..8f786167e6 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -259,63 +259,76 @@ public function specifyTypesInCondition( ); } - if ($context->true()) { - $type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left)); - $leftTypes = $this->create($expr->left, $type, $context, false, $scope); - $rightTypes = $this->create($expr->right, $type, $context, false, $scope); + $exprLeftType = $scope->getType($expr->left); + $exprRightType = $scope->getType($expr->right); + + $identicalType = $scope->getType($expr); + if ($identicalType instanceof ConstantBooleanType && !$context->null()) { + $never = new NeverType(); + $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; + $leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope); + $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope); return $leftTypes->unionWith($rightTypes); + } - } elseif ($context->false()) { - $identicalType = $scope->getType($expr); - if ($identicalType instanceof ConstantBooleanType) { - $never = new NeverType(); - $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; - $leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope); - $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope); - return $leftTypes->unionWith($rightTypes); - } + $types = null; - $exprLeftType = $scope->getType($expr->left); - $exprRightType = $scope->getType($expr->right); + if ( + ( + $exprLeftType instanceof ConstantType + && !$expr->right instanceof Node\Scalar + ) || $exprLeftType instanceof EnumCaseObjectType + ) { + $types = $this->create( + $expr->right, + $exprLeftType, + $context, + false, + $scope, + ); + } + if ( + ( + $exprRightType instanceof ConstantType + && !$expr->left instanceof Node\Scalar + ) || $exprRightType instanceof EnumCaseObjectType + ) { + $leftType = $this->create( + $expr->left, + $exprRightType, + $context, + false, + $scope, + ); + if ($types !== null) { + $types = $types->unionWith($leftType); + } else { + $types = $leftType; + } + } - $types = null; + if ($types !== null) { + return $types; + } - if ( - ( - $exprLeftType instanceof ConstantType - && !$expr->right instanceof Node\Scalar - ) || $exprLeftType instanceof EnumCaseObjectType - ) { - $types = $this->create( - $expr->right, - $exprLeftType, - $context, - false, - $scope, - ); + $furtherSpecificationPossible = static function (Expr $expr) use (&$furtherSpecificationPossible): bool { + if ($expr instanceof Expr\Variable) { + return true; } - if ( - ( - $exprRightType instanceof ConstantType - && !$expr->left instanceof Node\Scalar - ) || $exprRightType instanceof EnumCaseObjectType - ) { - $leftType = $this->create( - $expr->left, - $exprRightType, - $context, - false, - $scope, - ); - if ($types !== null) { - $types = $types->unionWith($leftType); - } else { - $types = $leftType; - } + + if ($expr instanceof ArrayDimFetch) { + return $furtherSpecificationPossible($expr->var); } - if ($types !== null) { - return $types; + return false; + }; + + if ($furtherSpecificationPossible($expr->left) || $furtherSpecificationPossible($expr->right)) { + if ($context->true()) { + $type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left)); + $leftTypes = $this->create($expr->left, $type, $context, false, $scope); + $rightTypes = $this->create($expr->right, $type, $context, false, $scope); + return $leftTypes->unionWith($rightTypes); } return $this->create($expr->left, $exprLeftType, $context, false, $scope)->normalize($scope) diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 9ea86aedaf..43af6df355 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -554,7 +554,6 @@ public function dataCondition(): array ), [ '$foo' => '123', - 123 => '123', ], ['$foo' => '~123'], ], diff --git a/tests/PHPStan/Analyser/data/equal.php b/tests/PHPStan/Analyser/data/equal.php index 113d2868b2..6fb7929cc1 100644 --- a/tests/PHPStan/Analyser/data/equal.php +++ b/tests/PHPStan/Analyser/data/equal.php @@ -99,4 +99,14 @@ public function stdClass(\stdClass $a, \stdClass $b): void assertType('stdClass', $b); } + /** + * @param array{a: string, b: array{c: string|null}} $a + */ + public function arrayOffset(array $a): void + { + if (strlen($a['a']) > 0 && $a['a'] === $a['b']['c']) { + assertType('array{a: non-empty-string, b: array{c: non-empty-string}}', $a); + } + } + } diff --git a/tests/PHPStan/Analyser/data/identical.php b/tests/PHPStan/Analyser/data/identical.php index 26602e2918..f46c675214 100644 --- a/tests/PHPStan/Analyser/data/identical.php +++ b/tests/PHPStan/Analyser/data/identical.php @@ -41,4 +41,14 @@ public function foo(\stdClass $a, \stdClass $b): void assertType('stdClass', $b); } + /** + * @param array{a: string, b: array{c: string|null}} $a + */ + public function arrayOffset(array $a): void + { + if (strlen($a['a']) > 0 && $a['a'] === $a['b']['c']) { + assertType('array{a: non-empty-string, b: array{c: non-empty-string}}', $a); + } + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index ccaacd8b11..5621266e40 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -163,6 +163,10 @@ public function testRule(): void 'Cannot access offset \'foo\' on array|int.', 443, ], + [ + 'Offset \'feature_pretty…\' does not exist on array{version: non-empty-string, commit: string|null, pretty_version: string|null, feature_version: non-empty-string, feature_pretty_version?: string|null}.', + 504, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php index d74ee08af5..065d636722 100644 --- a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php @@ -485,3 +485,26 @@ function test($array): void { } } + +/** + * @phpstan-type Version array{version: string, commit: string|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null} + */ +class VersionGuesser +{ + /** + * @param array $versionData + * + * @phpstan-param Version $versionData + * + * @return array + * @phpstan-return Version + */ + private function postprocess(array $versionData): array + { + if (!empty($versionData['feature_version']) && $versionData['feature_version'] === $versionData['version'] && $versionData['feature_pretty_version'] === $versionData['pretty_version']) { + unset($versionData['feature_version'], $versionData['feature_pretty_version']); + } + + return $versionData; + } +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 31d32db056..86128a9bd5 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -80,6 +80,54 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isSame() with *NEVER* and stdClass will always evaluate to false.', 84, ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', + 101, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', + 104, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', + 113, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{} and array{} will always evaluate to false.', + 116, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{1, 3} and array{1, 3} will always evaluate to true.', + 119, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{1, 3} and array{1, 3} will always evaluate to false.', + 122, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and stdClass will always evaluate to false.', + 126, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and stdClass will always evaluate to true.', + 130, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'1\' and stdClass will always evaluate to false.', + 133, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'1\' and stdClass will always evaluate to true.', + 136, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to false.', + 139, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to true.', + 142, + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php index c213bfb8d5..14564e2912 100644 --- a/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php @@ -92,6 +92,56 @@ public function doDolor(\stdClass $std1, \stdClass $std2) } } + if ($this->isSame(self::createStdClass('a'), self::createStdClass('a'))) { + + } + if ($this->isNotSame(self::createStdClass('b'), self::createStdClass('b'))) { + + } + if ($this->isSame(self::returnFoo('a'), self::returnFoo('a'))) { + + } + if ($this->isNotSame(self::returnFoo('b'), self::returnFoo('b'))) { + + } + if ($this->isSame(self::createStdClass('a')->foo, self::createStdClass('a')->foo)) { + + } + if ($this->isNotSame(self::createStdClass('b')->foo, self::createStdClass('b')->foo)) { + + } + if ($this->isSame([], [])) { + + } + if ($this->isNotSame([], [])) { + + } + if ($this->isSame([1, 3], [1, 3])) { + + } + if ($this->isNotSame([1, 3], [1, 3])) { + + } + $std3 = new \stdClass(); + if ($this->isSame(1, $std3)) { + + } + $std4 = new \stdClass(); + if ($this->isNotSame(1, $std4)) { + + } + if ($this->isSame('1', new \stdClass())) { + + } + if ($this->isNotSame('1', new \stdClass())) { + + } + if ($this->isSame(['a', 'b'], [1, 2])) { + + } + if ($this->isNotSame(['a', 'b'], [1, 2])) { + + } } public function nullableInt(): ?int @@ -99,4 +149,17 @@ public function nullableInt(): ?int } + public static function createStdClass(string $foo): \stdClass + { + return new \stdClass(); + } + + /** + * @return 'foo' + */ + public static function returnFoo(string $foo): string + { + return 'foo'; + } + }