mirror of
https://github.com/danog/psalm-plugin-symfony.git
synced 2024-11-26 20:04:58 +01:00
v3.0 - improve container handler & refactor (#202)
This commit is contained in:
parent
8a7744a540
commit
20604f13e7
1
.github/workflows/integrate.yaml
vendored
1
.github/workflows/integrate.yaml
vendored
@ -73,7 +73,6 @@ jobs:
|
||||
- 8.0
|
||||
|
||||
symfony-version:
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
|
||||
|
@ -2,28 +2,21 @@
|
||||
|
||||
namespace Psalm\SymfonyPsalmPlugin\Handler;
|
||||
|
||||
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\ClassMethod;
|
||||
use PhpParser\Node\Stmt\Return_;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
|
||||
use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
|
||||
use Psalm\Storage\FileStorage;
|
||||
use Psalm\SymfonyPsalmPlugin\Issue\NamingConventionViolation;
|
||||
use Psalm\SymfonyPsalmPlugin\Issue\PrivateService;
|
||||
use Psalm\SymfonyPsalmPlugin\Issue\ServiceNotFound;
|
||||
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
||||
use Psalm\SymfonyPsalmPlugin\Symfony\Service;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Union;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
|
||||
class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface
|
||||
{
|
||||
@ -88,8 +81,9 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
|
||||
return;
|
||||
}
|
||||
|
||||
$service = self::$containerMeta->get($serviceId);
|
||||
if ($service) {
|
||||
try {
|
||||
$service = self::$containerMeta->get($serviceId, $context->self);
|
||||
|
||||
if (!self::followsNamingConvention($serviceId) && false === strpos($serviceId, '\\')) {
|
||||
IssueBuffer::accepts(
|
||||
new NamingConventionViolation(new CodeLocation($statements_source, $expr->args[0]->value)),
|
||||
@ -97,7 +91,7 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
|
||||
);
|
||||
}
|
||||
|
||||
$class = $service->getClassName();
|
||||
$class = $service->getClass();
|
||||
if ($class) {
|
||||
$codebase->classlikes->addFullyQualifiedClassName($class);
|
||||
$event->setReturnTypeCandidate(new Union([new TNamedObject($class)]));
|
||||
@ -112,7 +106,7 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} catch (ServiceNotFoundException $e) {
|
||||
IssueBuffer::accepts(
|
||||
new ServiceNotFound($serviceId, new CodeLocation($statements_source, $expr->args[0]->value)),
|
||||
$statements_source->getSuppressedIssues()
|
||||
@ -128,7 +122,6 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
|
||||
$codebase = $event->getCodebase();
|
||||
$statements_source = $event->getStatementsSource();
|
||||
$storage = $event->getStorage();
|
||||
$stmt = $event->getStmt();
|
||||
|
||||
$fileStorage = $codebase->file_storage_provider->get($statements_source->getFilePath());
|
||||
|
||||
@ -140,64 +133,6 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// see https://symfony.com/doc/current/service_container/service_subscribers_locators.html
|
||||
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_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (null === self::$containerMeta->get($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
|
||||
|
@ -8,6 +8,7 @@ use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
|
||||
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Union;
|
||||
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
|
||||
|
||||
class ParameterBagHandler implements AfterMethodCallAnalysisInterface
|
||||
{
|
||||
@ -36,8 +37,15 @@ class ParameterBagHandler implements AfterMethodCallAnalysisInterface
|
||||
|
||||
$argument = $expr->args[0]->value->value;
|
||||
|
||||
try {
|
||||
$parameter = self::$containerMeta->getParameter($argument);
|
||||
} catch (ParameterNotFoundException $e) {
|
||||
// maybe emit ParameterNotFound issue
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo find a better way to calculate return type
|
||||
switch (gettype(self::$containerMeta->getParameter($argument))) {
|
||||
switch (gettype($parameter)) {
|
||||
case 'string':
|
||||
$event->setReturnTypeCandidate(new Union([Atomic::create('string')]));
|
||||
break;
|
||||
|
@ -5,47 +5,66 @@ declare(strict_types=1);
|
||||
namespace Psalm\SymfonyPsalmPlugin\Symfony;
|
||||
|
||||
use Psalm\Exception\ConfigException;
|
||||
use Symfony\Component\HttpKernel\Kernel;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
class ContainerMeta
|
||||
{
|
||||
/**
|
||||
* @psalm-var array<string, Service>
|
||||
*/
|
||||
private $services = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private $classNames = [];
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $parameters = [];
|
||||
private $classLocators = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private $serviceLocators = [];
|
||||
|
||||
/**
|
||||
* @var ContainerBuilder
|
||||
*/
|
||||
private $container;
|
||||
|
||||
public function __construct(array $containerXmlPaths)
|
||||
{
|
||||
$this->init($containerXmlPaths);
|
||||
}
|
||||
|
||||
public function get(string $id): ?Service
|
||||
/**
|
||||
* @throws ServiceNotFoundException
|
||||
*/
|
||||
public function get(string $id, ?string $contextClass = null): Definition
|
||||
{
|
||||
if (isset($this->services[$id])) {
|
||||
return $this->services[$id];
|
||||
if ($contextClass && isset($this->classLocators[$contextClass]) && isset($this->serviceLocators[$this->classLocators[$contextClass]]) && isset($this->serviceLocators[$this->classLocators[$contextClass]][$id])) {
|
||||
$id = $this->serviceLocators[$this->classLocators[$contextClass]][$id];
|
||||
|
||||
try {
|
||||
$definition = $this->getDefinition($id);
|
||||
} catch (ServiceNotFoundException $e) {
|
||||
if (!class_exists($id)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$definition = new Definition($id);
|
||||
}
|
||||
|
||||
$definition->setPublic(true);
|
||||
} else {
|
||||
$definition = $this->getDefinition($id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(Service $service): void
|
||||
{
|
||||
if (($alias = $service->getAlias()) && isset($this->services[$alias])) {
|
||||
$aliasedService = $this->services[$alias];
|
||||
$service->setClassName($aliasedService->getClassName());
|
||||
}
|
||||
|
||||
$this->services[$service->getId()] = $service;
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,11 +72,7 @@ class ContainerMeta
|
||||
*/
|
||||
public function getParameter(string $key)
|
||||
{
|
||||
if (isset($this->parameters[$key])) {
|
||||
return $this->parameters[$key];
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->container->getParameter($key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,103 +85,82 @@ class ContainerMeta
|
||||
|
||||
private function init(array $containerXmlPaths): void
|
||||
{
|
||||
/** @var string $containerXmlPath */
|
||||
foreach ($containerXmlPaths as $containerXmlPath) {
|
||||
$xmlPath = realpath($containerXmlPath);
|
||||
if (!$xmlPath || !file_exists($xmlPath)) {
|
||||
continue;
|
||||
$this->container = new ContainerBuilder();
|
||||
$xml = new XmlFileLoader($this->container, new FileLocator());
|
||||
|
||||
$containerXmlPath = null;
|
||||
foreach ($containerXmlPaths as $filePath) {
|
||||
$containerXmlPath = realpath((string) $filePath);
|
||||
if ($containerXmlPath) {
|
||||
break;
|
||||
}
|
||||
|
||||
$xml = simplexml_load_file($xmlPath);
|
||||
if (!$xml->services instanceof \SimpleXMLElement) {
|
||||
throw new ConfigException($xmlPath.' is not a valid container xml file');
|
||||
}
|
||||
|
||||
/** @psalm-var \SimpleXMLElement $serviceXml */
|
||||
foreach ($xml->services->service as $serviceXml) {
|
||||
/** @psalm-var \SimpleXMLElement $serviceAttributes */
|
||||
$serviceAttributes = $serviceXml->attributes();
|
||||
|
||||
$className = (string) $serviceAttributes->class;
|
||||
|
||||
if ($className) {
|
||||
$this->classNames[] = $className;
|
||||
}
|
||||
|
||||
$service = new Service((string) $serviceAttributes->id, $className);
|
||||
if (isset($serviceAttributes->alias)) {
|
||||
$service->setAlias((string) $serviceAttributes->alias);
|
||||
}
|
||||
|
||||
if (3 < Kernel::MAJOR_VERSION) {
|
||||
$service->setIsPublic('true' === (string) $serviceAttributes->public);
|
||||
} else {
|
||||
$service->setIsPublic('false' !== (string) $serviceAttributes->public);
|
||||
}
|
||||
|
||||
$this->add($service);
|
||||
}
|
||||
|
||||
/** @var \SimpleXMLElement $parameter */
|
||||
foreach ($xml->parameters->parameter as $parameter) {
|
||||
$value = $this->getXmlParameterValue($parameter);
|
||||
|
||||
$attributes = $parameter->attributes();
|
||||
if (!isset($attributes->key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->parameters[(string) $attributes->key] = $value;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ConfigException('Container xml file(s) not found at ');
|
||||
if (!$containerXmlPath) {
|
||||
throw new ConfigException('Container xml file(s) not found!');
|
||||
}
|
||||
|
||||
$xml->load($containerXmlPath);
|
||||
|
||||
foreach ($this->container->getDefinitions() as $definition) {
|
||||
$definitionFactory = $definition->getFactory();
|
||||
if ($definition->hasTag('container.service_locator_context') && is_array($definitionFactory)) {
|
||||
/** @var Reference $reference */
|
||||
$reference = $definitionFactory[0];
|
||||
$this->classLocators[$definition->getTag('container.service_locator_context')[0]['id']] = (string) $reference;
|
||||
} elseif ($definition->hasTag('container.service_locator')) {
|
||||
continue;
|
||||
} elseif ($className = $definition->getClass()) {
|
||||
$this->classNames[] = $className;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->container->findTaggedServiceIds('container.service_locator') as $key => $_) {
|
||||
$definition = $this->container->getDefinition($key);
|
||||
foreach ($definition->getArgument(0) as $id => $argument) {
|
||||
if ($argument instanceof Reference) {
|
||||
$this->addServiceLocator($key, $id, $argument);
|
||||
} elseif ($argument instanceof ServiceClosureArgument) {
|
||||
foreach ($argument->getValues() as $value) {
|
||||
$this->addServiceLocator($key, $id, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function addServiceLocator(string $key, string $id, Reference $reference): void
|
||||
{
|
||||
$this->serviceLocators[$key][$id] = (string) $reference;
|
||||
|
||||
try {
|
||||
$definition = $this->getDefinition((string) $reference);
|
||||
$className = $definition->getClass();
|
||||
if ($className) {
|
||||
$this->classNames[] = $className;
|
||||
}
|
||||
} catch (ServiceNotFoundException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws ServiceNotFoundException
|
||||
*/
|
||||
private function getXmlParameterValue(\SimpleXMLElement $parameter)
|
||||
private function getDefinition(string $id): Definition
|
||||
{
|
||||
$value = null;
|
||||
$attributes = $parameter->attributes();
|
||||
if (isset($attributes->type)) {
|
||||
switch ((string) $attributes->type) {
|
||||
case 'binary':
|
||||
$value = base64_decode((string) $parameter, true);
|
||||
break;
|
||||
case 'collection':
|
||||
foreach ($parameter->children() as $child) {
|
||||
$childAttributes = $child->attributes();
|
||||
if (isset($childAttributes->key)) {
|
||||
$value[(string) $childAttributes->key] = $this->getXmlParameterValue($child);
|
||||
} else {
|
||||
$value[] = $this->getXmlParameterValue($child);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
default:
|
||||
$value = (string) $parameter;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$value = (string) $parameter;
|
||||
if ('true' === $value || 'false' === $value) {
|
||||
$value = (bool) $value;
|
||||
} elseif ('null' === $value) {
|
||||
$value = null;
|
||||
} elseif (is_numeric($value)) {
|
||||
if (false === strpos($value, '.')) {
|
||||
$value = (int) $value;
|
||||
} else {
|
||||
$value = (float) $value;
|
||||
}
|
||||
try {
|
||||
$definition = $this->container->getDefinition($id);
|
||||
} catch (ServiceNotFoundException $serviceNotFoundException) {
|
||||
try {
|
||||
$alias = $this->container->getAlias($id);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw $serviceNotFoundException;
|
||||
}
|
||||
|
||||
$definition = $this->container->getDefinition((string) $alias);
|
||||
$definition->setPublic($alias->isPublic());
|
||||
}
|
||||
|
||||
return $value;
|
||||
return $definition;
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\SymfonyPsalmPlugin\Symfony;
|
||||
|
||||
class Service
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $className;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isPublic = false;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $alias = null;
|
||||
|
||||
public function __construct(string $id, string $className)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->className = $className;
|
||||
}
|
||||
|
||||
public function isPublic(): bool
|
||||
{
|
||||
return $this->isPublic;
|
||||
}
|
||||
|
||||
public function setIsPublic(bool $isPublic): void
|
||||
{
|
||||
$this->isPublic = $isPublic;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return $this->className;
|
||||
}
|
||||
|
||||
public function getAlias(): ?string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function setAlias(string $alias): void
|
||||
{
|
||||
$this->alias = $alias;
|
||||
}
|
||||
|
||||
public function setClassName(string $className): void
|
||||
{
|
||||
$this->className = $className;
|
||||
}
|
||||
}
|
@ -6,18 +6,18 @@ Feature: Service Subscriber
|
||||
"""
|
||||
<containerXml>../../tests/acceptance/container.xml</containerXml>
|
||||
"""
|
||||
|
||||
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices
|
||||
Given I have the following code
|
||||
And I have the following code preamble
|
||||
"""
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class SomeController implements ServiceSubscriberInterface
|
||||
class DummyController implements ServiceSubscriberInterface
|
||||
{
|
||||
private $container;
|
||||
|
||||
@ -26,96 +26,34 @@ Feature: Service Subscriber
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
/** @psalm-trace $entityManager */
|
||||
$entityManager = $this->container->get('em');
|
||||
|
||||
/** @psalm-trace $validator */
|
||||
$validator = $this->container->get(ValidatorInterface::class);
|
||||
}
|
||||
|
||||
public static function getSubscribedServices()
|
||||
{
|
||||
return [
|
||||
'em' => EntityManagerInterface::class, // with key
|
||||
ValidatorInterface::class, // without key
|
||||
// takes container.xml into account
|
||||
];
|
||||
}
|
||||
}
|
||||
"""
|
||||
When I run Psalm
|
||||
Then I see these errors
|
||||
| Type | Message |
|
||||
| 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
|
||||
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices
|
||||
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');
|
||||
}
|
||||
/** @psalm-trace $service1 */
|
||||
$service1 = $this->container->get('dummy_service_with_locator');
|
||||
|
||||
public static function getSubscribedServices(): array
|
||||
{
|
||||
return array_merge([
|
||||
'custom_service' => EntityManagerInterface::class,
|
||||
], parent::getSubscribedServices());
|
||||
/** @psalm-trace $service2 */
|
||||
$service2 = $this->container->get('dummy_service_with_locator2');
|
||||
|
||||
/** @psalm-trace $service3 */
|
||||
$service3 = $this->container->get('dummy_service_with_locator3');
|
||||
}
|
||||
}
|
||||
"""
|
||||
When I run Psalm
|
||||
Then I see these errors
|
||||
| Type | Message |
|
||||
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
|
||||
And I see no other errors
|
||||
|
||||
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices, already defined as an alias in containerXml
|
||||
Given I have the following code
|
||||
"""
|
||||
<?php
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||
|
||||
class SomeController implements ServiceSubscriberInterface
|
||||
{
|
||||
private $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
/** @psalm-trace $kernel */
|
||||
$kernel = $this->container->get('http_kernel');
|
||||
}
|
||||
|
||||
public static function getSubscribedServices(): array
|
||||
{
|
||||
return [
|
||||
'http_kernel' => HttpKernelInterface::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
"""
|
||||
When I run Psalm
|
||||
Then I see these errors
|
||||
| Type | Message |
|
||||
| Trace | $kernel: Symfony\Component\HttpKernel\HttpKernel |
|
||||
| Type | Message |
|
||||
| Trace | $service1: Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService |
|
||||
| Trace | $service2: Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService |
|
||||
| Trace | $service3: Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService |
|
||||
And I see no other errors
|
||||
|
@ -1,42 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
<parameters>
|
||||
<parameter key="kernel.environment">dev</parameter>
|
||||
<parameter key="debug_enabled">true</parameter>
|
||||
<parameter key="debug_disabled">false</parameter>
|
||||
<parameter key="version" type="string">1</parameter>
|
||||
<parameter key="integer_one">1</parameter>
|
||||
<parameter key="pi">3.14</parameter>
|
||||
<parameter key="collection1" type="collection">
|
||||
<parameter key="key1">val1</parameter>
|
||||
<parameter key="key2">val2</parameter>
|
||||
<parameters>
|
||||
<parameter key="kernel.environment">dev</parameter>
|
||||
<parameter key="debug_enabled">true</parameter>
|
||||
<parameter key="debug_disabled">false</parameter>
|
||||
<parameter key="version" type="string">1</parameter>
|
||||
<parameter key="integer_one">1</parameter>
|
||||
<parameter key="pi">3.14</parameter>
|
||||
<parameter key="collection1" type="collection">
|
||||
<parameter key="key1">val1</parameter>
|
||||
<parameter key="key2">val2</parameter>
|
||||
</parameter>
|
||||
<parameter key="nested_collection" type="collection">
|
||||
<parameter key="key">val</parameter>
|
||||
<parameter key="child_collection" type="collection">
|
||||
<parameter key="boolean">true</parameter>
|
||||
<parameter key="float">2.18</parameter>
|
||||
<parameter key="grandchild_collection" type="collection">
|
||||
<parameter key="string">something</parameter>
|
||||
</parameter>
|
||||
<parameter key="nested_collection" type="collection">
|
||||
<parameter key="key">val</parameter>
|
||||
<parameter key="child_collection" type="collection">
|
||||
<parameter key="boolean">true</parameter>
|
||||
<parameter key="float">2.18</parameter>
|
||||
<parameter key="grandchild_collection" type="collection">
|
||||
<parameter key="string">something</parameter>
|
||||
</parameter>
|
||||
</parameter>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<services>
|
||||
<service id="foo" alias="no_such_service" public="true"/>
|
||||
<service id="service_container" class="Symfony\Component\DependencyInjection\ContainerInterface" public="true" synthetic="true"/>
|
||||
<service id="http_kernel" class="Symfony\Component\HttpKernel\HttpKernel" public="true">
|
||||
<tag name="container.hot_path"/>
|
||||
<argument type="service" id="event_dispatcher"/>
|
||||
<argument type="service" id="controller_resolver"/>
|
||||
<argument type="service" id="request_stack"/>
|
||||
<argument type="service" id="argument_resolver"/>
|
||||
</service>
|
||||
<service id="Symfony\Component\HttpKernel\HttpKernelInterface" alias="http_kernel" public="true"/>
|
||||
<service id="Foo\Bar" class="Foo\Bar" public="false"/>
|
||||
<service id="private_service" class="Foo\Bar" public="false"/>
|
||||
<service id="dummy_private_service" class="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService" public="false"/>
|
||||
<service id="public_service_wo_public_attr" class="Foo\Bar"/>
|
||||
<service id="wronglyNamedService" class="Foo\Bar" public="true" />
|
||||
</services>
|
||||
</parameter>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<services>
|
||||
<service id="doctrine.orm.entity_manager" alias="doctrine.orm.default_entity_manager" public="true"/>
|
||||
<service id="doctrine.orm.default_entity_manager" class="Doctrine\ORM\EntityManager" public="true" lazy="true">
|
||||
<tag name="container.preload" class="Doctrine\ORM\Proxy\Autoloader"/>
|
||||
<argument type="service" id="doctrine.dbal.default_connection"/>
|
||||
<argument type="service" id="doctrine.orm.default_configuration"/>
|
||||
<factory class="Doctrine\ORM\EntityManager" method="create"/>
|
||||
<configurator service="doctrine.orm.default_manager_configurator" method="configure"/>
|
||||
</service>
|
||||
<service id="foo" alias="no_such_service" public="true"/>
|
||||
<service id="service_container" class="Symfony\Component\DependencyInjection\ContainerInterface" public="true" synthetic="true"/>
|
||||
<service id="http_kernel" class="Symfony\Component\HttpKernel\HttpKernel" public="true">
|
||||
<tag name="container.hot_path"/>
|
||||
<argument type="service" id="event_dispatcher"/>
|
||||
<argument type="service" id="controller_resolver"/>
|
||||
<argument type="service" id="request_stack"/>
|
||||
<argument type="service" id="argument_resolver"/>
|
||||
</service>
|
||||
<service id="Symfony\Component\HttpKernel\HttpKernelInterface" alias="http_kernel" public="true"/>
|
||||
<service id="Foo\Bar" class="Foo\Bar" public="false"/>
|
||||
<service id="private_service" class="Foo\Bar" public="false"/>
|
||||
<service id="dummy_private_service" class="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService" public="false"/>
|
||||
<service id="public_service_wo_public_attr" class="Foo\Bar"/>
|
||||
<service id="wronglyNamedService" class="Foo\Bar" public="true" />
|
||||
<service id=".service_locator.123xyz" class="Symfony\Component\DependencyInjection\ServiceLocator" public="false">
|
||||
<tag name="container.service_locator"/>
|
||||
<argument type="collection">
|
||||
<argument key="dummy_service_with_locator" type="service" id="dummy_private_service"/>
|
||||
<argument key="dummy_service_with_locator2" type="service" id="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService"/>
|
||||
<argument key="dummy_service_with_locator3" type="service_closure" id="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService"/>
|
||||
<argument key="validator" type="service" id="debug.validator"/>
|
||||
</argument>
|
||||
</service>
|
||||
<service id=".service_locator.123xyz.App\Controller\DummyController" class="Symfony\Component\DependencyInjection\ServiceLocator">
|
||||
<tag name="container.service_locator_context" id="App\Controller\DummyController"/>
|
||||
<argument>App\Controller\DummyController</argument>
|
||||
<argument type="service" id="service_container"/>
|
||||
<factory service=".service_locator.123xyz" method="withContext"/>
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
@ -5,7 +5,9 @@ namespace Psalm\SymfonyPsalmPlugin\Tests\Symfony;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psalm\Exception\ConfigException;
|
||||
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
||||
use Psalm\SymfonyPsalmPlugin\Symfony\Service;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Kernel;
|
||||
|
||||
/**
|
||||
@ -29,86 +31,38 @@ class ContainerMetaTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox service attributes for > Symfony 3
|
||||
* @testdox service attributes
|
||||
* @dataProvider publicServices
|
||||
*/
|
||||
public function testServices($id, string $className, bool $isPublic)
|
||||
{
|
||||
if (3 === Kernel::MAJOR_VERSION) {
|
||||
$this->markTestSkipped('Should run for > Symfony 3');
|
||||
}
|
||||
|
||||
$service = $this->containerMeta->get($id);
|
||||
$this->assertInstanceOf(Service::class, $service);
|
||||
$this->assertSame($className, $service->getClassName());
|
||||
$this->assertSame($isPublic, $service->isPublic());
|
||||
$serviceDefinition = $this->containerMeta->get($id);
|
||||
$this->assertInstanceOf(Definition::class, $serviceDefinition);
|
||||
$this->assertSame($className, $serviceDefinition->getClass());
|
||||
$this->assertSame($isPublic, $serviceDefinition->isPublic());
|
||||
}
|
||||
|
||||
public function publicServices()
|
||||
public function publicServices(): iterable
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 'service_container',
|
||||
'className' => 'Symfony\Component\DependencyInjection\ContainerInterface',
|
||||
'isPublic' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'Foo\Bar',
|
||||
'className' => 'Foo\Bar',
|
||||
'isPublic' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'Symfony\Component\HttpKernel\HttpKernelInterface',
|
||||
'className' => 'Symfony\Component\HttpKernel\HttpKernel',
|
||||
'isPublic' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'public_service_wo_public_attr',
|
||||
'className' => 'Foo\Bar',
|
||||
'isPublic' => false,
|
||||
],
|
||||
yield [
|
||||
'id' => 'service_container',
|
||||
'className' => 'Symfony\Component\DependencyInjection\ContainerInterface',
|
||||
'isPublic' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox service attributes for Symfony 3
|
||||
* @dataProvider publicServices3
|
||||
*/
|
||||
public function testServices3($id, string $className, bool $isPublic)
|
||||
{
|
||||
if (Kernel::MAJOR_VERSION > 3) {
|
||||
$this->markTestSkipped('Should run for Symfony 3');
|
||||
}
|
||||
|
||||
$service = $this->containerMeta->get($id);
|
||||
$this->assertInstanceOf(Service::class, $service);
|
||||
$this->assertSame($className, $service->getClassName());
|
||||
$this->assertSame($isPublic, $service->isPublic());
|
||||
}
|
||||
|
||||
public function publicServices3()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 'service_container',
|
||||
'className' => 'Symfony\Component\DependencyInjection\ContainerInterface',
|
||||
'isPublic' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'Foo\Bar',
|
||||
'className' => 'Foo\Bar',
|
||||
'isPublic' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'Symfony\Component\HttpKernel\HttpKernelInterface',
|
||||
'className' => 'Symfony\Component\HttpKernel\HttpKernel',
|
||||
'isPublic' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'public_service_wo_public_attr',
|
||||
'className' => 'Foo\Bar',
|
||||
'isPublic' => true,
|
||||
],
|
||||
yield [
|
||||
'id' => 'Foo\Bar',
|
||||
'className' => 'Foo\Bar',
|
||||
'isPublic' => false,
|
||||
];
|
||||
yield [
|
||||
'id' => 'public_service_wo_public_attr',
|
||||
'className' => 'Foo\Bar',
|
||||
'isPublic' => Kernel::MAJOR_VERSION < 5,
|
||||
];
|
||||
yield [
|
||||
'id' => 'doctrine.orm.entity_manager',
|
||||
'className' => 'Doctrine\ORM\EntityManager',
|
||||
'isPublic' => true,
|
||||
];
|
||||
}
|
||||
|
||||
@ -126,6 +80,7 @@ class ContainerMetaTest extends TestCase
|
||||
*/
|
||||
public function testNonExistentService()
|
||||
{
|
||||
$this->expectException(ServiceNotFoundException::class);
|
||||
$this->assertNull($this->containerMeta->get('non-existent-service'));
|
||||
}
|
||||
|
||||
@ -136,7 +91,7 @@ class ContainerMetaTest extends TestCase
|
||||
{
|
||||
$containerMeta = new ContainerMeta(['non-existent-file.xml', __DIR__.'/../../acceptance/container.xml']);
|
||||
$service = $containerMeta->get('service_container');
|
||||
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClassName());
|
||||
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClass());
|
||||
}
|
||||
|
||||
public function testGetParameter(): void
|
||||
@ -161,4 +116,33 @@ class ContainerMetaTest extends TestCase
|
||||
]
|
||||
], $this->containerMeta->getParameter('nested_collection'));
|
||||
}
|
||||
|
||||
public function testGetParameterP(): void
|
||||
{
|
||||
$this->expectException(ParameterNotFoundException::class);
|
||||
$this->containerMeta->getParameter('non_existent');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider serviceLocatorProvider
|
||||
*/
|
||||
public function testGetServiceWithContext(string $id, string $contextClass, string $expectedClass): void
|
||||
{
|
||||
$serviceDefinition = $this->containerMeta->get($id, $contextClass);
|
||||
$this->assertSame($expectedClass, $serviceDefinition->getClass());
|
||||
}
|
||||
|
||||
public function serviceLocatorProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
'dummy_service_with_locator2',
|
||||
'App\Controller\DummyController',
|
||||
'Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService'
|
||||
];
|
||||
yield [
|
||||
'dummy_service_with_locator3',
|
||||
'App\Controller\DummyController',
|
||||
'Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user