diff --git a/extension.neon b/extension.neon index 59d9f3f6..aab376e8 100644 --- a/extension.neon +++ b/extension.neon @@ -12,6 +12,8 @@ parameters: - stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub - stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.stub - stubs/Symfony/Bundle/FrameworkBundle/Test/TestContainer.stub + - stubs/Symfony/Component/Console/Command.stub + - stubs/Symfony/Component/Console/Helper/HelperInterface.stub - stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub - stubs/Symfony/Component/DependencyInjection/ContainerBuilder.stub - stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub @@ -255,3 +257,8 @@ services: - factory: PHPStan\Type\Symfony\Form\FormInterfaceDynamicReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Command::getHelper() return type + - + factory: PHPStan\Type\Symfony\CommandGetHelperDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php new file mode 100644 index 00000000..e9b6ed7e --- /dev/null +++ b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php @@ -0,0 +1,71 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Command\Command'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getHelper'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $defaultReturnType = new ObjectType('Symfony\Component\Console\Helper\HelperInterface'); + + if (!isset($methodCall->getArgs()[0])) { + return $defaultReturnType; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return $defaultReturnType; + } + + $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + if (count($argStrings) !== 1) { + return $defaultReturnType; + } + $argName = $argStrings[0]->getValue(); + + $returnTypes = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { + try { + $command->mergeApplicationDefinition(); + $returnTypes[] = new ObjectType(get_class($command->getHelper($argName))); + } catch (Throwable $e) { + // no-op + } + } + + return count($returnTypes) > 0 ? TypeCombinator::union(...$returnTypes) : $defaultReturnType; + } + +} diff --git a/stubs/Symfony/Component/Console/Command.stub b/stubs/Symfony/Component/Console/Command.stub new file mode 100644 index 00000000..6bc2ea4b --- /dev/null +++ b/stubs/Symfony/Component/Console/Command.stub @@ -0,0 +1,11 @@ +|string', $input->getArgument('diff')); assertType('array', $input->getArgument('arr')); assertType('string|null', $input->getArgument('both')); + assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question')); } }