Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d8a0bc0

Browse files
RobertMeondrejmirtes
authored andcommitted
Narrow type for Extension::getConfiguration if class exists
The base extension class automatically creates a Configuration instance when a Configuration class exists in the namespace of the extension. But PHPStan obviously doesn't understand this behaviour and always assumes that `getConfiguration()` returns `ConfigurationInterface|null` meaning that the default pattern to get and parse the configuration reports an error. I.e.: ```php namespace Foo; class SomeExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); } } ``` results in an error because `processConfiguration()` doesn't accept `ConfigurationInterface|null`. But when a `Configuration` class exists in the same namespace as the `Extension` class (so `Foo\Extension`) an instance of it is returned. This `DynamicReturnTypeExtension` overrides the return type of `Extension::getConfiguration()` so it automatically narrows the return type in case `getConfiguration()` is not overriden and a `Configuration` class exists. So that in the given example `getConfiguration()` doesn't return `ConfigurationInterface|null` anymore but `Foo\Configuration` and there is no error on calling `processConfiguration()`.
1 parent ef7db63 commit d8a0bc0

File tree

15 files changed

+322
-0
lines changed

15 files changed

+322
-0
lines changed

‎extension.neon‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,8 @@ services:
346346
-
347347
factory: PHPStan\Type\Symfony\CacheInterfaceGetDynamicReturnTypeExtension
348348
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
349+
350+
# Extension::getConfiguration() return type
351+
-
352+
factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension
353+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\NullType;
12+
use PHPStan\Type\ObjectType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
use function str_contains;
16+
use function strrpos;
17+
use function substr_replace;
18+
19+
class ExtensionGetConfigurationReturnTypeExtension implements DynamicMethodReturnTypeExtension
20+
{
21+
22+
/** @var ReflectionProvider */
23+
private $reflectionProvider;
24+
25+
public function __construct(ReflectionProvider $reflectionProvider)
26+
{
27+
$this->reflectionProvider = $reflectionProvider;
28+
}
29+
30+
public function getClass(): string
31+
{
32+
return 'Symfony\Component\DependencyInjection\Extension\Extension';
33+
}
34+
35+
public function isMethodSupported(MethodReflection $methodReflection): bool
36+
{
37+
return $methodReflection->getName() === 'getConfiguration'
38+
&& $methodReflection->getDeclaringClass()->getName() === 'Symfony\Component\DependencyInjection\Extension\Extension';
39+
}
40+
41+
public function getTypeFromMethodCall(
42+
MethodReflection $methodReflection,
43+
MethodCall $methodCall,
44+
Scope $scope
45+
): ?Type
46+
{
47+
$types = [];
48+
$extensionType = $scope->getType($methodCall->var);
49+
$classes = $extensionType->getObjectClassNames();
50+
51+
foreach ($classes as $extensionName) {
52+
if (str_contains($extensionName, "0円")) {
53+
$types[] = new NullType();
54+
continue;
55+
}
56+
57+
$lastBackslash = strrpos($extensionName, '\\');
58+
if ($lastBackslash === false) {
59+
$types[] = new NullType();
60+
continue;
61+
}
62+
63+
$configurationName = substr_replace($extensionName, '\Configuration', $lastBackslash);
64+
if (!$this->reflectionProvider->hasClass($configurationName)) {
65+
$types[] = new NullType();
66+
continue;
67+
}
68+
69+
$reflection = $this->reflectionProvider->getClass($configurationName);
70+
if ($this->hasRequiredConstructor($reflection)) {
71+
$types[] = new NullType();
72+
continue;
73+
}
74+
75+
$types[] = new ObjectType($configurationName);
76+
}
77+
78+
return TypeCombinator::union(...$types);
79+
}
80+
81+
private function hasRequiredConstructor(ClassReflection $class): bool
82+
{
83+
if (!$class->hasConstructor()) {
84+
return false;
85+
}
86+
87+
$constructor = $class->getConstructor();
88+
foreach ($constructor->getVariants() as $variant) {
89+
$anyRequired = false;
90+
foreach ($variant->getParameters() as $parameter) {
91+
if (!$parameter->isOptional()) {
92+
$anyRequired = true;
93+
break;
94+
}
95+
}
96+
97+
if (!$anyRequired) {
98+
return false;
99+
}
100+
}
101+
102+
return true;
103+
}
104+
105+
}

