Skip to content

Commit a4d8502

Browse files
committed
Support for InputInterface::hasArgument and ::hasOption
1 parent 0cb3c0b commit a4d8502

5 files changed

+166
-2
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This extension provides following features:
1515
* Provides correct return type for `Envelope::all()` method based on the `$stampFqcn` parameter.
1616
* Notifies you when you try to get an unregistered service from the container.
1717
* Notifies you when you try to get a private service from the container.
18-
* Optionally correct return types for `InputInterface::getArgument()` and `::getOption`
18+
* Optionally correct return types for `InputInterface::getArgument()`, `::getOption`, `::hasArgument`, and `::hasOption`.
1919

2020
## Usage
2121

extension.neon

+10
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ services:
7676
factory: PHPStan\Type\Symfony\ArgumentTypeSpecifyingExtension
7777
tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension]
7878

79+
# InputInterface::hasArgument() return type
80+
-
81+
factory: PHPStan\Type\Symfony\InputInterfaceHasArgumentDynamicReturnTypeExtension
82+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
83+
7984
# InputInterface::getOption() return type
8085
-
8186
factory: PHPStan\Type\Symfony\InputInterfaceGetOptionDynamicReturnTypeExtension
@@ -85,3 +90,8 @@ services:
8590
-
8691
factory: PHPStan\Type\Symfony\OptionTypeSpecifyingExtension
8792
tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension]
93+
94+
# InputInterface::hasOption() return type
95+
-
96+
factory: PHPStan\Type\Symfony\InputInterfaceHasOptionDynamicReturnTypeExtension
97+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use InvalidArgumentException;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\Symfony\ConsoleApplicationResolver;
11+
use PHPStan\Type\BooleanType;
12+
use PHPStan\Type\Constant\ConstantBooleanType;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeUtils;
16+
use function array_unique;
17+
use function count;
18+
19+
final class InputInterfaceHasArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
20+
{
21+
22+
/** @var \PHPStan\Symfony\ConsoleApplicationResolver */
23+
private $consoleApplicationResolver;
24+
25+
public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
26+
{
27+
$this->consoleApplicationResolver = $consoleApplicationResolver;
28+
}
29+
30+
public function getClass(): string
31+
{
32+
return 'Symfony\Component\Console\Input\InputInterface';
33+
}
34+
35+
public function isMethodSupported(MethodReflection $methodReflection): bool
36+
{
37+
return $methodReflection->getName() === 'hasArgument';
38+
}
39+
40+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
41+
{
42+
$defaultReturnType = new BooleanType();
43+
44+
if (!isset($methodCall->args[0])) {
45+
return $defaultReturnType;
46+
}
47+
48+
$classReflection = $scope->getClassReflection();
49+
if ($classReflection === null) {
50+
throw new ShouldNotHappenException();
51+
}
52+
53+
$argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[0]->value));
54+
if (count($argStrings) !== 1) {
55+
return $defaultReturnType;
56+
}
57+
$argName = $argStrings[0]->getValue();
58+
59+
$returnTypes = [];
60+
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
61+
try {
62+
$command->getDefinition()->getArgument($argName);
63+
$returnTypes[] = true;
64+
} catch (InvalidArgumentException $e) {
65+
$returnTypes[] = false;
66+
}
67+
}
68+
69+
if (count($returnTypes) === 0) {
70+
return $defaultReturnType;
71+
}
72+
73+
$returnTypes = array_unique($returnTypes);
74+
return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : $defaultReturnType;
75+
}
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use InvalidArgumentException;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\Symfony\ConsoleApplicationResolver;
11+
use PHPStan\Type\BooleanType;
12+
use PHPStan\Type\Constant\ConstantBooleanType;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeUtils;
16+
use function array_unique;
17+
use function count;
18+
19+
final class InputInterfaceHasOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
20+
{
21+
22+
/** @var \PHPStan\Symfony\ConsoleApplicationResolver */
23+
private $consoleApplicationResolver;
24+
25+
public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
26+
{
27+
$this->consoleApplicationResolver = $consoleApplicationResolver;
28+
}
29+
30+
public function getClass(): string
31+
{
32+
return 'Symfony\Component\Console\Input\InputInterface';
33+
}
34+
35+
public function isMethodSupported(MethodReflection $methodReflection): bool
36+
{
37+
return $methodReflection->getName() === 'hasOption';
38+
}
39+
40+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
41+
{
42+
$defaultReturnType = new BooleanType();
43+
44+
if (!isset($methodCall->args[0])) {
45+
return $defaultReturnType;
46+
}
47+
48+
$classReflection = $scope->getClassReflection();
49+
if ($classReflection === null) {
50+
throw new ShouldNotHappenException();
51+
}
52+
53+
$optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[0]->value));
54+
if (count($optStrings) !== 1) {
55+
return $defaultReturnType;
56+
}
57+
$optName = $optStrings[0]->getValue();
58+
59+
$returnTypes = [];
60+
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
61+
try {
62+
$command->getDefinition()->getOption($optName);
63+
$returnTypes[] = true;
64+
} catch (InvalidArgumentException $e) {
65+
$returnTypes[] = false;
66+
}
67+
}
68+
69+
if (count($returnTypes) === 0) {
70+
return $defaultReturnType;
71+
}
72+
73+
$returnTypes = array_unique($returnTypes);
74+
return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : $defaultReturnType;
75+
}
76+
77+
}

tests/Symfony/NeonTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function testExtensionNeon(): void
4141
], $container->getParameters());
4242

4343
self::assertCount(6, $container->findByTag('phpstan.rules.rule'));
44-
self::assertCount(9, $container->findByTag('phpstan.broker.dynamicMethodReturnTypeExtension'));
44+
self::assertCount(11, $container->findByTag('phpstan.broker.dynamicMethodReturnTypeExtension'));
4545
self::assertCount(5, $container->findByTag('phpstan.typeSpecifier.methodTypeSpecifyingExtension'));
4646
self::assertInstanceOf(ServiceMap::class, $container->getByType(ServiceMap::class));
4747
}

0 commit comments

Comments
 (0)