Skip to content

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

+5
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]
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

+9
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
/**
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+
};
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+
}
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+
}
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+
}
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+
}
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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructor;
4+
5+
use Symfony\Component\Config\Definition\ConfigurationInterface;
6+
7+
class Configuration implements ConfigurationInterface
8+
{
9+
public function __construct()
10+
{
11+
}
12+
13+
public function getConfigTreeBuilder()
14+
{
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructor;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use \Symfony\Component\DependencyInjection\Extension\Extension;
7+
8+
class WithConfigurationWithConstructorExtension 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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfiguration;
4+
5+
use Symfony\Component\Config\Definition\ConfigurationInterface;
6+
7+
class Configuration implements ConfigurationInterface
8+
{
9+
public function getConfigTreeBuilder()
10+
{
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithConfiguration;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use \Symfony\Component\DependencyInjection\Extension\Extension;
7+
8+
class WithConfigurationExtension 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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\Type\Symfony\Extension\WithoutConfiguration;
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
use \Symfony\Component\DependencyInjection\Extension\Extension;
7+
8+
class WithoutConfigurationExtension 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)