Skip to content

Commit 8e8fa73

Browse files
malarzmlookyman
authored andcommitted
Add Type extension for Controller::get
1 parent 83997c8 commit 8e8fa73

5 files changed

+147
-2
lines changed

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ services:
22
-
33
class: Lookyman\PHPStan\Symfony\Type\ContainerInterfaceDynamicReturnTypeExtension
44
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
5+
-
6+
class: Lookyman\PHPStan\Symfony\Type\ControllerDynamicReturnTypeExtension
7+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
58
-
69
class: Lookyman\PHPStan\Symfony\Rules\ContainerInterfacePrivateServiceRule
710
tags: [phpstan.rules.rule]

src/Rules/ContainerInterfacePrivateServiceRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array
3535
if ($node instanceof MethodCall && $node->name === 'get') {
3636
$type = $scope->getType($node->var);
3737
if ($type instanceof ObjectType
38-
&& $type->getClassName() === 'Symfony\Component\DependencyInjection\ContainerInterface'
38+
&& \in_array($type->getClassName(), ['Symfony\Component\DependencyInjection\ContainerInterface', 'Symfony\Bundle\FrameworkBundle\Controller\Controller'], \true)
3939
&& isset($node->args[0])
4040
&& $node->args[0] instanceof Arg
4141
) {

src/Rules/ContainerInterfaceUnknownServiceRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array
3535
if ($node instanceof MethodCall && $node->name === 'get') {
3636
$type = $scope->getType($node->var);
3737
if ($type instanceof ObjectType
38-
&& $type->getClassName() === 'Symfony\Component\DependencyInjection\ContainerInterface'
38+
&& \in_array($type->getClassName(), ['Symfony\Component\DependencyInjection\ContainerInterface', 'Symfony\Bundle\FrameworkBundle\Controller\Controller'], \true)
3939
&& isset($node->args[0])
4040
&& $node->args[0] instanceof Arg
4141
) {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Lookyman\PHPStan\Symfony\Type;
6+
7+
use Lookyman\PHPStan\Symfony\ServiceMap;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use PhpParser\Node\Arg;
14+
use PhpParser\Node\Expr\MethodCall;
15+
16+
final class ControllerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
17+
{
18+
19+
/**
20+
* @var ServiceMap
21+
*/
22+
private $serviceMap;
23+
24+
public function __construct(ServiceMap $symfonyServiceMap)
25+
{
26+
$this->serviceMap = $symfonyServiceMap;
27+
}
28+
29+
public function getClass(): string
30+
{
31+
return 'Symfony\Bundle\FrameworkBundle\Controller\Controller';
32+
}
33+
34+
public function isMethodSupported(MethodReflection $methodReflection): bool
35+
{
36+
return $methodReflection->getName() === 'get';
37+
}
38+
39+
public function getTypeFromMethodCall(
40+
MethodReflection $methodReflection,
41+
MethodCall $methodCall,
42+
Scope $scope
43+
): Type {
44+
if (isset($methodCall->args[0])
45+
&& $methodCall->args[0] instanceof Arg
46+
) {
47+
$service = $this->serviceMap->getServiceFromNode($methodCall->args[0]->value);
48+
if ($service !== \null && !$service['synthetic']) {
49+
return new ObjectType($service['class'] ?? $service['id']);
50+
}
51+
}
52+
return $methodReflection->getReturnType();
53+
}
54+
55+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Lookyman\PHPStan\Symfony\Type;
6+
7+
use Lookyman\PHPStan\Symfony\ServiceMap;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use PHPUnit\Framework\TestCase;
14+
use PhpParser\Node\Arg;
15+
use PhpParser\Node\Expr;
16+
use PhpParser\Node\Expr\MethodCall;
17+
use PhpParser\Node\Scalar\String_;
18+
19+
/**
20+
* @covers \Lookyman\PHPStan\Symfony\Type\ControllerDynamicReturnTypeExtension
21+
*/
22+
final class ControllerDynamicReturnTypeExtensionTest extends TestCase
23+
{
24+
25+
public function testImplementsDynamicMethodReturnTypeExtension()
26+
{
27+
self::assertInstanceOf(
28+
DynamicMethodReturnTypeExtension::class,
29+
new ControllerDynamicReturnTypeExtension(new ServiceMap(__DIR__ . '/../container.xml'))
30+
);
31+
}
32+
33+
public function testGetClass()
34+
{
35+
$extension = new ControllerDynamicReturnTypeExtension(new ServiceMap(__DIR__ . '/../container.xml'));
36+
self::assertEquals('Symfony\Bundle\FrameworkBundle\Controller\Controller', $extension->getClass());
37+
}
38+
39+
public function testIsMethodSupported()
40+
{
41+
$methodGet = $this->createMock(MethodReflection::class);
42+
$methodGet->expects(self::once())->method('getName')->willReturn('get');
43+
44+
$methodFoo = $this->createMock(MethodReflection::class);
45+
$methodFoo->expects(self::once())->method('getName')->willReturn('foo');
46+
47+
$extension = new ControllerDynamicReturnTypeExtension(new ServiceMap(__DIR__ . '/../container.xml'));
48+
self::assertTrue($extension->isMethodSupported($methodGet));
49+
self::assertFalse($extension->isMethodSupported($methodFoo));
50+
}
51+
52+
/**
53+
* @dataProvider getTypeFromMethodCallProvider
54+
*/
55+
public function testGetTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Type $expectedType)
56+
{
57+
$extension = new ControllerDynamicReturnTypeExtension(new ServiceMap(__DIR__ . '/../container.xml'));
58+
$type = $extension->getTypeFromMethodCall(
59+
$methodReflection,
60+
$methodCall,
61+
$this->createMock(Scope::class)
62+
);
63+
self::assertEquals($expectedType, $type);
64+
}
65+
66+
public function getTypeFromMethodCallProvider(): array
67+
{
68+
$notFoundType = $this->createMock(Type::class);
69+
70+
$methodReflectionNotFound = $this->createMock(MethodReflection::class);
71+
$methodReflectionNotFound->expects(self::once())->method('getReturnType')->willReturn($notFoundType);
72+
73+
return [
74+
'found' => [
75+
$this->createMock(MethodReflection::class),
76+
new MethodCall($this->createMock(Expr::class), '', [new Arg(new String_('withClass'))]),
77+
new ObjectType('Foo'),
78+
],
79+
'notFound' => [
80+
$methodReflectionNotFound,
81+
new MethodCall($this->createMock(Expr::class), ''),
82+
$notFoundType,
83+
],
84+
];
85+
}
86+
87+
}

0 commit comments

Comments
 (0)