Skip to content

Commit 4fd391d

Browse files
authored
container parameter types (#184)
fixes #177
1 parent ce6bb29 commit 4fd391d

File tree

8 files changed

+285
-16
lines changed

8 files changed

+285
-16
lines changed

phpunit.xml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
44
bootstrap="vendor/autoload.php"
5-
forceCoversAnnotation="true"
5+
forceCoversAnnotation="false"
66
beStrictAboutCoversAnnotation="true"
77
beStrictAboutOutputDuringTests="true"
88
beStrictAboutTodoAnnotatedTests="true"
9-
verbose="true">
10-
<testsuites>
11-
<testsuite name="default">
12-
<directory suffix="Test.php">tests/unit</directory>
13-
</testsuite>
14-
</testsuites>
15-
16-
<filter>
17-
<whitelist processUncoveredFilesFromWhitelist="true">
18-
<directory suffix=".php">src</directory>
19-
</whitelist>
20-
</filter>
9+
verbose="true"
10+
>
11+
<coverage processUncoveredFiles="true">
12+
<include>
13+
<directory suffix=".php">src</directory>
14+
</include>
15+
</coverage>
16+
<testsuites>
17+
<testsuite name="default">
18+
<directory suffix="Test.php">tests/unit</directory>
19+
</testsuite>
20+
</testsuites>
2121
</phpunit>

psalm-baseline.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="3.17.1@8f211792d813e4dc89f04ed372785ce93b902fd1">
2+
<files psalm-version="4.7.3@38c452ae584467e939d55377aaf83b5a26f19dd1">
33
<file src="src/Twig/Context.php">
44
<UnnecessaryVarAnnotation occurrences="2">
55
<code>int</code>
@@ -11,4 +11,9 @@
1111
<code>NonInvariantDocblockPropertyType</code>
1212
</UnusedPsalmSuppress>
1313
</file>
14+
<file src="src/Symfony/ContainerMeta.php">
15+
<PossiblyNullReference occurrences="1">
16+
<code>attributes</code>
17+
</PossiblyNullReference>
18+
</file>
1419
</files>

src/Handler/ParameterBagHandler.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Psalm\SymfonyPsalmPlugin\Handler;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Scalar\String_;
7+
use Psalm\Codebase;
8+
use Psalm\Context;
9+
use Psalm\Plugin\Hook\AfterMethodCallAnalysisInterface;
10+
use Psalm\StatementsSource;
11+
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
12+
use Psalm\Type\Atomic;
13+
use Psalm\Type\Union;
14+
15+
class ParameterBagHandler implements AfterMethodCallAnalysisInterface
16+
{
17+
/**
18+
* @var ContainerMeta|null
19+
*/
20+
private static $containerMeta;
21+
22+
public static function init(ContainerMeta $containerMeta): void
23+
{
24+
self::$containerMeta = $containerMeta;
25+
}
26+
27+
public static function afterMethodCallAnalysis(
28+
Expr $expr,
29+
string $method_id,
30+
string $appearing_method_id,
31+
string $declaring_method_id,
32+
Context $context,
33+
StatementsSource $statements_source,
34+
Codebase $codebase,
35+
array &$file_replacements = [],
36+
Union &$return_type_candidate = null
37+
): void {
38+
if (!self::$containerMeta || 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::get' !== $declaring_method_id) {
39+
return;
40+
}
41+
42+
if (!isset($expr->args[0]->value) || !($expr->args[0]->value instanceof String_)) {
43+
return;
44+
}
45+
46+
$argument = $expr->args[0]->value->value;
47+
48+
// @todo find a better way to calculate return type
49+
switch (gettype(self::$containerMeta->getParameter($argument))) {
50+
case 'string':
51+
$return_type_candidate = new Union([Atomic::create('string')]);
52+
break;
53+
case 'boolean':
54+
$return_type_candidate = new Union([Atomic::create('bool')]);
55+
break;
56+
case 'integer':
57+
$return_type_candidate = new Union([Atomic::create('integer')]);
58+
break;
59+
case 'double':
60+
$return_type_candidate = new Union([Atomic::create('float')]);
61+
break;
62+
case 'array':
63+
$return_type_candidate = new Union([Atomic::create('array')]);
64+
break;
65+
}
66+
}
67+
}

src/Plugin.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineQueryBuilderHandler;
1515
use Psalm\SymfonyPsalmPlugin\Handler\DoctrineRepositoryHandler;
1616
use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler;
17+
use Psalm\SymfonyPsalmPlugin\Handler\ParameterBagHandler;
1718
use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler;
1819
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
1920
use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter;
@@ -84,7 +85,12 @@ public function __invoke(RegistrationInterface $api, SimpleXMLElement $config =
8485
}
8586

8687
if (isset($config->containerXml)) {
87-
ContainerHandler::init(new ContainerMeta((array) $config->containerXml));
88+
$containerMeta = new ContainerMeta((array) $config->containerXml);
89+
ContainerHandler::init($containerMeta);
90+
91+
require_once __DIR__.'/Handler/ParameterBagHandler.php';
92+
ParameterBagHandler::init($containerMeta);
93+
$api->registerHooksFromClass(ParameterBagHandler::class);
8894
}
8995

9096
$api->registerHooksFromClass(ContainerHandler::class);

src/Symfony/ContainerMeta.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ class ContainerMeta
1919
*/
2020
private $classNames = [];
2121

22+
/**
23+
* @var array<string, mixed>
24+
*/
25+
private $parameters = [];
26+
2227
public function __construct(array $containerXmlPaths)
2328
{
2429
$this->init($containerXmlPaths);
@@ -43,6 +48,18 @@ public function add(Service $service): void
4348
$this->services[$service->getId()] = $service;
4449
}
4550

51+
/**
52+
* @return mixed|null
53+
*/
54+
public function getParameter(string $key)
55+
{
56+
if (isset($this->parameters[$key])) {
57+
return $this->parameters[$key];
58+
}
59+
60+
return null;
61+
}
62+
4663
/**
4764
* @return array<string>
4865
*/
@@ -90,9 +107,66 @@ private function init(array $containerXmlPaths): void
90107
$this->add($service);
91108
}
92109