‎tests/Type/Symfony/ExtensionTest.php‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ public function dataFileAsserts(): iterable
5757
yield from $this->gatherAssertTypes(__DIR__ . '/data/FormInterface_getErrors.php');
5858
yield from $this->gatherAssertTypes(__DIR__ . '/data/cache.php');
5959
yield from $this->gatherAssertTypes(__DIR__ . '/data/form_data_type.php');
60+
61+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration/WithConfigurationExtension.php');
62+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/without-configuration/WithoutConfigurationExtension.php');
63+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/anonymous/AnonymousExtension.php');
64+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/ignore-implemented/IgnoreImplementedExtension.php');
65+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/multiple-types/MultipleTypes.php');
66+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor/WithConfigurationWithConstructorExtension.php');
67+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-optional-params/WithConfigurationWithConstructorOptionalParamsExtension.php');
68+
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-required-params/WithConfigurationWithConstructorRequiredParamsExtension.php');
6069
}
6170

6271
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\Anonymous;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use Symfony\Component\DependencyInjection\Extension\Extension;
7+
8+
new class extends Extension
9+
{
10+
public function load(array $configs, ContainerBuilder $container)
11+
{
12+
\PHPStan\Testing\assertType(
13+
'null',
14+
$this->getConfiguration($configs, $container)
15+
);
16+
}
17+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\IgnoreImplemented;
4+
5+
use Symfony\Component\Config\Definition\ConfigurationInterface;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use \Symfony\Component\DependencyInjection\Extension\Extension;
8+
9+
class IgnoreImplementedExtension extends Extension
10+
{
11+
public function load(array $configs, ContainerBuilder $container): void
12+
{
13+
\PHPStan\Testing\assertType(
14+
'Symfony\Component\Config\Definition\ConfigurationInterface|null',
15+
$this->getConfiguration($configs, $container)
16+
);
17+
}
18+
19+
public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface
20+
{
21+
return null;
22+
}
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\MultipleTypes;
4+
5+
use PHPStan\Type\Symfony\Extension\WithConfiguration\WithConfigurationExtension;
6+
use PHPStan\Type\Symfony\Extension\WithoutConfiguration\WithoutConfigurationExtension;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
9+
/**
10+
* @param WithConfigurationExtension|WithoutConfigurationExtension $extension
11+
*/
12+
function test($extension, array $configs, ContainerBuilder $container)
13+
{
14+
\PHPStan\Testing\assertType(
15+
'PHPStan\Type\Symfony\Extension\WithConfiguration\Configuration|null',
16+
$extension->getConfiguration($configs, $container)
17+
);
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorOptionalParams;
4+
5+
use Symfony\Component\Config\Definition\ConfigurationInterface;
6+
7+
class Configuration implements ConfigurationInterface
8+
{
9+
public function __construct($foo = null)
10+
{
11+
}
12+
13+
public function getConfigTreeBuilder()
14+
{
15+
}
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorOptionalParams;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use \Symfony\Component\DependencyInjection\Extension\Extension;
7+
8+
class WithConfigurationWithConstructorOptionalParamsExtension extends Extension
9+
{
10+
public function load(array $configs, ContainerBuilder $container): void
11+
{
12+
\PHPStan\Testing\assertType(
13+
Configuration::class,
14+
$this->getConfiguration($configs, $container)
15+
);
16+
}
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorRequiredParams;
4+
5+
use Symfony\Component\Config\Definition\ConfigurationInterface;
6+
7+
class Configuration implements ConfigurationInterface
8+
{
9+
public function __construct($foo)
10+
{
11+
}
12+
13+
public function getConfigTreeBuilder()
14+
{
15+
}
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorRequiredParams;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use \Symfony\Component\DependencyInjection\Extension\Extension;
7+
8+
class WithConfigurationWithConstructorRequiredParamsExtension extends Extension
9+
{
10+
public function load(array $configs, ContainerBuilder $container): void
11+
{
12+
\PHPStan\Testing\assertType(
13+
'null',
14+
$this->getConfiguration($configs, $container)
15+
);
16+
}
17+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /