-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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()`.
- Loading branch information
1 parent
ef7db63
commit d8a0bc0
Showing
15 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Type\Symfony; | ||
|
||
use PhpParser\Node\Expr\MethodCall; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Reflection\MethodReflection; | ||
use PHPStan\Reflection\ReflectionProvider; | ||
use PHPStan\Type\DynamicMethodReturnTypeExtension; | ||
use PHPStan\Type\NullType; | ||
use PHPStan\Type\ObjectType; | ||
use PHPStan\Type\Type; | ||
use PHPStan\Type\TypeCombinator; | ||
use function str_contains; | ||
use function strrpos; | ||
use function substr_replace; | ||
|
||
class ExtensionGetConfigurationReturnTypeExtension implements DynamicMethodReturnTypeExtension | ||
{ | ||
|
||
/** @var ReflectionProvider */ | ||
private $reflectionProvider; | ||
|
||
public function __construct(ReflectionProvider $reflectionProvider) | ||
{ | ||
$this->reflectionProvider = $reflectionProvider; | ||
} | ||
|
||
public function getClass(): string | ||
{ | ||
return 'Symfony\Component\DependencyInjection\Extension\Extension'; | ||
} | ||
|
||
public function isMethodSupported(MethodReflection $methodReflection): bool | ||
{ | ||
return $methodReflection->getName() === 'getConfiguration' | ||
&& $methodReflection->getDeclaringClass()->getName() === 'Symfony\Component\DependencyInjection\Extension\Extension'; | ||
} | ||
|
||
public function getTypeFromMethodCall( | ||
MethodReflection $methodReflection, | ||
MethodCall $methodCall, | ||
Scope $scope | ||
): ?Type | ||
{ | ||
$types = []; | ||
$extensionType = $scope->getType($methodCall->var); | ||
$classes = $extensionType->getObjectClassNames(); | ||
|
||
foreach ($classes as $extensionName) { | ||
if (str_contains($extensionName, "\0")) { | ||
$types[] = new NullType(); | ||
continue; | ||
} | ||
|
||
$lastBackslash = strrpos($extensionName, '\\'); | ||
if ($lastBackslash === false) { | ||
$types[] = new NullType(); | ||
continue; | ||
} | ||
|
||
$configurationName = substr_replace($extensionName, '\Configuration', $lastBackslash); | ||
if (!$this->reflectionProvider->hasClass($configurationName)) { | ||
$types[] = new NullType(); | ||
continue; | ||
} | ||
|
||
$reflection = $this->reflectionProvider->getClass($configurationName); | ||
if ($this->hasRequiredConstructor($reflection)) { | ||
$types[] = new NullType(); | ||
continue; | ||
} | ||
|
||
$types[] = new ObjectType($configurationName); | ||
} | ||
|
||
return TypeCombinator::union(...$types); | ||
} | ||
|
||
private function hasRequiredConstructor(ClassReflection $class): bool | ||
{ | ||
if (!$class->hasConstructor()) { | ||
return false; | ||
} | ||
|
||
$constructor = $class->getConstructor(); | ||
foreach ($constructor->getVariants() as $variant) { | ||
$anyRequired = false; | ||
foreach ($variant->getParameters() as $parameter) { | ||
if (!$parameter->isOptional()) { | ||
$anyRequired = true; | ||
break; | ||
} | ||
} | ||
|
||
if (!$anyRequired) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\Anonymous; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
new class extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container) | ||
{ | ||
\PHPStan\Testing\assertType( | ||
'null', | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
}; |
23 changes: 23 additions & 0 deletions
23
tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\IgnoreImplemented; | ||
|
||
use Symfony\Component\Config\Definition\ConfigurationInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use \Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
class IgnoreImplementedExtension extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
\PHPStan\Testing\assertType( | ||
'Symfony\Component\Config\Definition\ConfigurationInterface|null', | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
|
||
public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface | ||
{ | ||
return null; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\MultipleTypes; | ||
|
||
use PHPStan\Type\Symfony\Extension\WithConfiguration\WithConfigurationExtension; | ||
use PHPStan\Type\Symfony\Extension\WithoutConfiguration\WithoutConfigurationExtension; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
|
||
/** | ||
* @param WithConfigurationExtension|WithoutConfigurationExtension $extension | ||
*/ | ||
function test($extension, array $configs, ContainerBuilder $container) | ||
{ | ||
\PHPStan\Testing\assertType( | ||
'PHPStan\Type\Symfony\Extension\WithConfiguration\Configuration|null', | ||
$extension->getConfiguration($configs, $container) | ||
); | ||
} |
16 changes: 16 additions & 0 deletions
16
...fony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorOptionalParams; | ||
|
||
use Symfony\Component\Config\Definition\ConfigurationInterface; | ||
|
||
class Configuration implements ConfigurationInterface | ||
{ | ||
public function __construct($foo = null) | ||
{ | ||
} | ||
|
||
public function getConfigTreeBuilder() | ||
{ | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...h-constructor-optional-params/WithConfigurationWithConstructorOptionalParamsExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorOptionalParams; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use \Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
class WithConfigurationWithConstructorOptionalParamsExtension extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
\PHPStan\Testing\assertType( | ||
Configuration::class, | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...fony/data/extension/with-configuration-with-constructor-required-params/Configuration.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorRequiredParams; | ||
|
||
use Symfony\Component\Config\Definition\ConfigurationInterface; | ||
|
||
class Configuration implements ConfigurationInterface | ||
{ | ||
public function __construct($foo) | ||
{ | ||
} | ||
|
||
public function getConfigTreeBuilder() | ||
{ | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...h-constructor-required-params/WithConfigurationWithConstructorRequiredParamsExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructorRequiredParams; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use \Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
class WithConfigurationWithConstructorRequiredParamsExtension extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
\PHPStan\Testing\assertType( | ||
'null', | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructor; | ||
|
||
use Symfony\Component\Config\Definition\ConfigurationInterface; | ||
|
||
class Configuration implements ConfigurationInterface | ||
{ | ||
public function __construct() | ||
{ | ||
} | ||
|
||
public function getConfigTreeBuilder() | ||
{ | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...tension/with-configuration-with-constructor/WithConfigurationWithConstructorExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfigurationWithConstructor; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use \Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
class WithConfigurationWithConstructorExtension extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
\PHPStan\Testing\assertType( | ||
Configuration::class, | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
tests/Type/Symfony/data/extension/with-configuration/Configuration.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfiguration; | ||
|
||
use Symfony\Component\Config\Definition\ConfigurationInterface; | ||
|
||
class Configuration implements ConfigurationInterface | ||
{ | ||
public function getConfigTreeBuilder() | ||
{ | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
tests/Type/Symfony/data/extension/with-configuration/WithConfigurationExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithConfiguration; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use \Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
class WithConfigurationExtension extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
\PHPStan\Testing\assertType( | ||
Configuration::class, | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace PHPStan\Type\Symfony\Extension\WithoutConfiguration; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use \Symfony\Component\DependencyInjection\Extension\Extension; | ||
|
||
class WithoutConfigurationExtension extends Extension | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
\PHPStan\Testing\assertType( | ||
'null', | ||
$this->getConfiguration($configs, $container) | ||
); | ||
} | ||
} |