Skip to content

Commit 01b5dcb

Browse files
authored
[container] read the DI required annotation (#100)
1 parent 6c6b4f5 commit 01b5dcb

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

src/Handler/RequiredSetterHandler.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psalm\SymfonyPsalmPlugin\Handler;
6+
7+
use PhpParser\Comment\Doc;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PhpParser\Node\Stmt\ClassLike;
10+
use PhpParser\NodeTraverser;
11+
use Psalm\Codebase;
12+
use Psalm\FileSource;
13+
use Psalm\Internal\PhpVisitor\AssignmentMapVisitor;
14+
use Psalm\Plugin\Hook\AfterClassLikeVisitInterface;
15+
use Psalm\Storage\ClassLikeStorage;
16+
17+
class RequiredSetterHandler implements AfterClassLikeVisitInterface
18+
{
19+
public static function afterClassLikeVisit(ClassLike $stmt, ClassLikeStorage $storage, FileSource $statements_source, Codebase $codebase, array &$file_replacements = [])
20+
{
21+
if (!$stmt instanceof Class_) {
22+
return;
23+
}
24+
25+
foreach ($stmt->getMethods() as $method) {
26+
$docComment = $method->getDocComment();
27+
28+
if ($docComment instanceof Doc && false !== strpos($docComment->getText(), '@required')) {
29+
$traverser = new NodeTraverser();
30+
$visitor = new AssignmentMapVisitor(null);
31+
$traverser->addVisitor($visitor);
32+
$traverser->traverse($method->getStmts() ?? []);
33+
34+
foreach (array_keys($visitor->getAssignmentMap()) as $assignment) {
35+
if (0 !== strpos($assignment, '$this->')) {
36+
continue;
37+
}
38+
39+
$property = substr($assignment, strlen('$this->'));
40+
if (!array_key_exists($property, $storage->properties)) {
41+
continue;
42+
}
43+
44+
$storage->initialized_properties[$property] = true;
45+
}
46+
}
47+
}
48+
}
49+
}

src/Plugin.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Psalm\SymfonyPsalmPlugin\Handler\ContainerHandler;
1313
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineRepositoryHandler;
1414
use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler;
15+
use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler;
1516
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
1617
use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter;
1718
use Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesMapping;
@@ -53,10 +54,12 @@ public function __invoke(RegistrationInterface $api, SimpleXMLElement $config =
5354
require_once __DIR__.'/Handler/ContainerHandler.php';
5455
require_once __DIR__.'/Handler/ConsoleHandler.php';
5556
require_once __DIR__.'/Handler/ContainerDependencyHandler.php';
57+
require_once __DIR__.'/Handler/RequiredSetterHandler.php';
5658

5759
$api->registerHooksFromClass(HeaderBagHandler::class);
5860
$api->registerHooksFromClass(ConsoleHandler::class);
5961
$api->registerHooksFromClass(ContainerDependencyHandler::class);
62+
$api->registerHooksFromClass(RequiredSetterHandler::class);
6063

6164
if (class_exists(AnnotationRegistry::class)) {
6265
require_once __DIR__.'/Handler/DoctrineRepositoryHandler.php';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@symfony-common
2+
Feature: Annotation class
3+
4+
Background:
5+
Given I have the following config
6+
"""
7+
<?xml version="1.0"?>
8+
<psalm errorLevel="1">
9+
<projectFiles>
10+
<directory name="."/>
11+
<ignoreFiles> <directory name="../../vendor"/> </ignoreFiles>
12+
</projectFiles>
13+
14+
<plugins>
15+
<pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin">
16+
<containerXml>../../tests/acceptance/container.xml</containerXml>
17+
</pluginClass>
18+
</plugins>
19+
</psalm>
20+
"""
21+
22+
Scenario: PropertyNotSetInConstructor error is not raised when the @required annotation is present.
23+
Given I have the following code
24+
"""
25+
<?php
26+
final class MyServiceA {
27+
}
28+
29+
final class MyServiceB {
30+
private MyServiceA $a;
31+
public function __construct(){}
32+
33+
/** @required */
34+
private function setMyServiceA(MyServiceA $a): void { $this->a = $a; }
35+
}
36+
"""
37+
When I run Psalm
38+
Then I see no errors
39+
40+
Scenario: PropertyNotSetInConstructor error is raised when the @required annotation is not present.
41+
Given I have the following code
42+
"""
43+
<?php
44+
final class MyServiceA {
45+
}
46+
47+
final class MyServiceB {
48+
private MyServiceA $a;
49+
public function __construct(){}
50+
51+
private function setMyServiceA(MyServiceA $a): void { $this->a = $a; }
52+
}
53+
"""
54+
When I run Psalm
55+
Then I see these errors
56+
| Type | Message |
57+
| PropertyNotSetInConstructor | Property MyServiceB::$a is not defined in constructor of MyServiceB and in any private or final methods called in the constructor |
58+
And I see no other errors

0 commit comments

Comments
 (0)