Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/1.3.x' into 1.4.x
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Mar 5, 2024
2 parents 67e208d + d8a0bc0 commit c9f5ceb
Show file tree
Hide file tree
Showing 22 changed files with 372 additions and 2 deletions.
10 changes: 10 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ parameters:
featureToggles:
skipCheckGenericClasses:
- Symfony\Component\Form\AbstractType
- Symfony\Component\Form\FormBuilderInterface
- Symfony\Component\Form\FormConfigBuilderInterface
- Symfony\Component\Form\FormConfigInterface
- Symfony\Component\Form\FormInterface
- Symfony\Component\Form\FormTypeExtensionInterface
- Symfony\Component\Form\FormTypeInterface
Expand Down Expand Up @@ -47,6 +50,8 @@ parameters:
- stubs/Symfony/Component/Form/Exception/TransformationFailedException.stub
- stubs/Symfony/Component/Form/DataTransformerInterface.stub
- stubs/Symfony/Component/Form/FormBuilderInterface.stub
- stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub
- stubs/Symfony/Component/Form/FormConfigInterface.stub
- stubs/Symfony/Component/Form/FormInterface.stub
- stubs/Symfony/Component/Form/FormFactoryInterface.stub
- stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
Expand Down Expand Up @@ -341,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;
}

}
1 change: 1 addition & 0 deletions stubs/Symfony/Component/Form/AbstractType.stub
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ abstract class AbstractType implements FormTypeInterface
{

/**
* @param FormBuilderInterface<TData|null> $builder
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;
Expand Down
12 changes: 10 additions & 2 deletions stubs/Symfony/Component/Form/FormBuilderInterface.stub
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@
namespace Symfony\Component\Form;

/**
* @extends \Traversable<int, \Symfony\Component\Form\FormBuilderInterface>
* @template TData
*
* @extends \Traversable<int, \Symfony\Component\Form\FormBuilderInterface<mixed>>
* @extends FormConfigBuilderInterface<TData>
*/
interface FormBuilderInterface extends \Traversable
interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface
{

/**
* @return FormInterface<TData|null>
*/
public function getForm(): FormInterface;

}
13 changes: 13 additions & 0 deletions stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Symfony\Component\Form;

/**
* @template TData
*
* @extends FormConfigInterface<TData>
*/
interface FormConfigBuilderInterface extends FormConfigInterface
{

}
16 changes: 16 additions & 0 deletions stubs/Symfony/Component/Form/FormConfigInterface.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Symfony\Component\Form;

/**
* @template TData
*/
interface FormConfigInterface
{

/**
* @return TData
*/
public function getData(): mixed;

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Symfony\Component\Form;
interface FormTypeExtensionInterface
{
/**
* @param FormBuilderInterface<TData|null> $builder
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;
Expand Down
1 change: 1 addition & 0 deletions stubs/Symfony/Component/Form/FormTypeInterface.stub
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Symfony\Component\Form;
interface FormTypeInterface
{
/**
* @param FormBuilderInterface<TData|null> $builder
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;
Expand Down
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)
);
}
}

0 comments on commit c9f5ceb

Please sign in to comment.