Skip to content

Commit ef1b4bb

Browse files
committed
Set properties autowired with @required as initialized
1 parent 4f984e5 commit ef1b4bb

8 files changed

+255
-1
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"require": {
1616
"php": "^7.2 || ^8.0",
1717
"ext-simplexml": "*",
18-
"phpstan/phpstan": "^1.11"
18+
"phpstan/phpstan": "^1.11",
19+
"symfony/contracts": "^2.2"
1920
},
2021
"conflict": {
2122
"symfony/framework-bundle": "<3.0"

extension.neon

+7
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,10 @@ services:
329329
-
330330
factory: PHPStan\Type\Symfony\InputBagTypeSpecifyingExtension
331331
tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension]
332+
333+
# Additional constructors and initialization checks for @required autowiring
334+
-
335+
class: PHPStan\Symfony\RequiredAutowiringExtension
336+
tags:
337+
- phpstan.properties.readWriteExtension
338+
- phpstan.additionalConstructorsExtension

phpstan-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
parameters:
22
ignoreErrors:
3+
-
4+
message: "#^Although PHPStan\\\\Reflection\\\\Php\\\\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it's not guaranteed to always stay the same\\.$#"
5+
count: 1
6+
path: src/Symfony/RequiredAutowiringExtension.php
7+
38
-
49
message: "#^Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Console\\\\Input\\\\InputOption and 'isNegatable' will always evaluate to true\\.$#"
510
count: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
6+
use PHPStan\Reflection\AdditionalConstructorsExtension;
7+
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Reflection\Php\PhpPropertyReflection;
9+
use PHPStan\Reflection\PropertyReflection;
10+
use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
11+
use PHPStan\Type\FileTypeMapper;
12+
use Symfony\Contracts\Service\Attribute\Required;
13+
use function count;
14+
15+
class RequiredAutowiringExtension implements ReadWritePropertiesExtension, AdditionalConstructorsExtension
16+
{
17+
18+
/** @var FileTypeMapper */
19+
private $fileTypeMapper;
20+
21+
public function __construct(FileTypeMapper $fileTypeMapper)
22+
{
23+
$this->fileTypeMapper = $fileTypeMapper;
24+
}
25+
26+
public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool
27+
{
28+
return false;
29+
}
30+
31+
public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool
32+
{
33+
return false;
34+
}
35+
36+
public function isInitialized(PropertyReflection $property, string $propertyName): bool
37+
{
38+
// If the property is public, check for @required on the property itself
39+
if (!$property->isPublic()) {
40+
return false;
41+
}
42+
43+
if ($property->getDocComment() !== null && $this->isRequiredFromDocComment($property->getDocComment())) {
44+
return true;
45+
}
46+
47+
// Check for the attribute version
48+
if ($property instanceof PhpPropertyReflection && count($property->getNativeReflection()->getAttributes(Required::class)) > 0) {
49+
return true;
50+
}
51+
52+
return false;
53+
}
54+
55+
public function getAdditionalConstructors(ClassReflection $classReflection): array
56+
{
57+
$additionalConstructors = [];
58+
/** @var ReflectionClass $nativeReflection */
59+
$nativeReflection = $classReflection->getNativeReflection();
60+
61+
foreach ($nativeReflection->getMethods() as $method) {
62+
if (!$method->isPublic()) {
63+
continue;
64+
}
65+
66+
if ($method->getDocComment() !== false && $this->isRequiredFromDocComment($method->getDocComment())) {
67+
$additionalConstructors[] = $method->getName();
68+
}
69+
70+
if (count($method->getAttributes(Required::class)) === 0) {
71+
continue;
72+
}
73+
74+
$additionalConstructors[] = $method->getName();
75+
}
76+
77+
return $additionalConstructors;
78+
}
79+
80+
private function isRequiredFromDocComment(string $docComment): bool
81+
{
82+
$phpDoc = $this->fileTypeMapper->getResolvedPhpDoc(null, null, null, null, $docComment);
83+
84+
foreach ($phpDoc->getPhpDocNodes() as $node) {
85+
// @required tag is available, meaning this property is always initialized
86+
if (count($node->getTagsByName('@required')) > 0) {
87+
return true;
88+
}
89+
}
90+
91+
return false;
92+
}
93+
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Reflection\AdditionalConstructorsExtension;
6+
use PHPStan\Rules\Properties\UninitializedPropertyRule;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
use Symfony\Contracts\Service\Attribute\Required;
10+
use function class_exists;
11+
12+
/**
13+
* @extends RuleTestCase<UninitializedPropertyRule>
14+
*/
15+
final class RequiredAutowiringExtensionTest extends RuleTestCase
16+
{
17+
18+
protected function getRule(): Rule
19+
{
20+
$container = self::getContainer();
21+
$container->getServicesByTag(AdditionalConstructorsExtension::EXTENSION_TAG);
22+
23+
return $container->getByType(UninitializedPropertyRule::class);
24+
}
25+
26+
public function testRequiredAnnotations(): void
27+
{
28+
$this->analyse([__DIR__ . '/data/required-annotations.php'], [
29+
[
30+
'Class RequiredAnnotationTest\TestAnnotations has an uninitialized property $three. Give it default value or assign it in the constructor.',
31+
12,
32+
],
33+
[
34+
'Class RequiredAnnotationTest\TestAnnotations has an uninitialized property $four. Give it default value or assign it in the constructor.',
35+
14,
36+
],
37+
]);
38+
}
39+
40+
public function testRequiredAttributes(): void
41+
{
42+
if (!class_exists(Required::class)) {
43+
self::markTestSkipped('Required symfony/[email protected] or higher is not installed');
44+
}
45+
46+
$this->analyse([__DIR__ . '/data/required-attributes.php'], [
47+
[
48+
'Class RequiredAttributesTest\TestAttributes has an uninitialized property $three. Give it default value or assign it in the constructor.',
49+
14,
50+
],
51+
[
52+
'Class RequiredAttributesTest\TestAttributes has an uninitialized property $four. Give it default value or assign it in the constructor.',
53+
16,
54+
],
55+
]);
56+
}
57+
58+
public static function getAdditionalConfigFiles(): array
59+
{
60+
return [
61+
__DIR__ . '/required-autowiring-config.neon',
62+
];
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php // lint >= 7.4
2+
3+
namespace RequiredAnnotationTest;
4+
5+
class TestAnnotations
6+
{
7+
/** @required */
8+
public string $one;
9+
10+
private string $two;
11+
12+
public string $three;
13+
14+
private string $four;
15+
16+
/**
17+
* @required
18+
*/
19+
public function setTwo(int $two): void
20+
{
21+
$this->two = $two;
22+
}
23+
24+
public function getTwo(): int
25+
{
26+
return $this->two;
27+
}
28+
29+
public function setFour(int $four): void
30+
{
31+
$this->four = $four;
32+
}
33+
34+
public function getFour(): int
35+
{
36+
return $this->four;
37+
}
38+
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php // lint >= 8.0
2+
3+
namespace RequiredAttributesTest;
4+
5+
use Symfony\Contracts\Service\Attribute\Required;
6+
7+
class TestAttributes
8+
{
9+
#[Required]
10+
public string $one;
11+
12+
private string $two;
13+
14+
public string $three;
15+
16+
private string $four;
17+
18+
#[Required]
19+
public function setTwo(int $two): void
20+
{
21+
$this->two = $two;
22+
}
23+
24+
public function getTwo(): int
25+
{
26+
return $this->two;
27+
}
28+
29+
public function setFour(int $four): void
30+
{
31+
$this->four = $four;
32+
}
33+
34+
public function getFour(): int
35+
{
36+
return $this->four;
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
-
3+
class: PHPStan\Symfony\RequiredAutowiringExtension
4+
tags:
5+
- phpstan.properties.readWriteExtension
6+
- phpstan.additionalConstructorsExtension

0 commit comments

Comments
 (0)