Skip to content

Commit

Permalink
Infer correct type for $input->getOptions() call
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 25, 2022
1 parent f4cb3b8 commit 70f8452
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 19 deletions.
7 changes: 7 additions & 0 deletions extension.neon
Expand Up @@ -175,10 +175,17 @@ services:
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

# InputInterface::getOption() return type
-
factory: PHPStan\Type\Symfony\GetOptionTypeHelper
-
factory: PHPStan\Type\Symfony\InputInterfaceGetOptionDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

# InputInterface::getOptions() return type
-
factory: PHPStan\Type\Symfony\InputInterfaceGetOptionsDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

# InputInterface::hasOption() type specification
-
factory: PHPStan\Type\Symfony\OptionTypeSpecifyingExtension
Expand Down
35 changes: 35 additions & 0 deletions src/Type/Symfony/GetOptionTypeHelper.php
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Symfony;

use PHPStan\Analyser\Scope;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Symfony\Component\Console\Input\InputOption;

class GetOptionTypeHelper
{

public function getOptionType(Scope $scope, InputOption $option): Type
{
if (!$option->acceptValue()) {
return new BooleanType();
}

$optType = TypeCombinator::union(new StringType(), new NullType());
if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) {
$optType = TypeCombinator::removeNull($optType);
}
if ($option->isArray()) {
$optType = new ArrayType(new IntegerType(), $optType);
}

return TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault()));
}

}
Expand Up @@ -8,12 +8,7 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
Expand All @@ -25,9 +20,13 @@ final class InputInterfaceGetOptionDynamicReturnTypeExtension implements Dynamic
/** @var ConsoleApplicationResolver */
private $consoleApplicationResolver;

public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
/** @var GetOptionTypeHelper */
private $getOptionTypeHelper;

public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, GetOptionTypeHelper $getOptionTypeHelper)
{
$this->consoleApplicationResolver = $consoleApplicationResolver;
$this->getOptionTypeHelper = $getOptionTypeHelper;
}

public function getClass(): string
Expand Down Expand Up @@ -64,19 +63,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
try {
$command->mergeApplicationDefinition();
$option = $command->getDefinition()->getOption($optName);
if (!$option->acceptValue()) {
$optType = new BooleanType();
} else {
$optType = TypeCombinator::union(new StringType(), new NullType());
if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) {
$optType = TypeCombinator::removeNull($optType);
}
if ($option->isArray()) {
$optType = new ArrayType(new IntegerType(), $optType);
}
$optType = TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault()));
}
$optTypes[] = $optType;
$optTypes[] = $this->getOptionTypeHelper->getOptionType($scope, $option);
} catch (InvalidArgumentException $e) {
// noop
}
Expand Down
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Symfony;

use InvalidArgumentException;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;

final class InputInterfaceGetOptionsDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{

/** @var ConsoleApplicationResolver */
private $consoleApplicationResolver;

/** @var GetOptionTypeHelper */
private $getOptionTypeHelper;

public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, GetOptionTypeHelper $getOptionTypeHelper)
{
$this->consoleApplicationResolver = $consoleApplicationResolver;
$this->getOptionTypeHelper = $getOptionTypeHelper;
}

public function getClass(): string
{
return 'Symfony\Component\Console\Input\InputInterface';
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'getOptions';
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return $defaultReturnType;
}

$optTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
$command->mergeApplicationDefinition();
$options = $command->getDefinition()->getOptions();
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($options as $name => $option) {
$optionType = $this->getOptionTypeHelper->getOptionType($scope, $option);
$builder->setOffsetValueType(new ConstantStringType($name), $optionType);
}

$optTypes[] = $builder->getArray();
} catch (InvalidArgumentException $e) {
// noop
}
}

return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : $defaultReturnType;
}

}
2 changes: 2 additions & 0 deletions tests/Type/Symfony/data/ExampleOptionCommand.php
Expand Up @@ -40,6 +40,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
assertType('1|string', $input->getOption('cc'));
assertType('array<int, 1|string|null>', $input->getOption('dd'));
assertType('array<int, 1|string>', $input->getOption('ee'));

assertType('array{a: bool, b: string|null, c: string|null, d: array<int, string|null>, e: array<int, string>, bb: 1|string|null, cc: 1|string, dd: array<int, 1|string|null>, ee: array<int, 1|string>, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool, no-interaction: bool}', $input->getOptions());
}

}
2 changes: 2 additions & 0 deletions tests/Type/Symfony/data/ExampleOptionLazyCommand.php
Expand Up @@ -42,6 +42,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
assertType('1|string', $input->getOption('cc'));
assertType('array<int, 1|string|null>', $input->getOption('dd'));
assertType('array<int, 1|string>', $input->getOption('ee'));

assertType('array{a: bool, b: string|null, c: string|null, d: array<int, string|null>, e: array<int, string>, bb: 1|string|null, cc: 1|string, dd: array<int, 1|string|null>, ee: array<int, 1|string>, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool, no-interaction: bool}', $input->getOptions());
}

}

0 comments on commit 70f8452

Please sign in to comment.