Skip to content

Commit d7d1624

Browse files
authored
Merge pull request #7 from phpstan/type-specifying-extensions
Extensions for ContainerInterface|Controller::has()
2 parents 34b1940 + 7857d54 commit d7d1624

12 files changed

+257
-38
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
This extension provides following features:
1010

11-
* Provides correct return type for `ContainerInterface::get()` method.
12-
* Provides correct return type for `Controller::get()` method.
11+
* Provides correct return type for `ContainerInterface::get()` and `::has()` methods.
12+
* Provides correct return type for `Controller::get()` and `::has()` methods.
1313
* Provides correct return type for `Request::getContent()` method based on the `$asResource` parameter.
1414
* Notifies you when you try to get an unregistered service from the container.
1515
* Notifies you when you try to get a private service from the container.

extension.neon

+12-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,15 @@ services:
1919
class: PHPStan\Rules\Symfony\ContainerInterfaceUnknownServiceRule
2020
tags:
2121
- phpstan.rules.rule
22-
- PHPStan\Symfony\ServiceMap(%symfony.container_xml_path%)
22+
-
23+
class: PHPStan\Type\Symfony\ContainerInterfaceMethodTypeSpecifyingExtension
24+
tags:
25+
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
26+
-
27+
class: PHPStan\Type\Symfony\ControllerMethodTypeSpecifyingExtension
28+
tags:
29+
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
30+
-
31+
class: PHPStan\Symfony\ServiceMap
32+
arguments:
33+
containerXml: %symfony.container_xml_path%

src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,26 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\PrettyPrinter\Standard;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Rules\Rule;
910
use PHPStan\Symfony\ServiceMap;
1011
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Symfony\Helper;
1113