110+
/** @var \SimpleXMLElement $parameter */
111+
foreach ($xml->parameters->parameter as $parameter) {
112+
$value = $this->getXmlParameterValue($parameter);
113+
114+
$attributes = $parameter->attributes();
115+
if (!isset($attributes->key)) {
116+
continue;
117+
}
118+
119+
$this->parameters[(string) $attributes->key] = $value;
120+
}
121+
93122
return;
94123
}
95124

96125
throw new ConfigException('Container xml file(s) not found at ');
97126
}
127+
128+
/**
129+
* @return mixed
130+
*/
131+
private function getXmlParameterValue(\SimpleXMLElement $parameter)
132+
{
133+
$value = null;
134+
$attributes = $parameter->attributes();
135+
if (isset($attributes->type)) {
136+
switch ((string) $attributes->type) {
137+
case 'binary':
138+
$value = base64_decode((string) $parameter, true);
139+
break;
140+
case 'collection':
141+
foreach ($parameter->children() as $child) {
142+
$childAttributes = $child->attributes();
143+
if (isset($childAttributes->key)) {
144+
$value[(string) $childAttributes->key] = $this->getXmlParameterValue($child);
145+
} else {
146+
$value[] = $this->getXmlParameterValue($child);
147+
}
148+
}
149+
break;
150+
case 'string':
151+
default:
152+
$value = (string) $parameter;
153+
break;
154+
}
155+
} else {
156+
$value = (string) $parameter;
157+
if ('true' === $value || 'false' === $value) {
158+
$value = (bool) $value;
159+
} elseif ('null' === $value) {
160+
$value = null;
161+
} elseif (is_numeric($value)) {
162+
if (false === strpos($value, '.')) {
163+
$value = (int) $value;
164+
} else {
165+
$value = (float) $value;
166+
}
167+
}
168+
}
169+
170+
return $value;
171+
}
98172
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@symfony-common
2+
Feature: ParameterBag
3+
4+
Background:
5+
Given I have Symfony plugin enabled with the following config
6+
"""
7+
<containerXml>../../tests/acceptance/container.xml</containerXml>
8+
"""
9+
And I have the following code preamble
10+
"""
11+
<?php
12+
13+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
14+
"""
15+
16+
Scenario: Asserting psalm recognizes return type of Symfony parameters if container.xml is provided
17+
Given I have the following code
18+
"""
19+
class Foo
20+
{
21+
public function __invoke(ParameterBagInterface $parameterBag)
22+
{
23+
/** @psalm-trace $kernelEnvironment */
24+
$kernelEnvironment = $parameterBag->get('kernel.environment');
25+
26+
/** @psalm-trace $debugEnabled */
27+
$debugEnabled = $parameterBag->get('debug_enabled');
28+
29+
/** @psalm-trace $debugDisabled */
30+
$debugDisabled = $parameterBag->get('debug_disabled');
31+
32+
/** @psalm-trace $version */
33+
$version = $parameterBag->get('version');
34+
35+
/** @psalm-trace $integerOne */
36+
$integerOne = $parameterBag->get('integer_one');
37+
38+
/** @psalm-trace $pi */
39+
$pi = $parameterBag->get('pi');
40+
41+
/** @psalm-trace $collection1 */
42+
$collection1 = $parameterBag->get('collection1');
43+
}
44+
}
45+
"""
46+
When I run Psalm
47+
Then I see these errors
48+
| Type | Message |
49+
| Trace | $kernelEnvironment: string |
50+
| Trace | $debugEnabled: bool |
51+
| Trace | $debugDisabled: bool |
52+
| Trace | $version: string |
53+
| Trace | $integerOne: int |
54+
| Trace | $pi: float |
55+
| Trace | $collection1: array |
56+
And I see no other errors
57+
58+
Scenario: Get non-existent parameter
59+
Given I have the following code
60+
"""
61+
class Foo
62+
{
63+
public function __invoke(ParameterBagInterface $parameterBag)
64+
{
65+
/** @psalm-trace $nonExistentParameter */
66+
$nonExistentParameter = $parameterBag->get('non_existent_parameter');
67+
}
68+
}
69+
"""
70+
When I run Psalm
71+
Then I see these errors
72+
| Type | Message |
73+
| MixedAssignment | Unable to determine the type that $nonExistentParameter is being assigned to |
74+
| Trace | $nonExistentParameter: mixed |
75+
And I see no other errors

