diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index bf653fdcb0..7b16197b33 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -143,6 +143,10 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); } diff --git a/tests/PHPStan/Analyser/data/array-column-php82.php b/tests/PHPStan/Analyser/data/array-column-php82.php index f372a26acf..b700980a37 100644 --- a/tests/PHPStan/Analyser/data/array-column-php82.php +++ b/tests/PHPStan/Analyser/data/array-column-php82.php @@ -1,6 +1,6 @@ ', array_column($array, 'nodeName')); assertType('array', array_column($array, 'nodeName', 'tagName')); assertType('array', array_column($array, null, 'tagName')); - assertType('array{}', array_column($array, 'foo')); - assertType('array{}', array_column($array, 'foo', 'tagName')); - assertType('array<*NEVER*, string>', array_column($array, 'nodeName', 'foo')); - assertType('array<*NEVER*, DOMElement>', array_column($array, null, 'foo')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); } /** @param non-empty-array $array */ @@ -187,10 +187,10 @@ public function testObjects1(array $array): void assertType('non-empty-list', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array{}', array_column($array, 'foo')); - assertType('array{}', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array<*NEVER*, string>', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array<*NEVER*, DOMElement>', array_column($array, null, 'foo')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } /** @param array{DOMElement} $array */ @@ -199,10 +199,22 @@ public function testObjects2(array $array): void assertType('array{string}', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array{*NEVER*}', array_column($array, 'foo')); - assertType('non-empty-array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array<*NEVER*, string>', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array<*NEVER*, DOMElement>', array_column($array, null, 'foo')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + +} + +final class Foo +{ + + /** @param array $a */ + public function doFoo(array $a): void + { + assertType('array{}', array_column($a, 'nodeName')); + assertType('array{}', array_column($a, 'nodeName', 'tagName')); } } diff --git a/tests/PHPStan/Analyser/data/array-column.php b/tests/PHPStan/Analyser/data/array-column.php index 379e96e38e..2bf6c929ba 100644 --- a/tests/PHPStan/Analyser/data/array-column.php +++ b/tests/PHPStan/Analyser/data/array-column.php @@ -220,3 +220,15 @@ public function testObjects2(array $array): void } } + +final class Foo +{ + + /** @param array $a */ + public function doFoo(array $a): void + { + assertType('list', array_column($a, 'nodeName')); + assertType('array', array_column($a, 'nodeName', 'tagName')); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 1dc25136c2..e3deeee811 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -657,4 +657,18 @@ public function testDocblockAssertEquality(): void ]); } + public function testBug8727(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8727.php'], []); + } + + public function testBug8474(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8474.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8474.php b/tests/PHPStan/Rules/Comparison/data/bug-8474.php new file mode 100644 index 0000000000..5412e7a695 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8474.php @@ -0,0 +1,32 @@ +data = 'Hello'; + } + } +} + +class Beta extends Alpha +{ + /** @var string|null */ + public $data = null; +} + +class Delta extends Alpha +{ +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8727.php b/tests/PHPStan/Rules/Comparison/data/bug-8727.php new file mode 100644 index 0000000000..dad24f9c7d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8727.php @@ -0,0 +1,26 @@ +message(); + } +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index b232a9ad29..562dad8be9 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; use const PHP_VERSION_ID; /** @@ -564,6 +565,13 @@ public function testBug3659(): void public function dataDynamicProperties(): array { $errors = [ + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 23, + ], + ]; + + $errorsWithMore = array_merge([ [ 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', 9, @@ -588,24 +596,48 @@ public function dataDynamicProperties(): array 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', 16, ], + ], $errors); + + $errorsWithMore = array_merge($errorsWithMore, [ [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 23, + 26, ], - ]; + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 27, + ], + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 28, + ], + ]); - $errorsWithMore = $errors; - $errorsWithMore[] = [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 26, - ]; - $errorsWithMore[] = [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 27, - ]; - $errorsWithMore[] = [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 28, + $otherErrors = [ + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 36, + ], + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 37, + ], + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 38, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 41, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 42, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 43, + ], ]; return [ @@ -614,8 +646,8 @@ public function dataDynamicProperties(): array 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', 23, ], - ] : $errors], - [true, $errorsWithMore], + ] : array_merge($errors, $otherErrors)], + [true, array_merge($errorsWithMore, $otherErrors)], ]; } @@ -675,15 +707,25 @@ public function testPhp82AndDynamicProperties(bool $b): void 'Access to an undefined property Php82DynamicProperties\ClassA::$properties.', 34, ]; + if ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + ]; + } $errors[] = [ - 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', - 71, + 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', + 105, ]; } elseif ($b) { $errors[] = [ 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', 71, ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', + 105, + ]; } $this->checkThisOnly = false; $this->checkUnionTypes = true; diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index f1e8b7d99f..3b71c235fb 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -373,6 +373,10 @@ public function testAccessStaticPropertiesPhp82(): void 'Cannot access static property $anotherProperty on ClassOrString|false.', 150, ], + [ + 'Static access to instance property ClassOrString::$instanceProperty.', + 152, + ], [ 'Access to an undefined static property AccessInIsset::$foo.', 178, diff --git a/tests/PHPStan/Rules/Properties/data/dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php index 0a90b9ad5d..055029152e 100644 --- a/tests/PHPStan/Rules/Properties/data/dynamic-properties.php +++ b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php @@ -29,3 +29,17 @@ public function doBar() { } } +final class FinalBar {} + +final class FinalFoo { + public function doBar() { + isset($this->dynamicProperty); + empty($this->dynamicProperty); + $this->dynamicProperty ?? 'test'; + + $bar = new FinalBar(); + isset($bar->dynamicProperty); + empty($bar->dynamicProperty); + $bar->dynamicProperty ?? 'test'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php index dc31d5c3be..ddd7d965a6 100644 --- a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php +++ b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php @@ -73,3 +73,37 @@ function (): void { echo $hello->world; } }; + +final class FinalHelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new FinalHelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index b09e1e7b20..3064e7a0e6 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -8,6 +8,7 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use DynamicProperties\FinalFoo; use Exception; use InvalidArgumentException; use Iterator; @@ -2744,8 +2745,16 @@ public function dataIntersect(): iterable new ObjectType(\Test\Foo::class), new HasPropertyType('fooProperty'), ], + IntersectionType::class, + 'Test\Foo&hasProperty(fooProperty)', + ], + [ + [ + new ObjectType(FinalFoo::class), + new HasPropertyType('fooProperty'), + ], PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, - PHP_VERSION_ID < 80200 ? 'Test\Foo&hasProperty(fooProperty)' : '*NEVER*=implicit', + PHP_VERSION_ID < 80200 ? 'DynamicProperties\FinalFoo&hasProperty(fooProperty)' : '*NEVER*=implicit', ], [ [ @@ -2809,8 +2818,19 @@ public function dataIntersect(): iterable ]), new HasPropertyType('fooProperty'), ], - PHP_VERSION_ID < 80200 ? UnionType::class : NeverType::class, - PHP_VERSION_ID < 80200 ? '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))' : '*NEVER*=implicit', + UnionType::class, + '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', + ], + [ + [ + new UnionType([ + new ObjectType(FinalFoo::class), + new ObjectType(FirstInterface::class), + ]), + new HasPropertyType('fooProperty'), + ], + PHP_VERSION_ID < 80200 ? UnionType::class : IntersectionType::class, + PHP_VERSION_ID < 80200 ? '(DynamicProperties\FinalFoo&hasProperty(fooProperty))|(Test\FirstInterface&hasProperty(fooProperty))' : 'Test\FirstInterface&hasProperty(fooProperty)', ], [ [