1214
final class ContainerInterfaceUnknownServiceRule implements Rule
1315
{
1416

1517
/** @var ServiceMap */
1618
private $serviceMap;
1719

18-
public function __construct(ServiceMap $symfonyServiceMap)
20+
/** @var \PhpParser\PrettyPrinter\Standard */
21+
private $printer;
22+
23+
public function __construct(ServiceMap $symfonyServiceMap, Standard $printer)
1924
{
2025
$this->serviceMap = $symfonyServiceMap;
26+
$this->printer = $printer;
2127
}
2228

2329
public function getNodeType(): string
@@ -50,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array
5056
$serviceId = ServiceMap::getServiceIdFromNode($node->args[0]->value, $scope);
5157
if ($serviceId !== null) {
5258
$service = $this->serviceMap->getService($serviceId);
53-
if ($service === null) {
59+
if ($service === null && !$scope->isSpecified(Helper::createMarkerNode($node->var, $scope->getType($node->args[0]->value), $this->printer))) {
5460
return [sprintf('Service "%s" is not registered in the container.', $serviceId)];
5561
}
5662
}

src/Type/Symfony/ContainerInterfaceDynamicReturnTypeExtension.php

+7-16
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
use PhpParser\Node\Expr\MethodCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\MethodReflection;
8-
use PHPStan\Reflection\ParametersAcceptorSelector;
98
use PHPStan\Symfony\ServiceMap;
109
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11-
use PHPStan\Type\ObjectType;
1210
use PHPStan\Type\Type;
1311

1412
final class ContainerInterfaceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
@@ -29,7 +27,7 @@ public function getClass(): string
2927

3028
public function isMethodSupported(MethodReflection $methodReflection): bool
3129
{
32-
return $methodReflection->getName() === 'get';
30+
return in_array($methodReflection->getName(), ['get', 'has'], true);
3331
}
3432

3533
public function getTypeFromMethodCall(
@@ -38,20 +36,13 @@ public function getTypeFromMethodCall(
3836
Scope $scope
3937
): Type
4038
{
41-
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
42-
if (!isset($methodCall->args[0])) {
43-
return $returnType;
39+
switch ($methodReflection->getName()) {
40+
case 'get':
41+
return Helper::getGetTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap);
42+
case 'has':
43+
return Helper::getHasTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap);
4444
}
45-
46-
$serviceId = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope);
47-
if ($serviceId !== null) {
48-
$service = $this->serviceMap->getService($serviceId);
49-
if ($service !== null && !$service->isSynthetic()) {
50-
return new ObjectType($service->getClass() ?? $serviceId);
51-
}
52-
}
53-
54-
return $returnType;
45+
throw new \PHPStan\ShouldNotHappenException();
5546
}
5647

5748
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PhpParser\PrettyPrinter\Standard;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Analyser\SpecifiedTypes;
9+
use PHPStan\Analyser\TypeSpecifier;
10+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\MethodTypeSpecifyingExtension;
14+
15+
final class ContainerInterfaceMethodTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
16+
{
17+
18+
/** @var \PhpParser\PrettyPrinter\Standard */
19+
private $printer;
20+
21+
/** @var \PHPStan\Analyser\TypeSpecifier */
22+
private $typeSpecifier;
23+
24+
public function __construct(Standard $printer)
25+
{
26+
$this->printer = $printer;
27+
}
28+
29+
public function getClass(): string
30+
{
31+
return 'Symfony\Component\DependencyInjection\ContainerInterface';
32+
}
33+
34+
public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool
35+
{
36+
return $methodReflection->getName() === 'has' && !$context->null();
37+
}
38+
39+
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
40+
{
41+
return Helper::specifyTypes($methodReflection, $node, $scope, $context, $this->typeSpecifier, $this->printer);
42+
}
43+
44+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
45+
{
46+
$this->typeSpecifier = $typeSpecifier;
47+
}
48+
49+
}

src/Type/Symfony/ControllerDynamicReturnTypeExtension.php

+7-16
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
use PhpParser\Node\Expr\MethodCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\MethodReflection;
8-
use PHPStan\Reflection\ParametersAcceptorSelector;
98
use PHPStan\Symfony\ServiceMap;
109
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11-
use PHPStan\Type\ObjectType;
1210
use PHPStan\Type\Type;
1311

1412
final class ControllerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
@@ -29,7 +27,7 @@ public function getClass(): string
2927

3028
public function isMethodSupported(MethodReflection $methodReflection): bool
3129
{
32-
return $methodReflection->getName() === 'get';
30+
return in_array($methodReflection->getName(), ['get', 'has'], true);
3331
}
3432

3533
public function getTypeFromMethodCall(
@@ -38,20 +36,13 @@ public function getTypeFromMethodCall(
3836
Scope $scope
3937
): Type
4038
{
41-
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
42-
if (!isset($methodCall->args[0])) {
43-
return $returnType;
39+
switch ($methodReflection->getName()) {
40+
case 'get':
41+
return Helper::getGetTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap);
42+
case 'has':
43+
return Helper::getHasTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap);
4444
}
45-
46-
$serviceId = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope);
47-
if ($serviceId !== null) {
48-
$service = $this->serviceMap->getService($serviceId);
49-
if ($service !== null && !$service->isSynthetic()) {
50-
return new ObjectType($service->getClass() ?? $serviceId);
51-
}
52-
}
53-
54-
return $returnType;
45+
throw new \PHPStan\ShouldNotHappenException();
5546
}
5647

5748
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PhpParser\PrettyPrinter\Standard;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Analyser\SpecifiedTypes;
9+
use PHPStan\Analyser\TypeSpecifier;
10+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\MethodTypeSpecifyingExtension;
14+
15+
final class ControllerMethodTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
16+
{
17+
18+
/** @var \PhpParser\PrettyPrinter\Standard */
19+
private $printer;
20+
21+
/** @var \PHPStan\Analyser\TypeSpecifier */
22+
private $typeSpecifier;
23+
24+
public function __construct(Standard $printer)
25+
{
26+
$this->printer = $printer;
27+
}
28+
29+
public function getClass(): string
30+
{
31+
return 'Symfony\Bundle\FrameworkBundle\Controller\Controller';
32+
}
33+
34+
public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool
35+
{
36+
return $methodReflection->getName() === 'has' && !$context->null();
37+
}
38+
39+
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
40+
{
41+
return Helper::specifyTypes($methodReflection, $node, $scope, $context, $this->typeSpecifier, $this->printer);
42+
}
43+
44+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
45+
{
46+
$this->typeSpecifier = $typeSpecifier;
47+
}
48+
49+
}

