Skip to content

Commit

Permalink
PHP 8.2: Non-final classes without #[AllowDynamicProperties] might …
Browse files Browse the repository at this point in the history
…still have dynamic properties

Closes phpstan/phpstan#8727
Closes phpstan/phpstan#8474
  • Loading branch information
ondrejmirtes committed Jan 18, 2023
1 parent 279c781 commit 051b06e
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 34 deletions.
4 changes: 4 additions & 0 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ public function hasProperty(string $propertyName): TrinaryLogic
return TrinaryLogic::createMaybe();
}

if (!$classReflection->isFinal()) {
return TrinaryLogic::createMaybe();
}

return TrinaryLogic::createNo();
}

Expand Down
38 changes: 25 additions & 13 deletions tests/PHPStan/Analyser/data/array-column-php82.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace ArrayColumn;
namespace ArrayColumn82;

use DOMElement;
use function PHPStan\Testing\assertType;
Expand Down Expand Up @@ -175,10 +175,10 @@ public function testImprecise5(array $array): void
assertType('list<string>', array_column($array, 'nodeName'));
assertType('array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('array<string, DOMElement>', 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<mixed>', array_column($array, 'foo'));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('array<int|string, string>', array_column($array, 'nodeName', 'foo'));
assertType('array<int|string, DOMElement>', array_column($array, null, 'foo'));
}

/** @param non-empty-array<int, DOMElement> $array */
Expand All @@ -187,10 +187,10 @@ public function testObjects1(array $array): void
assertType('non-empty-list<string>', array_column($array, 'nodeName'));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', 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<mixed>', array_column($array, 'foo'));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
}

/** @param array{DOMElement} $array */
Expand All @@ -199,10 +199,22 @@ public function testObjects2(array $array): void
assertType('array{string}', array_column($array, 'nodeName'));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('array{*NEVER*}', array_column($array, 'foo'));
assertType('non-empty-array<string, *NEVER*>', 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<mixed>', array_column($array, 'foo'));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
}

}

final class Foo
{

/** @param array<int, self> $a */
public function doFoo(array $a): void
{
assertType('array{}', array_column($a, 'nodeName'));
assertType('array{}', array_column($a, 'nodeName', 'tagName'));
}

}
12 changes: 12 additions & 0 deletions tests/PHPStan/Analyser/data/array-column.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,15 @@ public function testObjects2(array $array): void
}

}

final class Foo
{

/** @param array<int, self> $a */
public function doFoo(array $a): void
{
assertType('list<mixed>', array_column($a, 'nodeName'));
assertType('array<int|string, mixed>', array_column($a, 'nodeName', 'tagName'));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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'], []);
}

}
32 changes: 32 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-8474.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Bug8474;

class World {}
class HelloWorld extends World {
public string $hello = 'world';
}

function hello(World $world): bool {
return property_exists($world, 'hello');
}

class Alpha
{
public function __construct()
{
if (property_exists($this, 'data')) {
$this->data = 'Hello';
}
}
}

class Beta extends Alpha
{
/** @var string|null */
public $data = null;
}

class Delta extends Alpha
{
}
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-8727.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Bug8727;

abstract class Foo
{
abstract public function hello(): void;

protected function message(): string
{
if (property_exists($this, 'lala')) {
return 'Lala!';
}

return 'Hello!';
}
}

class Bar extends Foo {
protected bool $lala = true;

public function hello(): void
{
echo $this->message();
}
}
78 changes: 60 additions & 18 deletions tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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,
Expand All @@ -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 [
Expand All @@ -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)],
];
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/Properties/data/dynamic-properties.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
34 changes: 34 additions & 0 deletions tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

0 comments on commit 051b06e

Please sign in to comment.