Skip to content

Commit c9f5ceb

Browse files
committed
Merge remote-tracking branch 'origin/1.3.x' into 1.4.x
2 parents 67e208d + d8a0bc0 commit c9f5ceb

22 files changed

+372
-2
lines changed

Diff for: extension.neon

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ parameters:
1414
featureToggles:
1515
skipCheckGenericClasses:
1616
- Symfony\Component\Form\AbstractType
17+
- Symfony\Component\Form\FormBuilderInterface
18+
- Symfony\Component\Form\FormConfigBuilderInterface
19+
- Symfony\Component\Form\FormConfigInterface
1720
- Symfony\Component\Form\FormInterface
1821
- Symfony\Component\Form\FormTypeExtensionInterface
1922
- Symfony\Component\Form\FormTypeInterface
@@ -47,6 +50,8 @@ parameters:
4750
- stubs/Symfony/Component/Form/Exception/TransformationFailedException.stub
4851
- stubs/Symfony/Component/Form/DataTransformerInterface.stub
4952
- stubs/Symfony/Component/Form/FormBuilderInterface.stub
53+
- stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub
54+
- stubs/Symfony/Component/Form/FormConfigInterface.stub
5055
- stubs/Symfony/Component/Form/FormInterface.stub
5156
- stubs/Symfony/Component/Form/FormFactoryInterface.stub
5257
- stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
@@ -341,3 +346,8 @@ services:
341346
-
342347
factory: PHPStan\Type\Symfony\CacheInterfaceGetDynamicReturnTypeExtension
343348
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+
}

Diff for: stubs/Symfony/Component/Form/AbstractType.stub

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ abstract class AbstractType implements FormTypeInterface
1111
{
1212

1313
/**
14+
* @param FormBuilderInterface<TData|null> $builder
1415
* @param array<string, mixed> $options
1516
*/
1617
public function buildForm(FormBuilderInterface $builder, array $options): void;

Diff for: stubs/Symfony/Component/Form/FormBuilderInterface.stub

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
namespace Symfony\Component\Form;
44

55
/**
6-
* @extends \Traversable<int, \Symfony\Component\Form\FormBuilderInterface>
6+
* @template TData
7+
*
8+
* @extends \Traversable<int, \Symfony\Component\Form\FormBuilderInterface<mixed>>
9+
* @extends FormConfigBuilderInterface<TData>
710
*/
8-
interface FormBuilderInterface extends \Traversable
11+
interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface
912
{
1013

14+
/**
15+
* @return FormInterface<TData|null>
16+
*/
17+
public function getForm(): FormInterface;
18+
1119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Form;
4+
5+
/**
6+
* @template TData
7+
*
8+
* @extends FormConfigInterface<TData>
9+
*/
10+
interface FormConfigBuilderInterface extends FormConfigInterface
11+
{
12+
13+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Symfony\Component\Form;
4+
5+
/**
6+
* @template TData
7+
*/
8+
interface FormConfigInterface
9+
{
10+
11+
/**
12+
* @return TData
13+
*/
14+
public function getData(): mixed;
15+
16+
}

Diff for: stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Symfony\Component\Form;
88
interface FormTypeExtensionInterface
99
{
1010
/**
11+
* @param FormBuilderInterface<TData|null> $builder
1112
* @param array<string, mixed> $options
1213
*/
1314
public function buildForm(FormBuilderInterface $builder, array $options): void;

Diff for: stubs/Symfony/Component/Form/FormTypeInterface.stub

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Symfony\Component\Form;
88
interface FormTypeInterface
99
{
1010
/**
11+
* @param FormBuilderInterface<TData|null> $builder
1112
* @param array<string, mixed> $options
1213
*/
1314
public function buildForm(FormBuilderInterface $builder, array $options): void;

Diff for: 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+
}

0 commit comments

Comments
 (0)