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 577564f

Browse files
committed
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 577564f

File tree

9 files changed

+182
-0
lines changed

9 files changed

+182
-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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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\MethodReflection;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
10+
use PHPStan\Type\NullType;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use function count;
14+
use function str_contains;
15+
use function strrpos;
16+
use function substr_replace;
17+
18+
class ExtensionGetConfigurationReturnTypeExtension implements DynamicMethodReturnTypeExtension
19+
{
20+
21+
/** @var ReflectionProvider */
22+
private $reflectionProvider;
23+
24+
public function __construct(ReflectionProvider $reflectionProvider)
25+
{
26+
$this->reflectionProvider = $reflectionProvider;
27+
}
28+
29+
public function getClass(): string
30+
{
31+
return 'Symfony\Component\DependencyInjection\Extension\Extension';
32+
}
33+
34+
public function isMethodSupported(MethodReflection $methodReflection): bool
35+
{
36+
return $methodReflection->getName() === 'getConfiguration'
37+
&& $methodReflection->getDeclaringClass()->getName() === 'Symfony\Component\DependencyInjection\Extension\Extension';
38+
}
39+
40+
public function getTypeFromMethodCall(
41+
MethodReflection $methodReflection,
42+
MethodCall $methodCall,
43+
Scope $scope
44+
): ?Type
45+
{
46+
$extensionType = $scope->getType($methodCall->var);
47+
$classes = $extensionType->getObjectClassNames();
48+
if (count($classes) !== 1) {
49+
return null;
50+
}
51+
52+
$extensionName = $classes[0];
53+
if (str_contains($extensionName, "0円")) {
54+
return new NullType();
55+
}
56+
57+
$configurationName = substr_replace($extensionName, '\Configuration', strrpos($extensionName, '\\'));
58+
if (!$this->reflectionProvider->hasClass($configurationName)) {
59+
return new NullType();
60+
}
61+
62+
// TODO: return NullType if configuration class has constructor with required arguments
63+
64+
return new ObjectType($configurationName);
65+
}
66+
67+
}

‎tests/Type/Symfony/ExtensionTest.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ 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');
6066
}
6167

6268
/**
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+
'Symfony\Component\Config\Definition\ConfigurationInterface|null',
16+
$extension->getConfiguration($configs, $container)
17+
);
18+
}
Lines changed: 12 additions & 0 deletions
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+
}
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\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+
'PHPStan\Type\Symfony\Extension\WithConfiguration\Configuration',
14+
$this->getConfiguration($configs, $container)
15+
);
16+
}
17+
}
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\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)

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