Skip to content

Commit

Permalink
Narrow type for Extension::getConfiguration if class exists
Browse files Browse the repository at this point in the history
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
RobertMe authored and ondrejmirtes committed Mar 5, 2024
1 parent ef7db63 commit d8a0bc0
Show file tree
Hide file tree
Showing 15 changed files with 322 additions and 0 deletions.
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,8 @@ services:
-
factory: PHPStan\Type\Symfony\CacheInterfaceGetDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

# Extension::getConfiguration() return type
-
factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
105 changes: 105 additions & 0 deletions src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php
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;
}

}
9 changes: 9 additions & 0 deletions tests/Type/Symfony/ExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/FormInterface_getErrors.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/cache.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/form_data_type.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration/WithConfigurationExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/without-configuration/WithoutConfigurationExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/anonymous/AnonymousExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/ignore-implemented/IgnoreImplementedExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/multiple-types/MultipleTypes.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor/WithConfigurationWithConstructorExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-optional-params/WithConfigurationWithConstructorOptionalParamsExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-required-params/WithConfigurationWithConstructorRequiredParamsExtension.php');
}

/**
Expand Down
17 changes: 17 additions & 0 deletions tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php
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)
);
}
};
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 tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php
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)
);
}
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()
{
}
}
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)
);
}
}
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()
{
}
}
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)
);
}
}
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()
{
}
}
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)
);
}
}
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()
{
}
}
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)
);
}
}
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)
);
}
}

0 comments on commit d8a0bc0

Please sign in to comment.