src/Type/Symfony/Helper.php

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\PrettyPrinter\Standard;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Analyser\SpecifiedTypes;
10+
use PHPStan\Analyser\TypeSpecifier;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Reflection\ParametersAcceptorSelector;
14+
use PHPStan\Symfony\ServiceMap;
15+
use PHPStan\Type\Constant\ConstantBooleanType;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\VerbosityLevel;
19+
20+
final class Helper
21+
{
22+
23+
public static function getGetTypeFromMethodCall(
24+
MethodReflection $methodReflection,
25+
MethodCall $methodCall,
26+
Scope $scope,
27+
ServiceMap $serviceMap
28+
): Type
29+
{
30+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
31+
if (!isset($methodCall->args[0])) {
32+
return $returnType;
33+
}
34+
35+
$serviceId = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope);
36+
if ($serviceId !== null) {
37+
$service = $serviceMap->getService($serviceId);
38+
if ($service !== null && !$service->isSynthetic()) {
39+
return new ObjectType($service->getClass() ?? $serviceId);
40+
}
41+
}
42+
43+
return $returnType;
44+
}
45+
46+
public static function getHasTypeFromMethodCall(
47+
MethodReflection $methodReflection,
48+
MethodCall $methodCall,
49+
Scope $scope,
50+
ServiceMap $serviceMap
51+
): Type
52+
{
53+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
54+
if (!isset($methodCall->args[0])) {
55+
return $returnType;
56+
}
57+
58+
$serviceId = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope);
59+
if ($serviceId !== null) {
60+
$service = $serviceMap->getService($serviceId);
61+
return new ConstantBooleanType($service !== null && $service->isPublic());
62+
}
63+
64+
return $returnType;
65+
}
66+
67+
public static function specifyTypes(
68+
MethodReflection $methodReflection,
69+
MethodCall $node,
70+
Scope $scope,
71+
TypeSpecifierContext $context,
72+
TypeSpecifier $typeSpecifier,
73+
Standard $printer
74+
): SpecifiedTypes
75+
{
76+
if (!isset($node->args[0])) {
77+
return new SpecifiedTypes();
78+
}
79+
$argType = $scope->getType($node->args[0]->value);
80+
return $typeSpecifier->create(
81+
self::createMarkerNode($node->var, $argType, $printer),
82+
$argType,
83+
$context
84+
);
85+
}
86+
87+
public static function createMarkerNode(Expr $expr, Type $type, Standard $printer): Expr
88+
{
89+
return new Expr\Variable(md5(sprintf(
90+
'%s::%s',
91+
$printer->prettyPrintExpr($expr),
92+
$type->describe(VerbosityLevel::value())
93+
)));
94+
}
95+
96+
}

tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace PHPStan\Rules\Symfony;
44

5+
use PhpParser\PrettyPrinter\Standard;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Symfony\ServiceMap;
8+
use PHPStan\Type\Symfony\ContainerInterfaceMethodTypeSpecifyingExtension;
9+
use PHPStan\Type\Symfony\ControllerMethodTypeSpecifyingExtension;
710

811
final class ContainerInterfaceUnknownServiceRuleTest extends \PHPStan\Testing\RuleTestCase
912
{
@@ -12,7 +15,18 @@ protected function getRule(): Rule
1215
{
1316
$serviceMap = new ServiceMap(__DIR__ . '/../../Symfony/data/container.xml');
1417

15-
return new ContainerInterfaceUnknownServiceRule($serviceMap);
18+
return new ContainerInterfaceUnknownServiceRule($serviceMap, new Standard());
19+
}
20+
21+
/**
22+
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
23+
*/
24+
protected function getMethodTypeSpecifyingExtensions(): array
25+
{
26+
return [
27+
new ContainerInterfaceMethodTypeSpecifyingExtension(new Standard()),
28+
new ControllerMethodTypeSpecifyingExtension(new Standard()),
29+
];
1630
}
1731

1832
public function testGetUnknownService(): void

0 commit comments

Comments
 (0)