diff --git a/extension.neon b/extension.neon index af69d8bf..531eb7df 100644 --- a/extension.neon +++ b/extension.neon @@ -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 @@ -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 @@ -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] diff --git a/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php new file mode 100644 index 00000000..7af50773 --- /dev/null +++ b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php @@ -0,0 +1,105 @@ +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; + } + +} diff --git a/stubs/Symfony/Component/Form/AbstractType.stub b/stubs/Symfony/Component/Form/AbstractType.stub index da2b1439..e99b746c 100644 --- a/stubs/Symfony/Component/Form/AbstractType.stub +++ b/stubs/Symfony/Component/Form/AbstractType.stub @@ -11,6 +11,7 @@ abstract class AbstractType implements FormTypeInterface { /** + * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void; diff --git a/stubs/Symfony/Component/Form/FormBuilderInterface.stub b/stubs/Symfony/Component/Form/FormBuilderInterface.stub index 5173bb64..fe578c50 100644 --- a/stubs/Symfony/Component/Form/FormBuilderInterface.stub +++ b/stubs/Symfony/Component/Form/FormBuilderInterface.stub @@ -3,9 +3,17 @@ namespace Symfony\Component\Form; /** - * @extends \Traversable + * @template TData + * + * @extends \Traversable> + * @extends FormConfigBuilderInterface */ -interface FormBuilderInterface extends \Traversable +interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface { + /** + * @return FormInterface + */ + public function getForm(): FormInterface; + } diff --git a/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub b/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub new file mode 100644 index 00000000..a167ce43 --- /dev/null +++ b/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub @@ -0,0 +1,13 @@ + + */ +interface FormConfigBuilderInterface extends FormConfigInterface +{ + +} diff --git a/stubs/Symfony/Component/Form/FormConfigInterface.stub b/stubs/Symfony/Component/Form/FormConfigInterface.stub new file mode 100644 index 00000000..942d467b --- /dev/null +++ b/stubs/Symfony/Component/Form/FormConfigInterface.stub @@ -0,0 +1,16 @@ + $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void; diff --git a/stubs/Symfony/Component/Form/FormTypeInterface.stub b/stubs/Symfony/Component/Form/FormTypeInterface.stub index 2f745283..8536656a 100644 --- a/stubs/Symfony/Component/Form/FormTypeInterface.stub +++ b/stubs/Symfony/Component/Form/FormTypeInterface.stub @@ -8,6 +8,7 @@ namespace Symfony\Component\Form; interface FormTypeInterface { /** + * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void; diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php index 879abb5d..73eb49b7 100644 --- a/tests/Type/Symfony/ExtensionTest.php +++ b/tests/Type/Symfony/ExtensionTest.php @@ -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'); } /** diff --git a/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php b/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php new file mode 100644 index 00000000..1e33cb37 --- /dev/null +++ b/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php @@ -0,0 +1,17 @@ +getConfiguration($configs, $container) + ); + } +}; diff --git a/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php b/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php new file mode 100644 index 00000000..614431f2 --- /dev/null +++ b/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php @@ -0,0 +1,23 @@ +getConfiguration($configs, $container) + ); + } + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return null; + } +} diff --git a/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php b/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php new file mode 100644 index 00000000..77c44003 --- /dev/null +++ b/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php @@ -0,0 +1,18 @@ +getConfiguration($configs, $container) + ); +} diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php new file mode 100644 index 00000000..4ff16c39 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php @@ -0,0 +1,16 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php new file mode 100644 index 00000000..b9d5bcc1 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php @@ -0,0 +1,16 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php new file mode 100644 index 00000000..8eea9eb9 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php @@ -0,0 +1,16 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/with-configuration/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration/Configuration.php new file mode 100644 index 00000000..4e8c51b5 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration/Configuration.php @@ -0,0 +1,12 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php b/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php new file mode 100644 index 00000000..dccec3e2 --- /dev/null +++ b/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php @@ -0,0 +1,17 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php index 524a5b7c..e0cefb88 100644 --- a/tests/Type/Symfony/data/form_data_type.php +++ b/tests/Type/Symfony/data/form_data_type.php @@ -29,6 +29,9 @@ class DataClassType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { + assertType('GenericFormDataType\DataClass|null', $builder->getData()); + assertType('GenericFormDataType\DataClass|null', $builder->getForm()->getData()); + $builder ->add('foo', NumberType::class) ->add('bar', TextType::class)