Skip to content

Commit 0cfea5c

Browse files
committed
Type specifying extensions for ContainerInterface|Controller::has()
1 parent 34b1940 commit 0cfea5c

9 files changed

+217
-36
lines changed

extension.neon

Lines changed: 12 additions & 1 deletion
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

Lines changed: 8 additions & 2 deletions
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

Lines changed: 1 addition & 16 deletions
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
@@ -38,20 +36,7 @@ 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;
44-
}
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;
39+
return Helper::getTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap);
5540
}
5641

5742
}
Lines changed: 49 additions & 0 deletions
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

Lines changed: 1 addition & 16 deletions
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
@@ -38,20 +36,7 @@ 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;
44-
}
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;
39+
return Helper::getTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap);
5540
}
5641

5742
}
Lines changed: 49 additions & 0 deletions
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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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\ObjectType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\VerbosityLevel;
18+
19+
final class Helper
20+
{
21+
22+
public static function getTypeFromMethodCall(
23+
MethodReflection $methodReflection,
24+
MethodCall $methodCall,
25+
Scope $scope,
26+
ServiceMap $serviceMap
27+
): Type
28+
{
29+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
30+
if (!isset($methodCall->args[0])) {
31+
return $returnType;
32+
}
33+
34+
$serviceId = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope);
35+
if ($serviceId !== null) {
36+
$service = $serviceMap->getService($serviceId);
37+
if ($service !== null && !$service->isSynthetic()) {
38+
return new ObjectType($service->getClass() ?? $serviceId);
39+
}
40+
}
41+
42+
return $returnType;
43+
}
44+
45+
public static function specifyTypes(
46+
MethodReflection $methodReflection,
47+
MethodCall $node,
48+
Scope $scope,
49+
TypeSpecifierContext $context,
50+
TypeSpecifier $typeSpecifier,
51+
Standard $printer
52+
): SpecifiedTypes
53+
{
54+
if (!isset($node->args[0])) {
55+
return new SpecifiedTypes();
56+
}
57+
$argType = $scope->getType($node->args[0]->value);
58+
return $typeSpecifier->create(
59+
self::createMarkerNode($node->var, $argType, $printer),
60+
$argType,
61+
$context
62+
);
63+
}
64+
65+
public static function createMarkerNode(Expr $expr, Type $type, Standard $printer): Expr
66+
{
67+
return new Expr\Variable(md5(sprintf(
68+
'%s::%s',
69+
$printer->prettyPrintExpr($expr),
70+
$type->describe(VerbosityLevel::value())
71+
)));
72+
}
73+
74+
}

tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php

Lines changed: 15 additions & 1 deletion
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

tests/Symfony/data/ExampleController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,12 @@ private function getTestContainer(): TestContainer
5656
{
5757
}
5858

59+
public function testGetUnknownServiceWithGuard(): void
60+
{
61+
if ($this->has('service.not.found')) {
62+
$service = $this->get('service.not.found');
63+
$service->noMethod();
64+
}
65+
}
66+
5967
}

0 commit comments

Comments
 (0)