mirror of
https://github.com/danog/psalm-plugin-symfony.git
synced 2024-11-30 04:29:10 +01:00
container parameter types (#184)
fixes https://github.com/psalm/psalm-plugin-symfony/issues/177
This commit is contained in:
parent
ce6bb2995b
commit
4fd391d445
18
phpunit.xml
18
phpunit.xml
@ -1,21 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||||
bootstrap="vendor/autoload.php"
|
bootstrap="vendor/autoload.php"
|
||||||
forceCoversAnnotation="true"
|
forceCoversAnnotation="false"
|
||||||
beStrictAboutCoversAnnotation="true"
|
beStrictAboutCoversAnnotation="true"
|
||||||
beStrictAboutOutputDuringTests="true"
|
beStrictAboutOutputDuringTests="true"
|
||||||
beStrictAboutTodoAnnotatedTests="true"
|
beStrictAboutTodoAnnotatedTests="true"
|
||||||
verbose="true">
|
verbose="true"
|
||||||
|
>
|
||||||
|
<coverage processUncoveredFiles="true">
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</coverage>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="default">
|
<testsuite name="default">
|
||||||
<directory suffix="Test.php">tests/unit</directory>
|
<directory suffix="Test.php">tests/unit</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
<filter>
|
|
||||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
|
||||||
<directory suffix=".php">src</directory>
|
|
||||||
</whitelist>
|
|
||||||
</filter>
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<files psalm-version="3.17.1@8f211792d813e4dc89f04ed372785ce93b902fd1">
|
<files psalm-version="4.7.3@38c452ae584467e939d55377aaf83b5a26f19dd1">
|
||||||
<file src="src/Twig/Context.php">
|
<file src="src/Twig/Context.php">
|
||||||
<UnnecessaryVarAnnotation occurrences="2">
|
<UnnecessaryVarAnnotation occurrences="2">
|
||||||
<code>int</code>
|
<code>int</code>
|
||||||
@ -11,4 +11,9 @@
|
|||||||
<code>NonInvariantDocblockPropertyType</code>
|
<code>NonInvariantDocblockPropertyType</code>
|
||||||
</UnusedPsalmSuppress>
|
</UnusedPsalmSuppress>
|
||||||
</file>
|
</file>
|
||||||
|
<file src="src/Symfony/ContainerMeta.php">
|
||||||
|
<PossiblyNullReference occurrences="1">
|
||||||
|
<code>attributes</code>
|
||||||
|
</PossiblyNullReference>
|
||||||
|
</file>
|
||||||
</files>
|
</files>
|
||||||
|
67
src/Handler/ParameterBagHandler.php
Normal file
67
src/Handler/ParameterBagHandler.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Psalm\SymfonyPsalmPlugin\Handler;
|
||||||
|
|
||||||
|
use PhpParser\Node\Expr;
|
||||||
|
use PhpParser\Node\Scalar\String_;
|
||||||
|
use Psalm\Codebase;
|
||||||
|
use Psalm\Context;
|
||||||
|
use Psalm\Plugin\Hook\AfterMethodCallAnalysisInterface;
|
||||||
|
use Psalm\StatementsSource;
|
||||||
|
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
||||||
|
use Psalm\Type\Atomic;
|
||||||
|
use Psalm\Type\Union;
|
||||||
|
|
||||||
|
class ParameterBagHandler implements AfterMethodCallAnalysisInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ContainerMeta|null
|
||||||
|
*/
|
||||||
|
private static $containerMeta;
|
||||||
|
|
||||||
|
public static function init(ContainerMeta $containerMeta): void
|
||||||
|
{
|
||||||
|
self::$containerMeta = $containerMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function afterMethodCallAnalysis(
|
||||||
|
Expr $expr,
|
||||||
|
string $method_id,
|
||||||
|
string $appearing_method_id,
|
||||||
|
string $declaring_method_id,
|
||||||
|
Context $context,
|
||||||
|
StatementsSource $statements_source,
|
||||||
|
Codebase $codebase,
|
||||||
|
array &$file_replacements = [],
|
||||||
|
Union &$return_type_candidate = null
|
||||||
|
): void {
|
||||||
|
if (!self::$containerMeta || 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::get' !== $declaring_method_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($expr->args[0]->value) || !($expr->args[0]->value instanceof String_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$argument = $expr->args[0]->value->value;
|
||||||
|
|
||||||
|
// @todo find a better way to calculate return type
|
||||||
|
switch (gettype(self::$containerMeta->getParameter($argument))) {
|
||||||
|
case 'string':
|
||||||
|
$return_type_candidate = new Union([Atomic::create('string')]);
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
$return_type_candidate = new Union([Atomic::create('bool')]);
|
||||||
|
break;
|
||||||
|
case 'integer':
|
||||||
|
$return_type_candidate = new Union([Atomic::create('integer')]);
|
||||||
|
break;
|
||||||
|
case 'double':
|
||||||
|
$return_type_candidate = new Union([Atomic::create('float')]);
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
$return_type_candidate = new Union([Atomic::create('array')]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ use Psalm\SymfonyPsalmPlugin\Handler\ContainerHandler;
|
|||||||
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineQueryBuilderHandler;
|
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineQueryBuilderHandler;
|
||||||
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineRepositoryHandler;
|
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineRepositoryHandler;
|
||||||
use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler;
|
use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler;
|
||||||
|
use Psalm\SymfonyPsalmPlugin\Handler\ParameterBagHandler;
|
||||||
use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler;
|
use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler;
|
||||||
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
||||||
use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter;
|
use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter;
|
||||||
@ -84,7 +85,12 @@ class Plugin implements PluginEntryPointInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($config->containerXml)) {
|
if (isset($config->containerXml)) {
|
||||||
ContainerHandler::init(new ContainerMeta((array) $config->containerXml));
|
$containerMeta = new ContainerMeta((array) $config->containerXml);
|
||||||
|
ContainerHandler::init($containerMeta);
|
||||||
|
|
||||||
|
require_once __DIR__.'/Handler/ParameterBagHandler.php';
|
||||||
|
ParameterBagHandler::init($containerMeta);
|
||||||
|
$api->registerHooksFromClass(ParameterBagHandler::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
$api->registerHooksFromClass(ContainerHandler::class);
|
$api->registerHooksFromClass(ContainerHandler::class);
|
||||||
|
@ -19,6 +19,11 @@ class ContainerMeta
|
|||||||
*/
|
*/
|
||||||
private $classNames = [];
|
private $classNames = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, mixed>
|
||||||
|
*/
|
||||||
|
private $parameters = [];
|
||||||
|
|
||||||
public function __construct(array $containerXmlPaths)
|
public function __construct(array $containerXmlPaths)
|
||||||
{
|
{
|
||||||
$this->init($containerXmlPaths);
|
$this->init($containerXmlPaths);
|
||||||
@ -43,6 +48,18 @@ class ContainerMeta
|
|||||||
$this->services[$service->getId()] = $service;
|
$this->services[$service->getId()] = $service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getParameter(string $key)
|
||||||
|
{
|
||||||
|
if (isset($this->parameters[$key])) {
|
||||||
|
return $this->parameters[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string>
|
* @return array<string>
|
||||||
*/
|
*/
|
||||||
@ -90,9 +107,66 @@ class ContainerMeta
|
|||||||
$this->add($service);
|
$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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ConfigException('Container xml file(s) not found at ');
|
throw new ConfigException('Container xml file(s) not found at ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function getXmlParameterValue(\SimpleXMLElement $parameter)
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
75
tests/acceptance/acceptance/ParameterBag.feature
Normal file
75
tests/acceptance/acceptance/ParameterBag.feature
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
@symfony-common
|
||||||
|
Feature: ParameterBag
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I have Symfony plugin enabled with the following config
|
||||||
|
"""
|
||||||
|
<containerXml>../../tests/acceptance/container.xml</containerXml>
|
||||||
|
"""
|
||||||
|
And I have the following code preamble
|
||||||
|
"""
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Asserting psalm recognizes return type of Symfony parameters if container.xml is provided
|
||||||
|
Given I have the following code
|
||||||
|
"""
|
||||||
|
class Foo
|
||||||
|
{
|
||||||
|
public function __invoke(ParameterBagInterface $parameterBag)
|
||||||
|
{
|
||||||
|
/** @psalm-trace $kernelEnvironment */
|
||||||
|
$kernelEnvironment = $parameterBag->get('kernel.environment');
|
||||||
|
|
||||||
|
/** @psalm-trace $debugEnabled */
|
||||||
|
$debugEnabled = $parameterBag->get('debug_enabled');
|
||||||
|
|
||||||
|
/** @psalm-trace $debugDisabled */
|
||||||
|
$debugDisabled = $parameterBag->get('debug_disabled');
|
||||||
|
|
||||||
|
/** @psalm-trace $version */
|
||||||
|
$version = $parameterBag->get('version');
|
||||||
|
|
||||||
|
/** @psalm-trace $integerOne */
|
||||||
|
$integerOne = $parameterBag->get('integer_one');
|
||||||
|
|
||||||
|
/** @psalm-trace $pi */
|
||||||
|
$pi = $parameterBag->get('pi');
|
||||||
|
|
||||||
|
/** @psalm-trace $collection1 */
|
||||||
|
$collection1 = $parameterBag->get('collection1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
When I run Psalm
|
||||||
|
Then I see these errors
|
||||||
|
| Type | Message |
|
||||||
|
| Trace | $kernelEnvironment: string |
|
||||||
|
| Trace | $debugEnabled: bool |
|
||||||
|
| Trace | $debugDisabled: bool |
|
||||||
|
| Trace | $version: string |
|
||||||
|
| Trace | $integerOne: int |
|
||||||
|
| Trace | $pi: float |
|
||||||
|
| Trace | $collection1: array |
|
||||||
|
And I see no other errors
|
||||||
|
|
||||||
|
Scenario: Get non-existent parameter
|
||||||
|
Given I have the following code
|
||||||
|
"""
|
||||||
|
class Foo
|
||||||
|
{
|
||||||
|
public function __invoke(ParameterBagInterface $parameterBag)
|
||||||
|
{
|
||||||
|
/** @psalm-trace $nonExistentParameter */
|
||||||
|
$nonExistentParameter = $parameterBag->get('non_existent_parameter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
When I run Psalm
|
||||||
|
Then I see these errors
|
||||||
|
| Type | Message |
|
||||||
|
| MixedAssignment | Unable to determine the type that $nonExistentParameter is being assigned to |
|
||||||
|
| Trace | $nonExistentParameter: mixed |
|
||||||
|
And I see no other errors
|
@ -2,6 +2,25 @@
|
|||||||
<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">
|
<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>
|
<parameters>
|
||||||
<parameter key="kernel.environment">dev</parameter>
|
<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>
|
||||||
|
</parameter>
|
||||||
</parameters>
|
</parameters>
|
||||||
<services>
|
<services>
|
||||||
<service id="foo" alias="no_such_service" public="true"/>
|
<service id="foo" alias="no_such_service" public="true"/>
|
||||||
|
@ -138,4 +138,27 @@ class ContainerMetaTest extends TestCase
|
|||||||
$service = $containerMeta->get('service_container');
|
$service = $containerMeta->get('service_container');
|
||||||
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClassName());
|
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClassName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGetParameter(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('dev', $this->containerMeta->getParameter('kernel.environment'));
|
||||||
|
$this->assertSame(true, $this->containerMeta->getParameter('debug_enabled'));
|
||||||
|
$this->assertSame('1', $this->containerMeta->getParameter('version'));
|
||||||
|
$this->assertSame(1, $this->containerMeta->getParameter('integer_one'));
|
||||||
|
$this->assertSame(3.14, $this->containerMeta->getParameter('pi'));
|
||||||
|
$this->assertSame([
|
||||||
|
'key1' => 'val1',
|
||||||
|
'key2' => 'val2',
|
||||||
|
], $this->containerMeta->getParameter('collection1'));
|
||||||
|
$this->assertSame([
|
||||||
|
'key' => 'val',
|
||||||
|
'child_collection' => [
|
||||||
|
'boolean' => true,
|
||||||
|
'float' => 2.18,
|
||||||
|
'grandchild_collection' => [
|
||||||
|
'string' => 'something',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
], $this->containerMeta->getParameter('nested_collection'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user