Skip to content

Commit d9b295f

Browse files
committed
Set properties autowired with @required as initialized
Fixes #346
1 parent db75c81 commit d9b295f

6 files changed

+219
-1
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"require": {
1616
"php": "^7.2 || ^8.0",
1717
"ext-simplexml": "*",
18-
"phpstan/phpstan": "^1.9.18"
18+
"phpstan/phpstan": "^1.10.14"
1919
},
2020
"conflict": {
2121
"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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Reflection\AdditionalConstructorsExtension;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\Php\PhpPropertyReflection;
8+
use PHPStan\Reflection\PropertyReflection;
9+
use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
10+
use PHPStan\Type\FileTypeMapper;
11+
use Symfony\Contracts\Service\Attribute\Required;
12+
use function count;
13+
14+
class RequiredAutowiringExtension implements ReadWritePropertiesExtension, AdditionalConstructorsExtension
15+
{
16+
17+
/** @var FileTypeMapper */
18+
private $fileTypeMapper;
19+
20+
public function __construct(FileTypeMapper $fileTypeMapper)
21+
{
22+
$this->fileTypeMapper = $fileTypeMapper;
23+
}
24+
25+
public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool
26+
{
27+
return false;
28+
}
29+
30+
public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool
31+
{
32+
return false;
33+
}
34+
35+
public function isInitialized(PropertyReflection $property, string $propertyName): bool
36+
{
37+
// If the property is public, check for @required on the property itself
38+
if (!$property->isPublic()) {
39+
return false;
40+
}
41+
42+
if ($property->getDocComment() !== null && $this->isRequiredFromDocComment($property->getDocComment())) {
43+
return true;
44+
}
45+
46+
// Check for the attribute version
47+
if ($property instanceof PhpPropertyReflection && count($property->getNativeReflection()->getAttributes(Required::class)) > 0) {
48+
return true;
49+
}
50+
51+
return false;
52+
}
53+
54+
public function getAdditionalConstructors(ClassReflection $classReflection): array
55+
{
56+
$additionalConstructors = [];
57+
58+
foreach ($classReflection->getNativeReflection()->getMethods() as $method) {
59+
if (!$method->isPublic()) {
60+
continue;
61+
}
62+
63+
if ($method->getDocComment() !== false && $this->isRequiredFromDocComment($method->getDocComment())) {
64+
$additionalConstructors[] = $method->getName();
65+
}
66+
67+
// Check for the attribute version
68+
if (count($method->getAttributes(Required::class)) === 0) {
69+
continue;
70+
}
71+
72+
$additionalConstructors[] = $method->getName();
73+
}
74+
75+
return $additionalConstructors;
76+
}
77+
78+
private function isRequiredFromDocComment(string $docComment): bool
79+
{
80+
$phpDoc = $this->fileTypeMapper->getResolvedPhpDoc(null, null, null, null, $docComment);
81+
82+
foreach ($phpDoc->getPhpDocNodes() as $node) {
83+
// @required tag is available, meaning this property is always initialized
84+
if (count($node->getTagsByName('@required')) > 0) {
85+
return true;
86+
}
87+
}
88+
89+
return false;
90+
}
91+
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Reflection\AdditionalConstructorsExtension;
6+
use PHPStan\Reflection\ConstructorsHelper;
7+
use PHPStan\Rules\Properties\UninitializedPropertyRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
final class RequiredAutowiringExtensionTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
$container = self::getContainer();
17+
$container->getServicesByTag(AdditionalConstructorsExtension::EXTENSION_TAG);
18+
19+
return new UninitializedPropertyRule(
20+
new ConstructorsHelper(
21+
$container,
22+
[]
23+
)
24+
);
25+
}
26+
27+
public function testRequiredTags(): void
28+
{
29+
$this->analyse([__DIR__ . '/data/required-tags.php'], [
30+
[
31+
'Class RequiredTagTest\TestAnnotations has an uninitialized property $three. Give it default value or assign it in the constructor.',
32+
18,
33+
],
34+
[
35+
'Class RequiredTagTest\TestAnnotations has an uninitialized property $four. Give it default value or assign it in the constructor.',
36+
20,
37+
],
38+
[
39+
'Class RequiredTagTest\TestAttributes has an uninitialized property $three. Give it default value or assign it in the constructor.',
40+
43,
41+
],
42+
[
43+
'Class RequiredTagTest\TestAttributes has an uninitialized property $four. Give it default value or assign it in the constructor.',
44+
45,
45+
],
46+
]);
47+
}
48+
49+
public static function getAdditionalConfigFiles(): array
50+
{
51+
return [
52+
__DIR__ . '/required-autowiring-config.neon',
53+
];
54+
}
55+
56+
}

tests/Symfony/data/required-tags.php

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace RequiredTagTest;
4+
5+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
use Symfony\Component\Routing\Annotation\Route;
9+
use Symfony\Contracts\Service\Attribute\Required;
10+
11+
class TestAnnotations
12+
{
13+
/** @required */
14+
public string $one;
15+
16+
private string $two;
17+
18+
public string $three;
19+
20+
private string $four;
21+
22+
/**
23+
* @required
24+
*/
25+
public function setTwo(int $two): void
26+
{
27+
$this->two = $two;
28+
}
29+
30+
public function setFour(int $four): void
31+
{
32+
$this->four = $four;
33+
}
34+
}
35+
36+
class TestAttributes
37+
{
38+
#[Required]
39+
public string $one;
40+
41+
private string $two;
42+
43+
public string $three;
44+
45+
private string $four;
46+
47+
#[Required]
48+
public function setTwo(int $two): void
49+
{
50+
$this->two = $two;
51+
}
52+
53+
public function setFour(int $four): void
54+
{
55+
$this->four = $four;
56+
}
57+
}
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)