tests/acceptance/container.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
<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">
33
<parameters>
44
<parameter key="kernel.environment">dev</parameter>
5+
<parameter key="debug_enabled">true</parameter>
6+
<parameter key="debug_disabled">false</parameter>
7+
<parameter key="version" type="string">1</parameter>
8+
<parameter key="integer_one">1</parameter>
9+
<parameter key="pi">3.14</parameter>
10+
<parameter key="collection1" type="collection">
11+
<parameter key="key1">val1</parameter>
12+
<parameter key="key2">val2</parameter>
13+
</parameter>
14+
<parameter key="nested_collection" type="collection">
15+
<parameter key="key">val</parameter>
16+
<parameter key="child_collection" type="collection">
17+
<parameter key="boolean">true</parameter>
18+
<parameter key="float">2.18</parameter>
19+
<parameter key="grandchild_collection" type="collection">
20+
<parameter key="string">something</parameter>
21+
</parameter>
22+
</parameter>
23+
</parameter>
524
</parameters>
625
<services>
726
<service id="foo" alias="no_such_service" public="true"/>

tests/unit/Symfony/ContainerMetaTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,27 @@ public function testBothValidAndInvalidArray()
138138
$service = $containerMeta->get('service_container');
139139
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClassName());
140140
}
141+
142+
public function testGetParameter(): void
143+
{
144+
$this->assertSame('dev', $this->containerMeta->getParameter('kernel.environment'));
145+
$this->assertSame(true, $this->containerMeta->getParameter('debug_enabled'));
146+
$this->assertSame('1', $this->containerMeta->getParameter('version'));
147+
$this->assertSame(1, $this->containerMeta->getParameter('integer_one'));
148+
$this->assertSame(3.14, $this->containerMeta->getParameter('pi'));
149+
$this->assertSame([
150+
'key1' => 'val1',
151+
'key2' => 'val2',
152+
], $this->containerMeta->getParameter('collection1'));
153+
$this->assertSame([
154+
'key' => 'val',
155+
'child_collection' => [
156+
'boolean' => true,
157+
'float' => 2.18,
158+
'grandchild_collection' => [
159+
'string' => 'something',
160+
],
161+
]
162+
], $this->containerMeta->getParameter('nested_collection'));
163+
}
141164
}

0 commit comments

Comments
 (0)