mirror of
https://github.com/danog/psalm-plugin-symfony.git
synced 2024-11-26 11:55:00 +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
28
phpunit.xml
28
phpunit.xml
@ -1,21 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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"
|
||||
forceCoversAnnotation="true"
|
||||
forceCoversAnnotation="false"
|
||||
beStrictAboutCoversAnnotation="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
beStrictAboutTodoAnnotatedTests="true"
|
||||
verbose="true">
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory suffix="Test.php">tests/unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
verbose="true"
|
||||
>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory suffix="Test.php">tests/unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?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">
|
||||
<UnnecessaryVarAnnotation occurrences="2">
|
||||
<code>int</code>
|
||||
@ -11,4 +11,9 @@
|
||||
<code>NonInvariantDocblockPropertyType</code>
|
||||
</UnusedPsalmSuppress>
|
||||
</file>
|
||||
<file src="src/Symfony/ContainerMeta.php">
|
||||
<PossiblyNullReference occurrences="1">
|
||||
<code>attributes</code>
|
||||
</PossiblyNullReference>
|
||||
</file>
|
||||
</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\DoctrineRepositoryHandler;
|
||||
use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler;
|
||||
use Psalm\SymfonyPsalmPlugin\Handler\ParameterBagHandler;
|
||||
use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler;
|
||||
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
|
||||
use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter;
|
||||
@ -84,7 +85,12 @@ class Plugin implements PluginEntryPointInterface
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -19,6 +19,11 @@ class ContainerMeta
|
||||
*/
|
||||
private $classNames = [];
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $parameters = [];
|
||||
|
||||
public function __construct(array $containerXmlPaths)
|
||||
{
|
||||
$this->init($containerXmlPaths);
|
||||
@ -43,6 +48,18 @@ class ContainerMeta
|
||||
$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>
|
||||
*/
|
||||
@ -90,9 +107,66 @@ class ContainerMeta
|
||||
$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 ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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">
|
||||
<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>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<services>
|
||||
<service id="foo" alias="no_such_service" public="true"/>
|
||||
|
@ -138,4 +138,27 @@ class ContainerMetaTest extends TestCase
|
||||
$service = $containerMeta->get('service_container');
|
||||
$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