Skip to content

Commit

Permalink
Support @reqired annotation and #[Required] attribute for properties.
Browse files Browse the repository at this point in the history
Prevent PropertyNotSetInConstructor when ContainerAwareTrait used.
  • Loading branch information
Punk-UnDeaD committed Mar 1, 2021
1 parent a56f44a commit aa267ec
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/Handler/ContainerAwareTraitHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Psalm\SymfonyPsalmPlugin\Handler;

use PhpParser\Node\Stmt\ClassLike;
use Psalm\Codebase;
use Psalm\FileSource;
use Psalm\Plugin\Hook\AfterClassLikeVisitInterface;
use Psalm\Storage\ClassLikeStorage;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class ContainerAwareTraitHandler implements AfterClassLikeVisitInterface
{
public static function afterClassLikeVisit(
ClassLike $stmt,
ClassLikeStorage $storage,
FileSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
if (ContainerAwareTrait::class === $storage->name) {
$storage->initialized_properties['container'] = true;
}
}
}
52 changes: 52 additions & 0 deletions src/Handler/RequiredPropertyHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Psalm\SymfonyPsalmPlugin\Handler;

use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use Psalm\Codebase;
use Psalm\FileSource;
use Psalm\Plugin\Hook\AfterClassLikeVisitInterface;
use Psalm\Storage\ClassLikeStorage;
use Symfony\Contracts\Service\Attribute\Required;

class RequiredPropertyHandler implements AfterClassLikeVisitInterface
{
public static function afterClassLikeVisit(
ClassLike $stmt,
ClassLikeStorage $storage,
FileSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
if (!$stmt instanceof Class_) {
return;
}
$reflection = null;
foreach ($storage->properties as $name => $property) {
if (!empty($storage->initialized_properties[$name])) {
continue;
}
foreach ($property->attributes as $attribute) {
if (Required::class === $attribute->fq_class_name) {
$storage->initialized_properties[$name] = true;
continue 2;
}
}
$class = $storage->name;
if (!class_exists($class)) {
/** @psalm-suppress UnresolvableInclude */
require_once $statements_source->getRootFilePath();
}
/** @psalm-suppress ArgumentTypeCoercion */
$reflection = $reflection ?? new \ReflectionClass($class);
if ($reflection->hasProperty($name)) {
$reflectionProperty = $reflection->getProperty($name);
$docCommend = $reflectionProperty->getDocComment();
if ($docCommend && false !== strpos(strtoupper($docCommend), '@REQUIRED')) {
$storage->initialized_properties[$name] = true;
}
}
}
}
}
6 changes: 6 additions & 0 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
use Psalm\Plugin\RegistrationInterface;
use Psalm\SymfonyPsalmPlugin\Handler\AnnotationHandler;
use Psalm\SymfonyPsalmPlugin\Handler\ConsoleHandler;
use Psalm\SymfonyPsalmPlugin\Handler\ContainerAwareTraitHandler;
use Psalm\SymfonyPsalmPlugin\Handler\ContainerDependencyHandler;
use Psalm\SymfonyPsalmPlugin\Handler\ContainerHandler;
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineQueryBuilderHandler;
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineRepositoryHandler;
use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler;
use Psalm\SymfonyPsalmPlugin\Handler\RequiredPropertyHandler;
use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler;
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter;
Expand Down Expand Up @@ -56,13 +58,17 @@ public function __invoke(RegistrationInterface $api, SimpleXMLElement $config =
require_once __DIR__.'/Handler/HeaderBagHandler.php';
require_once __DIR__.'/Handler/ContainerHandler.php';
require_once __DIR__.'/Handler/ConsoleHandler.php';
require_once __DIR__.'/Handler/ContainerAwareTraitHandler.php';
require_once __DIR__.'/Handler/ContainerDependencyHandler.php';
require_once __DIR__.'/Handler/RequiredPropertyHandler.php';
require_once __DIR__.'/Handler/RequiredSetterHandler.php';
require_once __DIR__.'/Handler/DoctrineQueryBuilderHandler.php';

$api->registerHooksFromClass(HeaderBagHandler::class);
$api->registerHooksFromClass(ConsoleHandler::class);
$api->registerHooksFromClass(ContainerAwareTraitHandler::class);
$api->registerHooksFromClass(ContainerDependencyHandler::class);
$api->registerHooksFromClass(RequiredPropertyHandler::class);
$api->registerHooksFromClass(RequiredSetterHandler::class);

if (class_exists(\Doctrine\ORM\QueryBuilder::class)) {
Expand Down
55 changes: 55 additions & 0 deletions tests/acceptance/acceptance/RequiredAttribute.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@symfony-common
Feature: RequiredAttribute

Background:
Given I have the following config
"""
<?xml version="1.0"?>
<psalm errorLevel="1">
<projectFiles>
<directory name="."/>
<ignoreFiles> <directory name="../../vendor"/> </ignoreFiles>
</projectFiles>
<plugins>
<pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin">
<containerXml>../../tests/acceptance/container.xml</containerXml>
</pluginClass>
</plugins>
</psalm>
"""

Scenario: PropertyNotSetInConstructor error is not raised when the @required annotation is present.
Given I have the following code
"""
<?php
final class MyServiceA {
}
final class MyServiceB {
/** @required */
public MyServiceA $a;
public function __construct(){}
}
"""
When I run Psalm
Then I see no errors

Scenario: PropertyNotSetInConstructor error is raised when the @required annotation is not present.
Given I have the following code
"""
<?php
final class MyServiceA {
}
final class MyServiceB {
public MyServiceA $a;
public function __construct(){}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| PropertyNotSetInConstructor | Property MyServiceB::$a is not defined in constructor of MyServiceB and in any methods called in the constructor |
And I see no other errors

0 comments on commit aa267ec

Please sign in to comment.