Skip to content

Commit

Permalink
[container] support subscribed services in child classes (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
seferov authored Nov 15, 2020
1 parent e750732 commit 9dc1c34
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ vendor/bin/psalm-plugin enable psalm/plugin-symfony
### Features

- Detect `ContainerInterface::get()` result type. Works better if you [configure](#configuration) compiled container XML file.
- Support [Service Subscribers](https://github.com/psalm/psalm-plugin-symfony/issues/20).
- Support [Service Subscribers](https://github.com/psalm/psalm-plugin-symfony/issues/20). Works only if you [configure](#configuration) compiled container XML file.
- Detect return type of console arguments (`InputInterface::getArgument()`) and options (`InputInterface::getOption()`). Enforces
to use InputArgument and InputOption constants as a part of best practise.
- Detects correct Doctrine repository class if entities are configured with annotations.
Expand Down
65 changes: 45 additions & 20 deletions src/Handler/ContainerHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
Expand All @@ -18,6 +19,7 @@
use Psalm\Plugin\Hook\AfterMethodCallAnalysisInterface;
use Psalm\StatementsSource;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FileStorage;
use Psalm\SymfonyPsalmPlugin\Issue\NamingConventionViolation;
use Psalm\SymfonyPsalmPlugin\Issue\PrivateService;
use Psalm\SymfonyPsalmPlugin\Issue\ServiceNotFound;
Expand Down Expand Up @@ -146,30 +148,24 @@ public static function afterClassLikeVisit(
}

// see https://symfony.com/doc/current/service_container/service_subscribers_locators.html
if (self::$containerMeta && $stmt instanceof Class_ && isset($storage->class_implements['symfony\contracts\service\servicesubscriberinterface'])) {
if (self::$containerMeta && $stmt instanceof Class_ && in_array('getsubscribedservices', array_keys($storage->methods))) {
foreach ($stmt->stmts as $classStmt) {
if ($classStmt instanceof ClassMethod && 'getSubscribedServices' === $classStmt->name->name && $classStmt->stmts) {
foreach ($classStmt->stmts as $methodStmt) {
if ($methodStmt instanceof Return_ && ($return = $methodStmt->expr) && $return instanceof Expr\Array_) {
foreach ($return->items as $arrayItem) {
if ($arrayItem instanceof Expr\ArrayItem) {
$value = $arrayItem->value;
if (!$value instanceof Expr\ClassConstFetch) {
continue;
}

/** @var string $className */
$className = $value->class->getAttribute('resolvedName');

$key = $arrayItem->key;
$serviceId = $key instanceof String_ ? $key->value : $className;

$service = new Service($serviceId, $className);
$service->setIsPublic(true);
self::$containerMeta->add($service);
if (!$methodStmt instanceof Return_) {
continue;
}

$codebase->queueClassLikeForScanning($className);
$fileStorage->referenced_classlikes[strtolower($className)] = $className;
$return = $methodStmt->expr;
if ($return instanceof Expr\Array_) {
self::addSubscribedServicesArray($return, $codebase, $fileStorage);
} elseif ($return instanceof Expr\FuncCall) {
$funcName = $return->name;
if ($funcName instanceof Name && in_array('array_merge', $funcName->parts)) {
foreach ($return->args as $arg) {
if ($arg->value instanceof Expr\Array_) {
self::addSubscribedServicesArray($arg->value, $codebase, $fileStorage);
}
}
}
}
Expand All @@ -179,6 +175,35 @@ public static function afterClassLikeVisit(
}
}

private static function addSubscribedServicesArray(Expr\Array_ $array, Codebase $codebase, FileStorage $fileStorage): void
{
if (!self::$containerMeta) {
return;
}

foreach ($array->items as $arrayItem) {
if ($arrayItem instanceof Expr\ArrayItem) {
$value = $arrayItem->value;
if (!$value instanceof Expr\ClassConstFetch) {
continue;
}

/** @var string $className */
$className = $value->class->getAttribute('resolvedName');

$key = $arrayItem->key;
$serviceId = $key instanceof String_ ? $key->value : $className;

$service = new Service($serviceId, $className);
$service->setIsPublic(true);
self::$containerMeta->add($service);

$codebase->queueClassLikeForScanning($className);
$fileStorage->referenced_classlikes[strtolower($className)] = $className;
}
}
}

private static function isContainerMethod(string $declaringMethodId, string $methodName): bool
{
return in_array(
Expand Down
31 changes: 31 additions & 0 deletions tests/acceptance/acceptance/ServiceSubscriber.feature
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,34 @@ Feature: Service Subscriber
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
| Trace | $validator: Symfony\Component\Validator\Validator\ValidatorInterface |
And I see no other errors


Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices using array_merge
Given I have the following code
"""
<?php
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class SomeController extends AbstractController
{
public function __invoke()
{
/** @psalm-trace $entityManager */
$entityManager = $this->container->get('custom_service');
}
public static function getSubscribedServices(): array
{
return array_merge([
'custom_service' => EntityManagerInterface::class,
], parent::getSubscribedServices());
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
And I see no other errors

0 comments on commit 9dc1c34

Please sign in to comment.